"""Execution context for task functions."""

import os
import subprocess
import sys
import threading
import time
from typing import TYPE_CHECKING
import paramiko

from hil_testbench.run.output_streamer import OutputStreamer
from hil_testbench.run.task_logger import TaskLogger

if TYPE_CHECKING:
    from hil_testbench.config.task_config import (  # hil: allow-lazy - TYPE_CHECKING only
        TaskConfig,
    )

# Detect ConPTY environment early (VS Code with ConPTY enabled)
CONPTY_DETECTED = (
    os.environ.get("WT_SESSION") is not None
    or os.environ.get("TERM_PROGRAM") == "vscode"
)

# Platform-specific PTY support
# Skip WinPTY if ConPTY detected - they conflict and ConPTY is superior
try:
    import winpty  # hil: allow-lazy - optional platform-specific dependency

    if CONPTY_DETECTED:
        winpty = None  # Disable WinPTY in favor of ConPTY
except ImportError:
    winpty = None

# Unix PTY support (not available on Windows)
pty = None
select = None
if sys.platform != "win32":
    try:
        import pty  # hil: allow-lazy - optional platform-specific dependency
    except ImportError:
        pty = None

    try:
        import select  # hil: allow-lazy - optional platform-specific dependency
    except ImportError:
        select = None

# ConPTY (Windows Pseudo Console) is built into Windows 10 1809+ and modern terminal emulators
# When ConPTY is active, standard subprocess works well - no need for WinPTY
PTY_AVAILABLE = winpty is not None or pty is not None
PTY_MODE = "winpty" if winpty is not None else ("unix" if pty is not None else None)


