"""
Getting the robot back after a power cycle.

The X2 cannot reliably bring the dashboard agent up on its own:

  * `systemd --user` units only run while the user has a login session, and the
    `agi` account is not allowed to enable lingering (`loginctl enable-linger`
    is denied, and sudo forbids running as root).
  * The cron fallback does not fire either - the robot's clock jumps backwards
    by several hours shortly after boot (RTC vs NTP), and cron stalls on a
    backward jump.

The dashboard host is the dependable machine in this setup, so recovery lives
here instead: find the robot wherever DHCP put it, and start the agent over SSH
if it is not already running.
"""

from __future__ import annotations

import asyncio
import time

from . import netinfo, x2_spec


class RecoveryError(Exception):
    pass


async def find_robot(agent_port: int, ssh_port: int = 22,
                     networks: list[str] | None = None) -> list[dict]:
    """
    Sweep the current subnets for anything that looks like the robot.

    A host already running the agent is the strongest signal; a host answering
    on SSH is a candidate we may be able to start the agent on.
    """
    cidrs = networks or netinfo.local_networks()
    found: list[dict] = []

    for cidr in cidrs[:3]:
        try:
            hosts = await netinfo.scan_subnet(cidr, ports=[agent_port, ssh_port], timeout=0.4)
        except (ValueError, Exception):
            continue
        for host in hosts:
            open_ports = host.get("open_ports") or []
            found.append({
                "host": host["host"],
                "hostname": host.get("hostname"),
                "agent": agent_port in open_ports,
                "ssh": ssh_port in open_ports,
                "latency_ms": host.get("latency_ms"),
            })

    # Anything already running the agent sorts first.
    found.sort(key=lambda h: (not h["agent"], not h["ssh"], h["host"]))
    return found


def _start_over_ssh_blocking(host: str, port: int, user: str, password: str,
                             command: str, timeout: float = 25.0) -> tuple[bool, str]:
    try:
        import paramiko
    except ImportError:
        return False, ("paramiko is not installed on the dashboard host - "
                       "run: pip install -r requirements.txt")

    client = paramiko.SSHClient()
    client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    try:
        client.connect(host, port=port, username=user, password=password,
                       timeout=timeout, allow_agent=False, look_for_keys=False,
                       banner_timeout=timeout, auth_timeout=timeout)
    except Exception as exc:
        return False, f"SSH to {user}@{host}:{port} failed: {type(exc).__name__}: {exc}"

    try:
        # The helper is idempotent: it exits immediately if the agent is already
        # listening, so running it when we raced a manual start is harmless.
        stdin, stdout, stderr = client.exec_command(command, timeout=timeout)
        stdout.channel.recv_exit_status()
        err = stderr.read().decode("utf-8", "replace").strip()
        if err and "warning" not in err.lower():
            return True, f"start command ran with stderr: {err[:200]}"
        return True, "start command ran"
    except Exception as exc:
        return False, f"Running the start command failed: {exc}"
    finally:
        client.close()


async def start_agent(host: str, config: dict) -> tuple[bool, str]:
    """Log in and run the agent starter. Returns (attempted_ok, message)."""
    user = (config.get("robot_ssh_user") or "").strip()
    password = config.get("robot_ssh_password") or ""
    command = (config.get("agent_start_command") or "").strip()
    ssh_port = int(config.get("robot_ssh_port") or 22)

    if not user or not command:
        return False, "SSH user or start command is not configured"
    if not password:
        return False, ("No SSH password saved. Add it in Settings so the dashboard can "
                       "start the agent after the robot reboots.")

    return await asyncio.to_thread(
        _start_over_ssh_blocking, host, ssh_port, user, password, command)


async def wait_for_agent(host: str, port: int, timeout: float = 45.0) -> bool:
    """Poll until the agent's port opens, or give up."""
    deadline = time.time() + timeout
    while time.time() < deadline:
        info = await netinfo.probe_host(host, ports=[port], timeout=1.0)
        if info.get("reachable"):
            return True
        await asyncio.sleep(2.0)
    return False
