"""Heartbeat reporter — periodically sends agent status to a central hub."""

from __future__ import annotations

import json
import platform
import queue
import re
import socket
import subprocess
import threading
import time
import urllib.request
from datetime import datetime, timezone
from typing import Any


class HeartbeatReporter:
    """Background thread that POSTs heartbeats to a hub server.

    The hub may return pending tasks in the heartbeat response.  These are
    queued and can be drained by the runner via :meth:`pending_tasks`.
    """

    def __init__(
        self,
        hub_url: str,
        agent_id: str,
        interval: int = 30,
        *,
        mode: str = "interactive",
        skills_count: int = 0,
        goals_count: int = 0,
        port: int = 8000,
        skill_names: list[str] | None = None,
    ) -> None:
        self._hub_url = hub_url.rstrip("/")
        self._agent_id = agent_id
        self._interval = interval
        self._mode = mode
        self._skills_count = skills_count
        self._goals_count = goals_count
        self._port = port
        self._skill_names = skill_names or []
        self._start_time = time.monotonic()
        self._stop_event = threading.Event()
        self._thread: threading.Thread | None = None
        self._task_queue: queue.Queue[dict] = queue.Queue()
        self._ping_targets: list[str] = []  # IPs to ping for triangulation
        self._ping_results: list[dict[str, Any]] = []  # latest results

    def _get_local_ip(self) -> str:
        try:
            s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
            s.connect(("8.8.8.8", 80))
            ip = s.getsockname()[0]
            s.close()
            return ip
        except Exception:
            return "127.0.0.1"

    @staticmethod
    def _ping_ip(ip: str) -> float:
        """Ping an IP once, return RTT in ms or -1 if unreachable."""
        flag_c = "-c" if platform.system() != "Windows" else "-n"
        flag_w = "-W" if platform.system() == "Darwin" else "-w"
        try:
            r = subprocess.run(
                ["ping", flag_c, "1", flag_w, "1", ip],
                capture_output=True,
                text=True,
                timeout=3,
            )
            if r.returncode == 0:
                m = re.search(r"time[=<](\d+\.?\d*)\s*ms", r.stdout)
                if m:
                    return float(m.group(1))
        except Exception:
            pass
        return -1.0

    def _run_pings(self) -> None:
        """Ping all targets assigned by the hub (runs in heartbeat thread)."""
        results: list[dict[str, Any]] = []
        for ip in self._ping_targets[:10]:  # cap at 10 to avoid blocking too long
            rtt = self._ping_ip(ip)
            results.append({"target_ip": ip, "rtt_ms": rtt})
        self._ping_results = results

    def _build_payload(self) -> dict[str, Any]:
        return {
            "agent_id": self._agent_id,
            "hostname": socket.gethostname(),
            "ip": self._get_local_ip(),
            "port": self._port,
            "status": "running",
            "mode": self._mode,
            "skills_count": self._skills_count,
            "goals_count": self._goals_count,
            "uptime_seconds": int(time.monotonic() - self._start_time),
            "timestamp": datetime.now(timezone.utc).isoformat(),
            "skill_names": self._skill_names,
            "ping_results": self._ping_results,
        }

    def _send(self) -> None:
        url = f"{self._hub_url}/api/hub/heartbeat"
        data = json.dumps(self._build_payload()).encode()
        req = urllib.request.Request(url, data=data, headers={"Content-Type": "application/json"})
        try:
            with urllib.request.urlopen(req, timeout=5) as resp:
                body = json.loads(resp.read().decode())
                for task in body.get("pending_tasks", []):
                    self._task_queue.put(task)
                # Update ping targets for triangulation
                self._ping_targets = body.get("ping_targets", [])
        except Exception:
            pass  # Hub unreachable — agent continues normally

    def pending_tasks(self) -> list[dict]:
        """Drain and return all pending tasks received from the hub."""
        tasks: list[dict] = []
        while True:
            try:
                tasks.append(self._task_queue.get_nowait())
            except queue.Empty:
                break
        return tasks

    def _loop(self) -> None:
        while not self._stop_event.is_set():
            # Run pings for triangulation (before sending, so results are included)
            if self._ping_targets:
                self._run_pings()
            self._send()
            self._stop_event.wait(self._interval)

    def start(self) -> None:
        if self._thread is not None:
            return
        self._thread = threading.Thread(target=self._loop, daemon=True, name="heartbeat")
        self._thread.start()

    def stop(self) -> None:
        self._stop_event.set()
        if self._thread is not None:
            self._thread.join(timeout=2)
            self._thread = None
