"""Manager for remote host operations."""

from __future__ import annotations

from datetime import datetime
from pathlib import Path

from gaol.remote.exceptions import (
    ConfigSyncError,
    HostExistsError,
    HostNotFoundError,
)
from gaol.remote.executor import SSHExecutor
from gaol.remote.models import (
    HostConfig,
    HostConnectionStatus,
    HostState,
    RemoteCommandResult,
    StoredHost,
)
from gaol.remote.store import HostStore


class RemoteManager:
    """Manage remote hosts and execute commands on them.

    Handles host registration, connection checking, and remote command execution.

    Example:
        >>> manager = RemoteManager()
        >>> config = HostConfig(
        ...     name="production",
        ...     hostname="prod.example.com",
        ...     user="deploy",
        ...     key_path="~/.ssh/prod_key"
        ... )
        >>> manager.add_host(config)
        >>> manager.check_host("production")
        >>> result = manager.exec_gaol("production", ["list"])
    """

    def __init__(
        self,
        store: HostStore | None = None,
        executor: SSHExecutor | None = None,
    ) -> None:
        """Initialize the remote manager.

        Args:
            store: Optional custom store for persistence
            executor: Optional custom SSH executor
        """
        self._store = store or HostStore()
        self._executor = executor or SSHExecutor()

    # Host management

    def add_host(
        self,
        config: HostConfig,
        *,
        verify: bool = True,
    ) -> StoredHost:
        """Add a new remote host.

        Args:
            config: Host configuration
            verify: If True, verify connectivity before adding

        Returns:
            Stored host with initial state

        Raises:
            HostExistsError: If host name already exists
            SSHConnectionError: If verify=True and connection fails
        """
        if self._store.host_exists(config.name):
            raise HostExistsError(config.name)

        # Create initial state
        state = HostState()

        # Verify connectivity if requested
        if verify:
            reachable, error = self._executor.check_connection(config)
            if reachable:
                state.status = HostConnectionStatus.REACHABLE
                state.last_check = datetime.now()
                state.last_successful_connection = datetime.now()

                # Check gaol version
                mat_ok, version_or_error = self._executor.check_gaol(config)
                if mat_ok:
                    state.gaol_version = version_or_error
            else:
                state.status = HostConnectionStatus.UNREACHABLE
                state.last_check = datetime.now()
                state.error_message = error

        # Create and save host
        host = StoredHost(
            config=config,
            state=state,
            created_at=datetime.now(),
            updated_at=datetime.now(),
        )
        self._store.save_host(host)

        return host

    def remove_host(self, name: str) -> bool:
        """Remove a host from the registry.

        Args:
            name: Host name

        Returns:
            True if removed, False if not found
        """
        return self._store.delete_host(name)

    def get_host(self, name: str) -> StoredHost:
        """Get a host by name.

        Args:
            name: Host name

        Returns:
            Stored host

        Raises:
            HostNotFoundError: If host doesn't exist
        """
        host = self._store.get_host(name)
        if not host:
            raise HostNotFoundError(name)
        return host

    def list_hosts(self) -> list[StoredHost]:
        """List all registered hosts.

        Returns:
            List of stored hosts
        """
        return self._store.list_hosts()

    def update_host(self, config: HostConfig) -> StoredHost:
        """Update an existing host's configuration.

        Args:
            config: New host configuration (name must match existing host)

        Returns:
            Updated stored host

        Raises:
            HostNotFoundError: If host doesn't exist
        """
        existing = self._store.get_host(config.name)
        if not existing:
            raise HostNotFoundError(config.name)

        # Update with new config, preserving state
        updated = StoredHost(
            config=config,
            state=existing.state,
            created_at=existing.created_at,
            updated_at=datetime.now(),
        )
        self._store.save_host(updated)

        return updated

    def check_host(self, name: str) -> HostState:
        """Check connectivity and gaol availability on a host.

        Args:
            name: Host name

        Returns:
            Updated host state

        Raises:
            HostNotFoundError: If host doesn't exist
        """
        host = self.get_host(name)
        config = host.config

        # Check SSH connectivity
        reachable, error = self._executor.check_connection(config)

        if reachable:
            host.update_state(HostConnectionStatus.REACHABLE)

            # Check gaol version
            mat_ok, version_or_error = self._executor.check_gaol(config)
            if mat_ok:
                host.state.gaol_version = version_or_error
        else:
            # Determine error type
            if error and "authentication" in error.lower():
                host.update_state(HostConnectionStatus.AUTH_FAILED, error_message=error)
            elif error and "timed out" in error.lower():
                host.update_state(HostConnectionStatus.TIMEOUT, error_message=error)
            else:
                host.update_state(HostConnectionStatus.UNREACHABLE, error_message=error)

        # Save updated state
        self._store.save_host(host)

        return host.state

    # Remote command execution

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

        Args:
            host_name: Host name
            command: Command and arguments
            timeout: Command timeout in seconds
            check: If True, raise on non-zero exit

        Returns:
            Command result

        Raises:
            HostNotFoundError: If host doesn't exist
            RemoteCommandError: If check=True and command fails
        """
        host = self.get_host(host_name)
        return self._executor.execute(
            host.config,
            command,
            timeout=timeout,
            check=check,
        )

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

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

        Returns:
            Command result

        Raises:
            HostNotFoundError: If host doesn't exist
        """
        host = self.get_host(host_name)
        return self._executor.execute_shell(
            host.config,
            command,
            timeout=timeout,
            check=check,
        )

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

        Args:
            host_name: Host name
            args: Gaol command arguments
            json_output: If True, add --json flag and parse output
            timeout: Command timeout in seconds

        Returns:
            Command result with parsed JSON if applicable

        Raises:
            HostNotFoundError: If host doesn't exist
            GaolNotFoundError: If gaol not installed
        """
        host = self.get_host(host_name)
        return self._executor.exec_gaol(
            host.config,
            args,
            json_output=json_output,
            timeout=timeout,
        )

    # Remote container operations

    def list_containers(
        self,
        host_name: str,
        *,
        all: bool = False,
    ) -> list[dict]:
        """List containers on a remote host.

        Args:
            host_name: Host name
            all: Include stopped containers

        Returns:
            List of container info dicts
        """
        args = ["list"]
        if all:
            args.append("--all")

        result = self.exec_gaol(host_name, args)

        if result.parsed_json:
            return result.parsed_json if isinstance(result.parsed_json, list) else []
        return []

    def start_container(self, host_name: str, container_name: str) -> None:
        """Start a container on a remote host.

        Args:
            host_name: Host name
            container_name: Container name
        """
        self.exec_gaol(host_name, ["start", container_name])

    def stop_container(
        self,
        host_name: str,
        container_name: str,
        *,
        timeout: int = 10,
    ) -> None:
        """Stop a container on a remote host.

        Args:
            host_name: Host name
            container_name: Container name
            timeout: Stop timeout in seconds
        """
        self.exec_gaol(
            host_name,
            ["stop", container_name, "--timeout", str(timeout)],
        )

    def container_status(
        self,
        host_name: str,
        container_name: str,
    ) -> dict | None:
        """Get container status from a remote host.

        Args:
            host_name: Host name
            container_name: Container name

        Returns:
            Container status dict or None if not found
        """
        result = self.exec_gaol(host_name, ["status", container_name])
        return result.parsed_json if isinstance(result.parsed_json, dict) else None

    def exec_in_container(
        self,
        host_name: str,
        container_name: str,
        command: list[str],
        *,
        timeout: int | None = None,
    ) -> RemoteCommandResult:
        """Execute a command in a container on a remote host.

        Args:
            host_name: Host name
            container_name: Container name
            command: Command to execute
            timeout: Command timeout

        Returns:
            Command result
        """
        args = ["exec", container_name, "--"]
        args.extend(command)
        return self.exec_gaol(
            host_name,
            args,
            json_output=False,
            timeout=timeout,
        )

    # Host installation / bootstrapping

    def install_host(
        self,
        host_name: str,
        runtimes: list[str] | None = None,
    ) -> list[str]:
        """Install gaol and prerequisites on a remote host.

        Bootstraps the remote with uv, gaol, and runtime prerequisites
        (debootstrap, systemd-container, docker, etc).

        Args:
            host_name: Registered host name
            runtimes: Runtimes to set up (default: ["nspawn"])
            callback: Optional callable(step: str, status: str) for progress

        Returns:
            List of steps completed
        """
        host = self.get_host(host_name)
        runtimes = runtimes or ["nspawn"]
        completed: list[str] = []

        def _run(desc: str, cmd: str, *, check: bool = True) -> RemoteCommandResult:
            result = self._executor.execute_shell(host.config, cmd, timeout=300)
            if check and result.exit_code != 0:
                raise RuntimeError(f"{desc} failed: {result.stderr.strip()}")
            completed.append(desc)
            return result

        # Detect OS
        result = self._executor.execute_shell(
            host.config, "cat /etc/os-release", timeout=10
        )
        os_info = result.stdout
        is_debian = "debian" in os_info.lower() or "ubuntu" in os_info.lower()
        is_rpm = "fedora" in os_info.lower() or "rhel" in os_info.lower()

        if not is_debian and not is_rpm:
            raise RuntimeError(
                f"Unsupported OS on {host_name}. Only Debian/Ubuntu and Fedora/RHEL supported."
            )

        pkg_install = "DEBIAN_FRONTEND=noninteractive apt-get install -y -qq" if is_debian else "dnf install -y -q"
        pkg_update = "apt-get update -qq" if is_debian else "dnf check-update || true"

        # Update package lists
        _run("update packages", f"sudo {pkg_update}")

        # Install base prerequisites
        _run(
            "install base tools",
            f"sudo {pkg_install} curl ca-certificates",
        )

        # Install uv if not present
        result = self._executor.execute_shell(
            host.config, "command -v uv", timeout=5
        )
        if result.exit_code != 0:
            _run(
                "install uv",
                "curl -LsSf https://astral.sh/uv/install.sh | sh",
            )
            # Add to PATH for this session
            _run(
                "verify uv",
                'export PATH="$HOME/.local/bin:$PATH" && uv --version',
            )

        # Install gaol via uv
        result = self._executor.execute_shell(
            host.config,
            'export PATH="$HOME/.local/bin:$PATH" && command -v gaol',
            timeout=5,
        )
        if result.exit_code != 0:
            _run(
                "install gaol",
                'export PATH="$HOME/.local/bin:$PATH" && uv tool install gaol',
            )

        # Runtime-specific prerequisites
        if "nspawn" in runtimes:
            nspawn_pkgs = "systemd-container debootstrap"
            _run(
                "install nspawn prerequisites",
                f"sudo {pkg_install} {nspawn_pkgs}",
            )
            # Enable systemd-networkd for container networking
            _run(
                "enable systemd-networkd",
                "sudo systemctl enable --now systemd-networkd || true",
            )

        if "docker" in runtimes:
            if is_debian:
                _run(
                    "install docker",
                    f"sudo {pkg_install} docker.io",
                )
            else:
                _run(
                    "install docker",
                    f"sudo {pkg_install} docker",
                )
            _run(
                "enable docker",
                "sudo systemctl enable --now docker",
            )
            # Add user to docker group
            _run(
                "add user to docker group",
                f"sudo usermod -aG docker {host.config.user}",
            )

        if "libvirt" in runtimes:
            if is_debian:
                _run(
                    "install libvirt",
                    f"sudo {pkg_install} qemu-kvm libvirt-daemon-system virtinst",
                )
            else:
                _run(
                    "install libvirt",
                    f"sudo {pkg_install} qemu-kvm libvirt virt-install",
                )
            _run(
                "enable libvirtd",
                "sudo systemctl enable --now libvirtd",
            )

        # Set up passwordless sudo for gaol
        gaol_path = self._executor.execute_shell(
            host.config,
            'export PATH="$HOME/.local/bin:$PATH" && which gaol',
            timeout=5,
        ).stdout.strip()
        if gaol_path:
            _run(
                "configure sudo for gaol",
                f'echo "{host.config.user} ALL=(ALL) NOPASSWD: {gaol_path}" '
                f"| sudo tee /etc/sudoers.d/gaol-{host.config.user} > /dev/null "
                f"&& sudo chmod 440 /etc/sudoers.d/gaol-{host.config.user}",
            )

        # Update host config with correct gaol_path
        if gaol_path and gaol_path != host.config.gaol_path:
            new_config = HostConfig(
                name=host.config.name,
                hostname=host.config.hostname,
                user=host.config.user,
                port=host.config.port,
                key_path=host.config.key_path,
                key_secret_name=host.config.key_secret_name,
                connect_timeout=host.config.connect_timeout,
                strict_host_key_checking=host.config.strict_host_key_checking,
                gaol_path=gaol_path,
                use_sudo=host.config.use_sudo,
                description=host.config.description,
                tags=host.config.tags,
            )
            self.update_host(new_config)

        # Verify installation
        ok, version = self._executor.check_gaol(host.config)
        if ok:
            completed.append(f"verified gaol {version}")
            host.state.gaol_version = version
        else:
            # Try with updated path
            if gaol_path:
                updated_host = self.get_host(host_name)
                ok, version = self._executor.check_gaol(updated_host.config)
                if ok:
                    completed.append(f"verified gaol {version}")

        return completed

    # Config sync operations

    def sync_config_to(
        self,
        container_name: str,
        host_name: str,
        *,
        create: bool = True,
    ) -> None:
        """Sync a container configuration to a remote host.

        Args:
            container_name: Local container name
            host_name: Target host name
            create: Create container on remote if doesn't exist

        Raises:
            ConfigSyncError: If sync fails
        """
        from gaol.config import get_config_dir

        host = self.get_host(host_name)

        # Get local config path
        local_config = get_config_dir() / "containers" / f"{container_name}.yaml"
        if not local_config.exists():
            raise ConfigSyncError("sync_to", f"Local config not found: {container_name}")

        # Copy config to remote
        remote_config_dir = "~/.config/gaol/containers"
        remote_config = f"{remote_config_dir}/{container_name}.yaml"

        try:
            # Ensure remote directory exists
            self._executor.execute_shell(
                host.config,
                f"mkdir -p {remote_config_dir}",
            )

            # Copy config file
            self._executor.copy_to(host.config, local_config, remote_config)

        except Exception as e:
            raise ConfigSyncError("sync_to", str(e)) from e

    def sync_config_from(
        self,
        container_name: str,
        host_name: str,
    ) -> Path:
        """Sync a container configuration from a remote host.

        Args:
            container_name: Remote container name
            host_name: Source host name

        Returns:
            Path to synced local config

        Raises:
            ConfigSyncError: If sync fails
        """
        from gaol.config import get_config_dir

        host = self.get_host(host_name)

        # Remote config path
        remote_config = f"~/.config/gaol/containers/{container_name}.yaml"

        # Local config path
        local_config_dir = get_config_dir() / "containers"
        local_config_dir.mkdir(parents=True, exist_ok=True)
        local_config = local_config_dir / f"{container_name}.yaml"

        try:
            self._executor.copy_from(host.config, remote_config, local_config)
            return local_config
        except Exception as e:
            raise ConfigSyncError("sync_from", str(e)) from e


# Global instance
_manager: RemoteManager | None = None


def get_remote_manager() -> RemoteManager:
    """Get the global remote manager instance."""
    global _manager
    if _manager is None:
        _manager = RemoteManager()
    return _manager