class ExecutionContext:
    """
    Context object passed to task functions with run() method.
    """

    def __init__(
        self,
        logger: TaskLogger,
        ssh_client: paramiko.SSHClient | None,
        streamer: OutputStreamer | None,
        cancel_event: threading.Event | None,
        execution_dir: str,
        task_name: str,
        config: "TaskConfig",  # type: ignore
        use_pty: bool = False,
    ):
        self.ssh_client: paramiko.SSHClient | None = ssh_client
        self.streamer: OutputStreamer | None = streamer
        self.cancel_event: threading.Event | None = cancel_event
        self.execution_dir: str = execution_dir
        self.use_pty: bool = use_pty
        self.is_remote: bool = ssh_client is not None
        self._process = None
        self._channel = None  # Track SSH channel for remote execution
        self._remote_pid = None  # Track remote process PID
        self._pty_proc = None  # Track winpty process
        self._logger: TaskLogger = logger
        self.task_name: str = task_name
        self.config = config

    @property
    def has_active_process(self) -> bool:
        """Check if there is an active process running."""
        return (
            self._process is not None
            or self._channel is not None
            or self._remote_pid is not None
            or self._pty_proc is not None
        )

    def run(self, command: str, capture: bool | None = None, **kwargs) -> int:
        """
        Execute a command locally or remotely.

        Args:
            command: The command to execute
            capture: If True, capture output without streaming (immediate mode)
                    If None, use streamer if available

        Returns:
            Exit code of the command
        """
        if self.cancel_event and self.cancel_event.is_set():
            raise KeyboardInterrupt("Task cancelled")

        # Determine if we should use immediate mode
        immediate_mode = capture if capture is not None else (self.streamer is None)

        if self.is_remote:
            return self._run_remote(command, immediate_mode=immediate_mode, **kwargs)
        else:
            return self._run_local(command, immediate_mode=immediate_mode, **kwargs)

    def _run_local(self, command: str, immediate_mode: bool = False, **_kwargs) -> int:
        """Execute command locally using subprocess."""
        # Log the command being executed (command echo removed - redundant with shell_command_started event)
        self._logger.log_info(
            "shell_command_started", task=self.task_name, command=command, remote=False
        )

        # Check if PTY is disabled via config
        disable_pty = self.config.run_config.disable_pty if self.config else False

        # Use PTY if requested, available, and not disabled
        # Note: WinPTY is automatically disabled when ConPTY detected
        if self.use_pty and PTY_AVAILABLE and not disable_pty:
            if PTY_MODE == "winpty":
                return self._run_local_pty_windows(command, immediate_mode)
            elif PTY_MODE == "unix":
                return self._run_local_pty_unix(command, immediate_mode)

        # Fallback to standard subprocess
        return self._run_local_subprocess(command, immediate_mode)

    def _run_local_subprocess(self, command: str, immediate_mode: bool = False) -> int:
        """Execute command using standard subprocess (buffered output)."""
        try:
            # Use shell=True on Windows to properly handle commands
            shell = sys.platform == "win32"

            start_time: float = time.time()

            # Set PYTHONUNBUFFERED in environment for child process
            env = os.environ.copy()
            env["PYTHONUNBUFFERED"] = "1"

            self._process = subprocess.Popen(
                command,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                shell=shell,
                text=True,
                bufsize=1,  # Line buffered
                env=env,
            )

            # Log process creation
            self._logger.log_debug(
                "process_spawned",
                task=self.task_name,
                pid=self._process.pid,
                command=command[:100],  # Truncate long commands
                mode="local_subprocess",
            )

            if immediate_mode:
                # Immediate mode: capture output synchronously without streaming
                stdout, stderr = self._process.communicate()
                if self.streamer:
                    # Store captured output in buffers
                    for line in stdout.splitlines():
                        self.streamer.stdout_buffer.write(line + "\n")
                    for line in stderr.splitlines():
                        self.streamer.stderr_buffer.write(line + "\n")
                return self._process.returncode
            else:
                # Stream output in real-time
                def stream_output(pipe, is_error):
                    try:
                        for line in iter(pipe.readline, ""):
                            if not line:
                                break
                            line = line.rstrip()
                            if self.streamer:
                                self.streamer.process_line(line, is_error)
                    except Exception as e:  # pylint: disable=broad-except
                        self._logger.log_error(
                            "stream_error",
                            task=self.task_name,
                            error=str(e),
                            mode="local_subprocess",
                        )

                # Create threads for stdout and stderr
                stdout_thread = threading.Thread(
                    target=stream_output,
                    args=(self._process.stdout, False),
                    name=f"{self.task_name}_stdout",
                )
                stdout_thread.daemon = True
                stderr_thread = threading.Thread(
                    target=stream_output,
                    args=(self._process.stderr, True),
                    name=f"{self.task_name}_stderr",
                )
                stderr_thread.daemon = True

                # Log thread creation
                self._logger.log_debug(
                    "thread_created",
                    task=self.task_name,
                    thread_name=stdout_thread.name,
                    daemon=True,
                    purpose="stdout streaming",
                )
                self._logger.log_debug(
                    "thread_created",
                    task=self.task_name,
                    thread_name=stderr_thread.name,
                    daemon=True,
                    purpose="stderr streaming",
                )

                stdout_thread.start()
                stderr_thread.start()

                # Wait for process to complete
                return_code = self._process.wait()

                # Wait for streaming threads to finish
                stdout_thread.join(timeout=1)
                stderr_thread.join(timeout=1)

                # Log thread termination
                self._logger.log_debug(
                    "thread_terminated",
                    task=self.task_name,
                    thread_name=stdout_thread.name,
                    alive=stdout_thread.is_alive(),
                )
                self._logger.log_debug(
                    "thread_terminated",
                    task=self.task_name,
                    thread_name=stderr_thread.name,
                    alive=stderr_thread.is_alive(),
                )

                # Log process termination
                self._logger.log_debug(
                    "process_terminated",
                    task=self.task_name,
                    pid=self._process.pid,
                    exit_code=return_code,
                )

                duration: float = time.time() - start_time
                self._logger.log_info(
                    "shell_command_ended",
                    task=self.task_name,
                    exit_code=return_code,
                    duration=f"{duration:.2f}",
                    remote=False,
                )
                return return_code

        except (OSError, subprocess.SubprocessError, ValueError) as e:
            if self.streamer:
                self.streamer.process_line(f"Error executing command: {e}", True)
            self._logger.log_error(
                "command_error",
                task=self.task_name,
                error=str(e),
                remote=False,
            )
            raise

    def _run_local_pty_windows(self, command: str, immediate_mode: bool = False) -> int:
        """Execute command using winpty for unbuffered output (Windows only)."""
        if winpty is None:
            raise RuntimeError("winpty not available on this system")

        # Type assertion for mypy
        assert winpty is not None

        try:
            # Create PTY process
            self._pty_proc = winpty.PtyProcess.spawn(command)  # type: ignore[attr-defined]

            if self._pty_proc is None:
                raise RuntimeError("Failed to create PTY process")

            # Log process creation
            self._logger.log_debug(
                "process_spawned",
                task=self.task_name,
                pid=getattr(self._pty_proc, "pid", "unknown"),
                command=command[:100],
                mode="local_pty_windows",
            )

            if immediate_mode:
                start_time: float = time.time()
                # Read all output at once
                output = []
                while self._pty_proc.isalive():  # type: ignore[attr-defined]
                    try:
                        line = self._pty_proc.readline()  # type: ignore[attr-defined]
                        if line:
                            output.append(line.rstrip("\r\n"))
                    except EOFError:
                        break

                if self.streamer:
                    for line in output:
                        self.streamer.stdout_buffer.write(line + "\n")

                # Wait for process to finish
                self._pty_proc.wait()
                exit_code = (
                    self._pty_proc.exitstatus
                    if self._pty_proc.exitstatus is not None
                    else 0
                )
                duration: float = time.time() - start_time
                self._logger.log_info(
                    "shell_command_ended",
                    task=self.task_name,
                    exit_code=exit_code,
                    duration=f"{duration:.2f}",
                    remote=False,
                    pty=True,
                )
                return exit_code
            else:
                # Stream output in real-time
                def stream_pty_output():
                    try:
                        while self._pty_proc.isalive():  # type: ignore[attr-defined]
                            try:
                                line = self._pty_proc.readline()  # type: ignore[attr-defined]
                                if line:
                                    line = line.rstrip("\r\n")
                                    if self.streamer:
                                        self.streamer.process_line(line, False)
                            except EOFError:
                                break
                    except Exception as e:  # pylint: disable=broad-except
                        self._logger.log_error(
                            "stream_error",
                            task=self.task_name,
                            error=str(e),
                            mode="local_pty_windows_streaming",
                        )

                # Create thread for output streaming
                output_thread = threading.Thread(
                    target=stream_pty_output, name=f"{self.task_name}_pty_output"
                )
                output_thread.daemon = True

                # Log thread creation
                self._logger.log_debug(
                    "thread_created",
                    task=self.task_name,
                    thread_name=output_thread.name,
                    daemon=True,
                    purpose="PTY output streaming",
                )

                output_thread.start()

                start_time = time.time()
                # Wait for process to complete
                self._pty_proc.wait()

                # Wait for streaming thread to finish
                output_thread.join(timeout=1)

                # Log thread and process termination
                self._logger.log_debug(
                    "thread_terminated",
                    task=self.task_name,
                    thread_name=output_thread.name,
                    alive=output_thread.is_alive(),
                )
                self._logger.log_debug(
                    "process_terminated",
                    task=self.task_name,
                    pid=getattr(self._pty_proc, "pid", "unknown"),
                    exit_code=(
                        self._pty_proc.exitstatus
                        if self._pty_proc.exitstatus is not None
                        else 0
                    ),
                )

                exit_code = (
                    self._pty_proc.exitstatus
                    if self._pty_proc.exitstatus is not None
                    else 0
                )
                duration = time.time() - start_time
                self._logger.log_info(
                    "shell_command_ended",
                    task=self.task_name,
                    exit_code=exit_code,
                    duration=f"{duration:.2f}",
                    remote=False,
                    pty=True,
                )
                return exit_code

        except Exception as e:  # pylint: disable=broad-except
            if self.streamer:
                self.streamer.process_line(
                    f"Error executing command with PTY: {e}", True
                )
            self._logger.log_error(
                "command_error",
                task=self.task_name,
                error=str(e),
                remote=False,
                pty=True,
                mode="windows",
            )
            raise

    def _run_local_pty_unix(self, command: str, immediate_mode: bool = False) -> int:
        """Execute command using Unix PTY for unbuffered output (Linux/macOS)."""
        if pty is None:
            # Fall back to standard subprocess if PTY not available
            return self._run_local_subprocess(command, immediate_mode)

        if select is None:
            # Fall back to standard subprocess if select not available
            # This can happen on some systems where select is not implemented
            return self._run_local_subprocess(command, immediate_mode)

        # Type assertions for mypy (now guaranteed to be non-None)
        assert pty is not None
        assert select is not None

        try:
            # Create master and slave PTY file descriptors
            master_fd, slave_fd = pty.openpty()  # type: ignore[attr-defined]

            # Use shell to execute command
            self._process = subprocess.Popen(
                command,
                stdin=slave_fd,
                stdout=slave_fd,
                stderr=slave_fd,
                shell=True,
                text=False,  # Binary mode for PTY
                close_fds=True,
            )

            # Log process creation
            self._logger.log_debug(
                "process_spawned",
                task=self.task_name,
                pid=self._process.pid,
                command=command[:100],
                mode="local_pty_unix",
            )

            # Close slave fd in parent (child keeps it)
            os.close(slave_fd)

            if immediate_mode:
                start_time = time.time()
                # Read all output at once
                output = []
                while True:
                    try:
                        ready, _, _ = select.select([master_fd], [], [], 0.1)  # type: ignore[attr-defined,union-attr]
                        if ready:
                            data = os.read(master_fd, 4096)
                            if not data:
                                break
                            lines = data.decode("utf-8", errors="replace").splitlines()
                            output.extend(lines)
                        elif self._process.poll() is not None:
                            # Process finished, read any remaining data
                            try:
                                data = os.read(master_fd, 4096)
                                if data:
                                    lines = data.decode(
                                        "utf-8", errors="replace"
                                    ).splitlines()
                                    output.extend(lines)
                            except OSError:
                                pass
                            break
                    except OSError:
                        break

                if self.streamer:
                    for line in output:
                        self.streamer.stdout_buffer.write(line + "\\n")

                os.close(master_fd)
                return_code = self._process.wait()
                duration = time.time() - start_time
                self._logger.log_info(
                    "shell_command_ended",
                    task=self.task_name,
                    exit_code=return_code,
                    duration=f"{duration:.2f}",
                    remote=False,
                    pty=True,
                )
                return return_code
            else:
                # Stream output in real-time
                def stream_pty_output():
                    try:
                        while True:
                            ready, _, _ = select.select([master_fd], [], [], 0.1)  # type: ignore[attr-defined,union-attr]
                            if ready:
                                data = os.read(master_fd, 4096)
                                if not data:
                                    break
                                lines = data.decode("utf-8", errors="replace").split(
                                    "\\n"
                                )
                                for line in lines:
                                    if line and self.streamer:
                                        self.streamer.process_line(
                                            line.rstrip("\\r"), False
                                        )
                            elif (
                                self._process is not None
                                and self._process.poll() is not None
                            ):
                                # Process finished, read any remaining data
                                try:
                                    data = os.read(master_fd, 4096)
                                    if data:
                                        lines = data.decode(
                                            "utf-8", errors="replace"
                                        ).split("\\n")
                                        for line in lines:
                                            if line and self.streamer:
                                                self.streamer.process_line(
                                                    line.rstrip("\\r"), False
                                                )
                                except OSError:
                                    pass
                                break
                    except Exception as e:  # pylint: disable=broad-except
                        self._logger.log_error(
                            "stream_error",
                            task=self.task_name,
                            error=str(e),
                            mode="local_pty_unix",
                        )
                    finally:
                        try:
                            os.close(master_fd)
                        except OSError:
                            pass

                # Create thread for output streaming
                output_thread = threading.Thread(
                    target=stream_pty_output, name=f"{self.task_name}_pty_unix"
                )
                output_thread.daemon = True

                # Log thread creation
                self._logger.log_debug(
                    "thread_created",
                    task=self.task_name,
                    thread_name=output_thread.name,
                    daemon=True,
                    purpose="PTY output streaming (Unix)",
                )

                output_thread.start()

                start_time = time.time()
                # Wait for process to complete
                return_code = self._process.wait()

                # Wait for streaming thread to finish
                output_thread.join(timeout=1)

                # Log thread and process termination
                self._logger.log_debug(
                    "thread_terminated",
                    task=self.task_name,
                    thread_name=output_thread.name,
                    alive=output_thread.is_alive(),
                )
                self._logger.log_debug(
                    "process_terminated",
                    task=self.task_name,
                    pid=self._process.pid,
                    exit_code=return_code,
                )

                duration = time.time() - start_time
                self._logger.log_info(
                    "shell_command_ended",
                    task=self.task_name,
                    exit_code=return_code,
                    duration=f"{duration:.2f}",
                    remote=False,
                    pty=True,
                )
                return return_code

        except (OSError, ValueError) as e:
            if self.streamer:
                self.streamer.process_line(
                    f"Error executing command with Unix PTY: {e}", True
                )
            self._logger.log_error(
                "command_error",
                task=self.task_name,
                error=str(e),
                remote=False,
                pty=True,
            )
            raise

    def _run_remote(self, command: str, immediate_mode: bool = False, **_kwargs) -> int:
        """Execute command remotely using SSH."""
        # Log the command being executed (command echo removed - redundant with shell_command_started event)
        self._logger.log_info(
            "shell_command_started", task=self.task_name, command=command, remote=True
        )

        try:
            # Ensure SSH client exists
            if self.ssh_client is None:
                raise RuntimeError("SSH client is not initialized for remote execution")

            # Wrap command to track PID and enable killing
            # This allows us to send kill signals to the remote process
            wrapped_command = f"sh -c 'echo $$; exec {command}'"

            start_time = time.time()
            # Request PTY to get unbuffered output from remote commands
            _, stdout, stderr = self.ssh_client.exec_command(
                wrapped_command, get_pty=True
            )
            self._channel = stdout.channel  # Track channel for cancellation

            # Log remote channel creation
            self._logger.log_debug(
                "remote_channel_created",
                task=self.task_name,
                command=command[:100],
            )

            # Read the PID from first line of output
            pid_line = stdout.readline().strip()
            try:
                self._remote_pid = int(pid_line)
                self._logger.log_debug(
                    "remote_pid_captured",
                    task=self.task_name,
                    pid=self._remote_pid,
                )
            except (ValueError, AttributeError):
                self._remote_pid = None
                self._logger.log_warning(
                    "remote_pid_missing",
                    task=self.task_name,
                    raw_line=pid_line,
                )

            if immediate_mode:
                # Immediate mode: read all output synchronously
                stdout_data = stdout.read().decode("utf-8", errors="replace")
                stderr_data = stderr.read().decode("utf-8", errors="replace")

                if self.streamer:
                    # Store captured output in buffers
                    for line in stdout_data.splitlines():
                        self.streamer.stdout_buffer.write(line + "\n")
                    for line in stderr_data.splitlines():
                        self.streamer.stderr_buffer.write(line + "\n")

                exit_code = stdout.channel.recv_exit_status()
                duration = time.time() - start_time
                self._logger.log_info(
                    "shell_command_ended",
                    task=self.task_name,
                    exit_code=exit_code,
                    duration=f"{duration:.2f}",
                    remote=True,
                    pid=self._remote_pid,
                    immediate=True,
                )
                return exit_code
            else:
                # Stream output in real-time with proper line buffering
                def stream_channel_output():
                    channel = stdout.channel
                    stdout_buffer = ""
                    stderr_buffer = ""

                    while not channel.exit_status_ready():
                        if self.cancel_event and self.cancel_event.is_set():
                            break

                        # Read from stdout
                        if channel.recv_ready():
                            data = channel.recv(4096).decode("utf-8", errors="replace")
                            stdout_buffer += data
                            # Process complete lines
                            while "\n" in stdout_buffer:
                                line, stdout_buffer = stdout_buffer.split("\n", 1)
                                if self.streamer:
                                    self.streamer.process_line(line, False)

                        # Read from stderr
                        if channel.recv_stderr_ready():
                            data = channel.recv_stderr(4096).decode(
                                "utf-8", errors="replace"
                            )
                            stderr_buffer += data
                            # Process complete lines
                            while "\n" in stderr_buffer:
                                line, stderr_buffer = stderr_buffer.split("\n", 1)
                                if self.streamer:
                                    self.streamer.process_line(line, True)

                        # Small sleep to avoid tight polling when no data
                        if not channel.recv_ready() and not channel.recv_stderr_ready():
                            time.sleep(0.01)

                    # Process any remaining buffered lines
                    if stdout_buffer and self.streamer:
                        for line in stdout_buffer.splitlines():
                            if line:
                                self.streamer.process_line(line, False)

                    if stderr_buffer and self.streamer:
                        for line in stderr_buffer.splitlines():
                            if line:
                                self.streamer.process_line(line, True)

                    # Read any remaining output from channel
                    remaining_stdout = stdout.read().decode("utf-8", errors="replace")
                    for line in remaining_stdout.splitlines():
                        if line and self.streamer:
                            self.streamer.process_line(line, False)

                    remaining_stderr = stderr.read().decode("utf-8", errors="replace")
                    for line in remaining_stderr.splitlines():
                        if line and self.streamer:
                            self.streamer.process_line(line, True)

                stream_channel_output()
                exit_code = stdout.channel.recv_exit_status()

                # Log remote process termination
                self._logger.log_debug(
                    "remote_process_terminated",
                    task=self.task_name,
                    pid=self._remote_pid,
                    exit_code=exit_code,
                )

                duration = time.time() - start_time
                self._logger.log_info(
                    "shell_command_ended",
                    task=self.task_name,
                    exit_code=exit_code,
                    duration=f"{duration:.2f}",
                    remote=True,
                    pid=self._remote_pid,
                )
                return exit_code

        except (paramiko.SSHException, OSError) as e:
            if self.streamer:
                self.streamer.process_line(f"Error executing command: {e}", True)
                self._logger.log_error(
                    "command_error",
                    task=self.task_name,
                    error=str(e),
                    remote=True,
                )
            raise

    def kill(self):
        """Kill the running process/command."""
        self._logger.log_info("kill_request", task=self.task_name)
        # Kill local process
        if self._process:
            self._process.terminate()
            try:
                self._process.wait(timeout=2)
            except subprocess.TimeoutExpired:
                self._logger.log_warning(
                    "kill_local_timeout",
                    task=self.task_name,
                    pid=self._process.pid,
                )
                self._process.kill()
                try:
                    self._process.wait(timeout=1)
                except subprocess.TimeoutExpired:
                    self._logger.log_warning(
                        "kill_local_failed",
                        task=self.task_name,
                        pid=self._process.pid,
                    )
                    # Process is truly stuck, nothing more we can do

        # Kill remote SSH process
        if self.ssh_client and self._remote_pid:
            try:
                # Send SIGTERM first
                self.ssh_client.exec_command(f"kill {self._remote_pid}")
                time.sleep(0.5)
                # Force kill if still running
                self.ssh_client.exec_command(
                    f"kill -9 {self._remote_pid} 2>/dev/null || true"
                )
                time.sleep(0.3)  # Give it time to die
                self._logger.log_info(
                    "kill_remote_sent", task=self.task_name, pid=self._remote_pid
                )
            except Exception:  # pylint: disable=broad-except
                self._logger.log_warning(
                    "kill_remote_exception",
                    task=self.task_name,
                    pid=self._remote_pid,
                )

        # Close remote SSH channel
        if self._channel:
            try:
                self._channel.close()
            except Exception:  # pylint: disable=broad-except
                pass  # Channel may already be closed
