"""SSH command execution for remote hosts."""

from __future__ import annotations

import json
import subprocess
import time
from pathlib import Path

from gaol.remote.exceptions import (
    KeyNotFoundError,
    GaolNotFoundError,
    RemoteCommandError,
    SSHAuthenticationError,
    SSHConnectionError,
    SSHTimeoutError,
)
from gaol.remote.models import HostConfig, RemoteCommandResult


class SSHExecutor:
    """Execute commands on remote hosts via SSH.

    Example:
        >>> executor = SSHExecutor()
        >>> config = HostConfig(name="prod", hostname="prod.example.com", user="deploy")
        >>> result = executor.execute(config, ["ls", "-la"])
        >>> print(result.stdout)
    """

    def build_ssh_command(self, host: HostConfig) -> list[str]:
        """Build the base SSH command with options.

        Args:
            host: Host configuration

        Returns:
            List of command parts for SSH connection

        Raises:
            KeyNotFoundError: If key_path is specified but doesn't exist
        """
        cmd = ["ssh"]

        # Connection timeout
        cmd.extend(["-o", f"ConnectTimeout={host.connect_timeout}"])

        # Batch mode to prevent password prompts
        cmd.extend(["-o", "BatchMode=yes"])

        # Host key checking
        if not host.strict_host_key_checking:
            cmd.extend(["-o", "StrictHostKeyChecking=no"])
            cmd.extend(["-o", "UserKnownHostsFile=/dev/null"])

        # Suppress warnings about adding host to known hosts
        cmd.extend(["-o", "LogLevel=ERROR"])

        # SSH key
        if host.key_path:
            key_path = Path(host.key_path).expanduser()
            if not key_path.exists():
                raise KeyNotFoundError(str(key_path))
            cmd.extend(["-i", str(key_path)])

        # Port
        cmd.extend(["-p", str(host.port)])

        # User@hostname
        cmd.append(host.ssh_destination)

        return cmd

    def build_scp_command(
        self,
        host: HostConfig,
        *,
        recursive: bool = False,
    ) -> list[str]:
        """Build the base SCP command with options.

        Args:
            host: Host configuration
            recursive: Whether to copy directories recursively

        Returns:
            List of command parts for SCP
        """
        cmd = ["scp"]

        # Connection timeout
        cmd.extend(["-o", f"ConnectTimeout={host.connect_timeout}"])

        # Batch mode
        cmd.extend(["-o", "BatchMode=yes"])

        # Host key checking
        if not host.strict_host_key_checking:
            cmd.extend(["-o", "StrictHostKeyChecking=no"])
            cmd.extend(["-o", "UserKnownHostsFile=/dev/null"])

        # Suppress warnings
        cmd.extend(["-o", "LogLevel=ERROR"])

        # SSH key
        if host.key_path:
            key_path = Path(host.key_path).expanduser()
            if key_path.exists():
                cmd.extend(["-i", str(key_path)])

        # Port
        cmd.extend(["-P", str(host.port)])

        # Recursive
        if recursive:
            cmd.append("-r")

        return cmd

    def execute(
        self,
        host: HostConfig,
        command: list[str],
        *,
        timeout: int | None = None,
        check: bool = False,
    ) -> RemoteCommandResult:
        """Execute a command on a remote host.

        Args:
            host: Host configuration
            command: Command and arguments to execute
            timeout: Command timeout in seconds (default: no timeout)
            check: If True, raise RemoteCommandError on non-zero exit

        Returns:
            Command result with stdout, stderr, exit code

        Raises:
            SSHConnectionError: If connection fails
            SSHAuthenticationError: If authentication fails
            SSHTimeoutError: If connection times out
            RemoteCommandError: If check=True and command fails
            KeyNotFoundError: If SSH key file not found
        """
        ssh_cmd = self.build_ssh_command(host)
        ssh_cmd.extend(command)

        start_time = time.time()

        try:
            result = subprocess.run(
                ssh_cmd,
                capture_output=True,
                text=True,
                timeout=timeout,
            )
        except subprocess.TimeoutExpired:
            duration_ms = int((time.time() - start_time) * 1000)
            raise SSHTimeoutError(host.hostname, timeout or 0)
        except Exception as e:
            raise SSHConnectionError(host.hostname, str(e))

        duration_ms = int((time.time() - start_time) * 1000)

        # Check for SSH-specific errors
        if result.returncode == 255:
            stderr = result.stderr.lower()
            if "permission denied" in stderr or "authentication" in stderr:
                raise SSHAuthenticationError(host.hostname, host.user)
            elif "connection refused" in stderr:
                raise SSHConnectionError(host.hostname, "Connection refused")
            elif "connection timed out" in stderr or "timed out" in stderr:
                raise SSHTimeoutError(host.hostname, host.connect_timeout)
            elif "no route to host" in stderr:
                raise SSHConnectionError(host.hostname, "No route to host")
            elif "name or service not known" in stderr:
                raise SSHConnectionError(host.hostname, "Host not found")
            else:
                raise SSHConnectionError(host.hostname, result.stderr.strip())

        cmd_result = RemoteCommandResult(
            host_name=host.name,
            exit_code=result.returncode,
            stdout=result.stdout,
            stderr=result.stderr,
            duration_ms=duration_ms,
        )

        if check and result.returncode != 0:
            raise RemoteCommandError(
                host.name,
                " ".join(command),
                result.returncode,
                result.stderr.strip(),
            )

        return cmd_result

    def execute_shell(
        self,
        host: HostConfig,
        command: str,
        *,
        timeout: int | None = None,
        check: bool = False,
    ) -> RemoteCommandResult:
        """Execute a shell command on a remote host.

        Wraps the command in a shell for proper handling of pipes, redirects, etc.

        Args:
            host: Host configuration
            command: Shell command string
            timeout: Command timeout in seconds
            check: If True, raise RemoteCommandError on non-zero exit

        Returns:
            Command result
        """
        return self.execute(host, ["bash", "-c", command], timeout=timeout, check=check)

    def exec_gaol(
        self,
        host: HostConfig,
        args: list[str],
        *,
        json_output: bool = True,
        timeout: int | None = None,
        sudo: bool | None = None,
    ) -> RemoteCommandResult:
        """Execute a gaol command on a remote host.

        Args:
            host: Host configuration
            args: Gaol command arguments (e.g., ["list", "--all"])
            json_output: If True, add --json flag and parse output
            timeout: Command timeout in seconds
            sudo: If True, prefix with sudo. None uses host.use_sudo default.

        Returns:
            Command result with parsed_json if json_output=True

        Raises:
            GaolNotFoundError: If gaol is not installed on remote
        """
        use_sudo = sudo if sudo is not None else host.use_sudo
        command = ["sudo", "-n", host.gaol_path] if use_sudo else [host.gaol_path]

        if json_output:
            command.append("--json")

        command.extend(args)

        result = self.execute(host, command, timeout=timeout)

        # Check if gaol was found
        if result.exit_code == 127 or "command not found" in result.stderr.lower():
            raise GaolNotFoundError(host.name, host.gaol_path)

        # Parse JSON output if requested
        if json_output and result.exit_code == 0 and result.stdout.strip():
            try:
                parsed = json.loads(result.stdout)
                # Create new result with parsed JSON
                result = RemoteCommandResult(
                    host_name=result.host_name,
                    exit_code=result.exit_code,
                    stdout=result.stdout,
                    stderr=result.stderr,
                    duration_ms=result.duration_ms,
                    parsed_json=parsed,
                )
            except json.JSONDecodeError:
                # JSON parsing failed, return result without parsed_json
                pass

        return result

    def check_connection(self, host: HostConfig) -> tuple[bool, str | None]:
        """Check if a host is reachable via SSH.

        Args:
            host: Host configuration

        Returns:
            Tuple of (reachable, error_message)
        """
        try:
            result = self.execute(host, ["echo", "ok"], timeout=host.connect_timeout + 5)
            if result.exit_code == 0 and "ok" in result.stdout:
                return True, None
            return False, f"Unexpected response: {result.stdout}"
        except SSHAuthenticationError as e:
            return False, f"Authentication failed: {e.message}"
        except SSHTimeoutError as e:
            return False, f"Connection timed out: {e.message}"
        except SSHConnectionError as e:
            return False, f"Connection failed: {e.message}"
        except Exception as e:
            return False, str(e)

    def check_gaol(self, host: HostConfig) -> tuple[bool, str | None]:
        """Check if gaol is installed and get version.

        Args:
            host: Host configuration

        Returns:
            Tuple of (available, version_or_error)
        """
        try:
            cmd = (
                ["sudo", "-n", host.gaol_path, "--version"]
                if host.use_sudo
                else [host.gaol_path, "--version"]
            )
            result = self.execute(
                host,
                cmd,
                timeout=host.connect_timeout + 5,
            )
            if result.exit_code == 0:
                # Extract version from output
                version = result.stdout.strip()
                return True, version
            return False, result.stderr.strip() or "Unknown error"
        except GaolNotFoundError:
            return False, f"gaol not found at: {host.gaol_path}"
        except Exception as e:
            return False, str(e)

    def copy_to(
        self,
        host: HostConfig,
        local_path: str | Path,
        remote_path: str,
        *,
        recursive: bool = False,
    ) -> None:
        """Copy file(s) to remote host.

        Args:
            host: Host configuration
            local_path: Local file or directory path
            remote_path: Remote destination path
            recursive: Copy directories recursively

        Raises:
            SSHConnectionError: If copy fails
        """
        scp_cmd = self.build_scp_command(host, recursive=recursive)
        scp_cmd.append(str(local_path))
        scp_cmd.append(f"{host.ssh_destination}:{remote_path}")

        result = subprocess.run(scp_cmd, capture_output=True, text=True)

        if result.returncode != 0:
            raise SSHConnectionError(host.hostname, f"SCP failed: {result.stderr}")

    def copy_from(
        self,
        host: HostConfig,
        remote_path: str,
        local_path: str | Path,
        *,
        recursive: bool = False,
    ) -> None:
        """Copy file(s) from remote host.

        Args:
            host: Host configuration
            remote_path: Remote file or directory path
            local_path: Local destination path
            recursive: Copy directories recursively

        Raises:
            SSHConnectionError: If copy fails
        """
        scp_cmd = self.build_scp_command(host, recursive=recursive)
        scp_cmd.append(f"{host.ssh_destination}:{remote_path}")
        scp_cmd.append(str(local_path))

        result = subprocess.run(scp_cmd, capture_output=True, text=True)

        if result.returncode != 0:
            raise SSHConnectionError(host.hostname, f"SCP failed: {result.stderr}")
