"""Command executor for MakePython."""

from __future__ import annotations

import subprocess
import time
from pathlib import Path
from typing import Dict, List, Optional, Union

from .._tool_utils import DEFAULT_ENCODING
from ..interfaces.command import CommandResult
from .context import ExecutionContext


class CommandExecutor:
    """Handles the execution of commands with standardized processing."""

    def __init__(self, context: ExecutionContext):
        """Initialize command executor with execution context.

        Args:
            context: Execution context containing config, logger, etc.
        """
        self.context = context
        self.logger = context.logger

    def execute_shell_command(
        self,
        cmd: Union[str, List[str]],
        cwd: Optional[Path] = None,
        *,
        check: bool = False,
        timeout: Optional[int] = None,
        capture_output: bool = False,
        env: Optional[Dict[str, str]] = None,
    ) -> CommandResult:
        """Execute a shell command with standardized processing.

        Args:
            cmd: Command to execute (as string or list of arguments)
            cwd: Working directory (defaults to context's cwd)
            check: Whether to raise exception on non-zero exit code
            timeout: Command timeout in seconds
            capture_output: Whether to capture and return output
            env: Environment variables to use (merged with context env)

        Returns
        -------
            CommandResult with execution status and output
        """
        # Prepare command
        if isinstance(cmd, str):
            import shlex

            cmd_list = shlex.split(cmd)
        else:
            cmd_list = cmd

        # Prepare working directory
        working_dir = cwd or self.context.cwd

        # Prepare environment
        if env is None:
            exec_env = self.context.get_full_env()
        else:
            exec_env = self.context.get_full_env().copy()
            exec_env.update(env)

        # Log command execution
        cmd_str = " ".join(cmd_list)
        self.logger.info(f"Executing command: {cmd_str} in {working_dir}")

        start_time = time.time()

        try:
            if capture_output:
                # Execute with captured output
                result = subprocess.run(
                    cmd_list,
                    cwd=working_dir,
                    check=check,
                    timeout=timeout,
                    capture_output=True,
                    text=True,
                    encoding=DEFAULT_ENCODING,
                    env=exec_env,
                )

                elapsed_time = time.time() - start_time
                self.logger.debug(f"Command completed in {elapsed_time:.2f}s")

                return CommandResult(
                    success=True,
                    message=result.stdout,
                    data={
                        "stdout": result.stdout,
                        "stderr": result.stderr,
                        "returncode": result.returncode,
                        "elapsed_time": elapsed_time,
                    },
                    exit_code=result.returncode,
                )
            # Execute with real-time output
            self.logger.info(f"Running command: {cmd_str}")

            process = subprocess.Popen(
                cmd_list,
                cwd=working_dir,
                stdout=subprocess.PIPE,
                stderr=subprocess.STDOUT,
                text=True,
                encoding=DEFAULT_ENCODING,
                errors="replace",
                bufsize=1,
                universal_newlines=True,
                env=exec_env,
            )

            # Stream output in real-time
            output_lines = []
            if process.stdout:
                for line in process.stdout:
                    line_stripped = line.rstrip()
                    output_lines.append(line_stripped)
                    self.logger.info(line_stripped)  # Also log the output

            # Wait for completion
            return_code = process.wait(timeout=timeout)
            elapsed_time = time.time() - start_time

            output_str = "\n".join(output_lines)

            if check and return_code != 0:
                error_msg = f"Command failed with exit code {return_code}: {cmd_str}"
                self.logger.error(error_msg)
                return CommandResult(
                    success=False,
                    message=output_str,
                    data={
                        "stdout": output_str,
                        "returncode": return_code,
                        "elapsed_time": elapsed_time,
                    },
                    exit_code=return_code,
                )

            return CommandResult(
                success=True,
                message=output_str,
                data={
                    "stdout": output_str,
                    "returncode": return_code,
                    "elapsed_time": elapsed_time,
                },
                exit_code=return_code,
            )

        except subprocess.TimeoutExpired as e:
            elapsed_time = time.time() - start_time
            error_msg = f"Command timed out after {timeout}s: {cmd_str}"
            self.logger.error(error_msg)
            return CommandResult(
                success=False,
                message=error_msg,
                data={
                    "stdout": "",
                    "stderr": str(e),
                    "returncode": -1,
                    "elapsed_time": elapsed_time,
                },
                exit_code=-1,
            )

        except subprocess.CalledProcessError as e:
            elapsed_time = time.time() - start_time
            error_msg = f"Command failed with exit code {e.returncode}: {cmd_str}"
            self.logger.error(error_msg)
            return CommandResult(
                success=False,
                message=str(e),
                data={
                    "stdout": e.output or "",
                    "stderr": e.stderr or "",
                    "returncode": e.returncode,
                    "elapsed_time": elapsed_time,
                },
                exit_code=e.returncode,
            )

        except Exception as e:
            elapsed_time = time.time() - start_time
            error_msg = f"Command execution failed: {e}"
            self.logger.error(error_msg)
            return CommandResult(
                success=False,
                message=error_msg,
                data={
                    "stdout": "",
                    "stderr": str(e),
                    "returncode": -1,
                    "elapsed_time": elapsed_time,
                },
                exit_code=-1,
            )

    def execute_with_retry(
        self,
        cmd: Union[str, List[str]],
        cwd: Optional[Path] = None,
        *,
        retries: int = 3,
        delay: float = 1.0,
        check: bool = True,
        timeout: Optional[int] = None,
    ) -> CommandResult:
        """Execute a command with retry logic.

        Args:
            cmd: Command to execute
            cwd: Working directory
            retries: Number of retry attempts
            delay: Delay between retries in seconds
            check: Whether to raise exception on non-zero exit code
            timeout: Command timeout in seconds

        Returns
        -------
            CommandResult with final execution status
        """
        last_result = None

        for attempt in range(retries + 1):
            if attempt > 0:
                self.logger.info(
                    f"Retry {attempt}/{retries} for command: {' '.join(cmd if isinstance(cmd, list) else [cmd])}"
                )
                time.sleep(delay)

            result = self.execute_shell_command(cmd, cwd, check=check, timeout=timeout)
            last_result = result

            if result.success:
                if attempt > 0:
                    self.logger.info(f"Command succeeded on attempt {attempt + 1}")
                return result

        # All retries exhausted
        self.logger.error(f"Command failed after {retries + 1} attempts")
        return last_result


def execute_command_with_context(
    cmd: Union[str, List[str]],
    context: ExecutionContext,
    *,
    check: bool = False,
    capture_output: bool = False,
    timeout: Optional[int] = None,
) -> CommandResult:
    """Execute a command with a given context.

    Args:
        cmd: Command to execute
        context: Execution context
        check: Whether to raise exception on non-zero exit code
        timeout: Command timeout in seconds

    Returns
    -------
        CommandResult with execution status
    """
    executor = CommandExecutor(context)
    return executor.execute_shell_command(cmd, check=check, timeout=timeout, capture_output=capture_output)
