"""Swarm announce loop — this Navi's heartbeat to the hive registry.

The hive (see ``hive/``) is the swarm's address book: a tiny standalone service
on the main server. Each Navi announces itself (name, uuid, address, machine
info) every ANNOUNCE_INTERVAL_SEC seconds.

Design rules:
  - The hive is NEVER a blocking dependency. Startup, agent turns and all
    tools work with the hive down; the loop just keeps trying on its next tick.
  - The Navi must KNOW about reachability, though: state transitions are logged
    (up→down, down→up — not every tick), /health carries the status, and the
    ``hive_status`` context provider tells the agent when the registry is down
    so it can act (this system is supposed to fix its own network).
"""

from __future__ import annotations

import asyncio
import os
import platform
import socket
from datetime import datetime, timezone
from pathlib import Path

import httpx
import structlog

from navi import __version__
from navi.identity import InstanceIdentity

log = structlog.get_logger()


def read_swarm_key(path: str | Path) -> str | None:
    """The swarm PSK from a key file, or None if the file is missing/empty.

    A ``.swarm-key.previous`` file next to the main key is NOT read here — the
    rotation window applies to the receiving side (hive/peer endpoints accept
    both); the announcer always sends the current key.
    """
    try:
        value = Path(path).read_text(encoding="utf-8").strip()
    except OSError:
        return None
    return value or None


def _machine_meta() -> dict:
    """Basic machine facts for the address book (the mini knowledge base)."""
    meta: dict = {
        "hostname": socket.gethostname(),
        "os": f"{platform.system()} {platform.release()}",
        "cpu_cores": os.cpu_count(),
    }
    try:  # Linux-only, cheap; absent fields are fine
        with open("/proc/meminfo") as f:
            for line in f:
                if line.startswith("MemTotal:"):
                    meta["ram_mb"] = round(int(line.split()[1]) / 1024)
                    break
    except OSError:
        pass
    return meta


def _now_iso() -> str:
    return datetime.now(timezone.utc).isoformat(timespec="seconds")


class HiveAnnouncer:
    """Periodic announce loop with reachable/unreachable state tracking."""

    def __init__(
        self,
        hive_url: str,
        key: str,
        identity: InstanceIdentity,
        port: int,
        interval: int = 120,
        client: httpx.AsyncClient | None = None,
    ) -> None:
        self._hive_url = hive_url.rstrip("/")
        self._key = key
        self._identity = identity
        self._port = port
        self._interval = interval
        self._client = client  # injectable for tests; created lazily otherwise
        # state
        self._reachable: bool | None = None  # None = never announced yet
        self._last_success: str | None = None
        self._last_error: str | None = None
        self._down_since: str | None = None
        self._consecutive_failures = 0

    @property
    def payload(self) -> dict:
        return {
            "name": self._identity.name,
            "instance_id": self._identity.instance_id,
            "version": __version__,
            "port": self._port,
            "meta": _machine_meta(),
        }

    async def announce_once(self) -> bool:
        """One announce attempt; updates and logs state transitions."""
        client = self._client or httpx.AsyncClient(timeout=5.0)
        try:
            resp = await client.post(
                f"{self._hive_url}/announce",
                json=self.payload,
                headers={"X-Swarm-Key": self._key},
            )
            resp.raise_for_status()
        except Exception as e:
            self._consecutive_failures += 1
            self._last_error = f"{type(e).__name__}: {e}"
            if self._reachable is not False:
                self._down_since = _now_iso()
                log.warning(
                    "hive.announce_failed",
                    hive=self._hive_url,
                    error=self._last_error,
                    hint="registry unreachable - other agents cannot discover this navi",
                )
            self._reachable = False
            return False
        finally:
            if self._client is None:
                await client.aclose()

        if self._reachable is not True:
            log.info("hive.announce_recovered" if self._reachable is False else "hive.announce_ok", hive=self._hive_url)
        self._reachable = True
        self._last_success = _now_iso()
        self._last_error = None
        self._down_since = None
        self._consecutive_failures = 0
        return True

    async def run(self) -> None:
        """Loop forever; the first announce fires immediately at startup."""
        log.info("hive.announce_started", hive=self._hive_url, interval=self._interval)
        while True:
            try:
                await self.announce_once()
            except asyncio.CancelledError:
                raise
            except Exception:  # never die — the loop outlives any single error
                log.exception("hive.announce_loop_error")
            await asyncio.sleep(self._interval)

    def get_status(self) -> dict:
        return {
            "configured": True,
            "url": self._hive_url,
            "reachable": self._reachable,
            "last_success": self._last_success,
            "last_error": self._last_error,
            "down_since": self._down_since,
            "consecutive_failures": self._consecutive_failures,
        }


# ── process-wide instance (lifespan creates it; provider/health read it) ──

_announcer: HiveAnnouncer | None = None


def set_announcer(announcer: HiveAnnouncer | None) -> None:
    global _announcer
    _announcer = announcer


def get_announcer() -> HiveAnnouncer | None:
    return _announcer


def build_announcer_from_settings() -> HiveAnnouncer | None:
    """Assemble the announcer from config; None (with a warning) if misconfigured."""
    from navi.config import settings
    from navi.identity import ensure_identity

    key = read_swarm_key(settings.swarm_key_file)
    if not key:
        log.warning(
            "hive.key_missing",
            key_file=settings.swarm_key_file,
            hint="HIVE_URL is set but the swarm key file is absent - announce disabled",
        )
        return None
    return HiveAnnouncer(
        hive_url=settings.hive_url,
        key=key,
        identity=ensure_identity(settings.instance_file),
        port=settings.navi_port,
        interval=settings.announce_interval_sec,
    )