"""SSH remote-exec + file-push + host bootstrap (the ``[remote]`` extra, paramiko).

Lets mgf-cloud stand a freshly-provisioned host up: run commands, push files, and
deploy a docker-compose bundle. Importing this module pulls **no** paramiko —
:func:`connect` imports it lazily, so the ``[remote]`` extra is only needed when
you actually connect. Tests + dry-runs use :class:`FakeRemoteClient` (in-memory).

Host-key policy is **reject-by-default**; pass ``accept_new=True`` for a brand-new
host (a documented MITM trade-off — the host key isn't known yet).
"""

from __future__ import annotations

import time
from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable

from mgf.common.observability import get_logger
from pydantic import BaseModel, ConfigDict

from mgf.cloud.exceptions import ConfigError, OperationError

if TYPE_CHECKING:
    from collections.abc import Mapping, Sequence

LOG = get_logger(__name__)

__all__ = [
    "DEFAULT_SSH_USER",
    "FakeRemoteClient",
    "RemoteClient",
    "RemoteHost",
    "RemoteResult",
    "connect",
    "deploy_compose_bundle",
    "install_docker",
    "run_commands",
]

DEFAULT_SSH_USER = "ubuntu"


class RemoteHost(BaseModel):
    """An SSH target."""

    model_config = ConfigDict(frozen=True)

    host: str
    user: str = DEFAULT_SSH_USER
    port: int = 22
    key_filename: str | None = None


class RemoteResult(BaseModel):
    """The outcome of one remote command."""

    model_config = ConfigDict(frozen=True)

    command: str
    exit_status: int
    stdout: str = ""
    stderr: str = ""

    @property
    def ok(self) -> bool:
        return self.exit_status == 0


@runtime_checkable
class RemoteClient(Protocol):
    """A connected remote host mgf-cloud can run commands on + push files to."""

    def exec(self, command: str) -> RemoteResult:
        """Run ``command`` and return its result."""
        ...

    def push_bytes(self, data: bytes, remote_path: str, mode: int = 0o644) -> None:
        """Write ``data`` to ``remote_path``."""
        ...

    def close(self) -> None:
        """Close the connection."""
        ...


class FakeRemoteClient:
    """In-memory :class:`RemoteClient` — records commands + files, returns success.

    Pass ``fail=("substr", …)`` to make any command containing a substring fail.
    """

    name = "fake"

    def __init__(self, *, fail: tuple[str, ...] = ()) -> None:
        self.commands: list[str] = []
        self.files: dict[str, bytes] = {}
        self.closed = False
        self._fail = fail

    def exec(self, command: str) -> RemoteResult:
        self.commands.append(command)
        failed = any(token in command for token in self._fail)
        return RemoteResult(
            command=command,
            exit_status=1 if failed else 0,
            stdout="" if failed else "ok",
            stderr="boom" if failed else "",
        )

    def push_bytes(self, data: bytes, remote_path: str, mode: int = 0o644) -> None:
        self.files[remote_path] = data

    def close(self) -> None:
        self.closed = True


class _ParamikoClient:
    """:class:`RemoteClient` backed by a connected paramiko ``SSHClient``."""

    def __init__(self, ssh: Any, host: RemoteHost) -> None:
        self._ssh = ssh
        self._host = host

    def exec(self, command: str) -> RemoteResult:
        _stdin, stdout, stderr = self._ssh.exec_command(command)
        out = stdout.read().decode("utf-8", "replace")
        err = stderr.read().decode("utf-8", "replace")
        status = int(stdout.channel.recv_exit_status())
        return RemoteResult(command=command, exit_status=status, stdout=out, stderr=err)

    def push_bytes(self, data: bytes, remote_path: str, mode: int = 0o644) -> None:
        sftp = self._ssh.open_sftp()
        try:
            with sftp.file(remote_path, "wb") as handle:
                handle.write(data)
            sftp.chmod(remote_path, mode)
        finally:
            sftp.close()

    def close(self) -> None:
        self._ssh.close()
        # SC-17 audit seam: an SSH session to a federation host was closed.
        LOG.info(
            "ssh.disconnect",
            extra={
                "audit": True,
                "operation": "ssh.disconnect",
                "host": self._host.host,
                "user": self._host.user,
            },
        )


def connect(host: RemoteHost, *, accept_new: bool = False, timeout: float = 30.0) -> RemoteClient:
    """Open an SSH connection to ``host`` (needs the ``[remote]`` extra).

    ``accept_new=False`` (default) rejects unknown host keys; ``True`` auto-adds
    them — use it only for a brand-new host whose key you can't yet know.
    """
    try:
        import paramiko
    except ImportError as exc:
        raise ConfigError(
            "the remote layer needs the [remote] extra: pip install 'mgf-cloud[remote]'"
        ) from exc
    ssh = paramiko.SSHClient()
    ssh.load_system_host_keys()
    if accept_new:
        ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())  # noqa: S507 - explicit opt-in for a fresh host
    else:
        ssh.set_missing_host_key_policy(paramiko.RejectPolicy())
    ssh.connect(
        hostname=host.host,
        port=host.port,
        username=host.user,
        key_filename=host.key_filename,
        timeout=timeout,
    )
    # SC-17 audit seam: an SSH session to a federation host was opened. Carries
    # host/user/port only — never a key or password.
    LOG.info(
        "ssh.connect",
        extra={
            "audit": True,
            "operation": "ssh.connect",
            "host": host.host,
            "user": host.user,
            "port": host.port,
        },
    )
    return _ParamikoClient(ssh, host)


def run_commands(
    client: RemoteClient, commands: Sequence[str], *, sudo: bool = False
) -> list[RemoteResult]:
    """Run ``commands`` in order, raising :class:`OperationError` on the first failure."""
    # LG-11: stable operation field (the verb), command detail kept separate.
    started = time.perf_counter()
    LOG.info(
        "remote.run_commands",
        extra={"operation": "remote.run_commands", "phase": "start", "count": len(commands)},
    )
    results: list[RemoteResult] = []
    try:
        for command in commands:
            full = f"sudo {command}" if sudo else command
            result = client.exec(full)
            results.append(result)
            if not result.ok:
                raise OperationError(
                    f"remote command failed (exit {result.exit_status}): {full}\n"
                    f"{result.stderr.strip()}"
                )
        return results
    finally:
        LOG.info(
            "remote.run_commands",
            extra={
                "operation": "remote.run_commands",
                "phase": "end",
                "duration_ms": round((time.perf_counter() - started) * 1000, 2),
                "ran": len(results),
            },
        )


# Idempotent Docker install (the canonical get.docker.com convenience script) +
# enable the service. Runs under sudo.
_DOCKER_INSTALL = (
    "command -v docker >/dev/null 2>&1 || (curl -fsSL https://get.docker.com | sh)",
    "systemctl enable --now docker",
)


def install_docker(client: RemoteClient) -> list[RemoteResult]:
    """Install Docker (idempotent) and enable the service."""
    return run_commands(client, _DOCKER_INSTALL, sudo=True)


def deploy_compose_bundle(
    client: RemoteClient,
    files: Mapping[str, bytes],
    remote_dir: str,
    *,
    up: bool = True,
) -> list[RemoteResult]:
    """Push a docker-compose bundle (``name -> bytes``) into ``remote_dir`` and bring it up."""
    results = run_commands(client, (f"mkdir -p {remote_dir}",), sudo=True)
    for name, data in files.items():
        client.push_bytes(data, f"{remote_dir}/{name}")
    if up:
        results += run_commands(client, (f"cd {remote_dir} && docker compose up -d",), sudo=True)
    return results
