"""HTTPS client for the BrinkhausFleetManager.

Sends heartbeats, diagnostics, and status messages to the fleet
HTTPS ingest API.  Wire-compatible with the BrinkhausFleetManager server.

Endpoints: ``POST /api/ingest/{customer}/{machine}/{software}/{heartbeat|diagnostics|status}``
"""

import json
import logging
import os
import re
import socket
import ssl
import threading
import time
import urllib.error
import urllib.request
from datetime import datetime, timezone
from typing import Callable, List, Optional, Tuple

log = logging.getLogger(__name__)


def _slugify(value: str) -> str:
    value = value.lower().strip()
    value = re.sub(r"[^a-z0-9]+", "-", value)
    return value.strip("-")


def _now_iso() -> str:
    return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")


def _get_local_ips() -> List[str]:
    env_ips = os.environ.get("FLEET_HOST_IPS", "").strip()
    if env_ips:
        return [ip.strip() for ip in env_ips.split(",") if ip.strip()]

    ips: List[str] = []
    try:
        for info in socket.getaddrinfo(socket.gethostname(), None, socket.AF_UNSPEC):
            addr = str(info[4][0])
            if addr not in ("127.0.0.1", "::1", "0.0.0.0") and addr not in ips:
                ips.append(addr)
    except Exception:
        pass
    if not ips:
        try:
            s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
            s.connect(("8.8.8.8", 80))
            ips.append(s.getsockname()[0])
            s.close()
        except Exception:
            pass
    return ips


class FleetMonitorClient:
    """HTTPS client for the BrinkhausFleetManager fleet protocol."""

    def __init__(
        self,
        base_url: str = "",
        customer: str = "",
        machine: str = "",
        software: str = "",
        token: Optional[str] = None,
        heartbeat_interval: int = 60,
        version: Optional[str] = None,
        verify_ssl: bool = True,
        # Legacy params for backward compatibility with existing config files
        broker: Optional[str] = None,
        port: Optional[int] = None,
        username: Optional[str] = None,
        password: Optional[str] = None,
        use_tls: Optional[bool] = None,
    ) -> None:
        self.customer = _slugify(customer)
        self.machine = _slugify(machine)
        self.software = _slugify(software)
        self.version = version
        self.heartbeat_interval = heartbeat_interval
        self.token = token

        # Derive base_url from legacy broker param if not provided
        if base_url:
            self.base_url = base_url.rstrip("/")
        elif broker:
            proto = "https" if (use_tls is not False) else "http"
            if port and port not in (80, 443):
                self.base_url = f"{proto}://{broker}:{port}"
            else:
                self.base_url = f"{proto}://{broker}"
        else:
            self.base_url = "https://fleet.brinkhaus-gmbh.de"

        # Derive token from legacy password param if not provided
        if not self.token and password:
            self.token = password

        self._rebuild_ingest_url()

        # SSL context
        if verify_ssl:
            self._ssl_ctx = ssl.create_default_context()
        else:
            self._ssl_ctx = ssl.create_default_context()
            self._ssl_ctx.check_hostname = False
            self._ssl_ctx.verify_mode = ssl.CERT_NONE

        self._heartbeat_thread: Optional[threading.Thread] = None
        self._stop_event = threading.Event()
        self._start_time: Optional[float] = None
        self._connected = False
        self._last_send_attempt: Optional[str] = None
        self._last_send_success: Optional[str] = None
        self._last_connect_error: Optional[str] = None
        self._on_connection_change: Optional[Callable[[bool], None]] = None

    def _rebuild_ingest_url(self) -> None:
        self._ingest_url = (
            f"{self.base_url}/api/ingest"
            f"/{self.customer}/{self.machine}/{self.software}"
        )

    def reconfigure(
        self,
        base_url: str,
        customer: str,
        machine: str,
        token: Optional[str] = None,
    ) -> None:
        """Update connection parameters (e.g. after a late config load)."""
        self.base_url = base_url.rstrip("/")
        self.customer = _slugify(customer)
        self.machine = _slugify(machine)
        self.token = token
        self._rebuild_ingest_url()
        log.info("Fleet Monitor reconfigured: %s", self._ingest_url)

    # ------------------------------------------------------------------
    # Lifecycle
    # ------------------------------------------------------------------
    def start(self) -> None:
        log.info("Fleet Monitor starting: %s", self._ingest_url)
        self._start_time = time.monotonic()
        self._stop_event.clear()

        self._heartbeat_thread = threading.Thread(
            target=self._heartbeat_loop,
            name="fleet-monitor-heartbeat",
            daemon=True,
        )
        self._heartbeat_thread.start()

        self.send_status("running", "Connected")

    def stop(self) -> None:
        log.info("Fleet Monitor stopping: %s", self._ingest_url)
        self._stop_event.set()
        try:
            self.send_status("stopped", "Service stopped")
        except Exception:
            pass
        if self._heartbeat_thread:
            self._heartbeat_thread.join(timeout=5)

    @property
    def is_connected(self) -> bool:
        return self._connected

    # ------------------------------------------------------------------
    # Sending
    # ------------------------------------------------------------------
    def send_heartbeat(self) -> None:
        uptime = int(time.monotonic() - self._start_time) if self._start_time else 0
        payload = {
            "ts": _now_iso(),
            "version": self.version,
            "uptime_seconds": uptime,
            "pid": os.getpid(),
            "ip_addresses": _get_local_ips(),
        }
        self._post("heartbeat", payload)

    def send_diagnostics(self, diagnostics: List[Tuple[int, str, int]]) -> None:
        """Send diagnostics as ``(code, message, severity)`` tuples.

        Severity: 0 = OK/Info, 1 = Warning, 2 = Error.
        """
        payload = {
            "ts": _now_iso(),
            "items": [
                {"code": code, "message": msg, "severity": sev}
                for code, msg, sev in diagnostics
            ],
        }
        self._post("diagnostics", payload)

    def send_status(self, status: str, message: Optional[str] = None) -> None:
        """Send a status change.

        *status* should be one of: starting, running, degraded, error,
        stopping, stopped.
        """
        payload = {
            "ts": _now_iso(),
            "status": status,
            "message": message,
        }
        self._post("status", payload)

    def send_snapshot(self, snapshot: dict, status: Optional[str] = None,
                      message: Optional[str] = None) -> None:
        """Send a full status snapshot (output of StatusEngine.get_last_status()).

        The server stores the snapshot as JSONB and derives the status from it
        if *status* is not given explicitly.
        """
        payload: dict = {
            "ts": _now_iso(),
            "snapshot": snapshot,
        }
        if status:
            payload["status"] = status
        if message:
            payload["message"] = message
        self._post("status", payload)

    # ------------------------------------------------------------------
    # Internals
    # ------------------------------------------------------------------
    def _post(self, msg_type: str, payload: dict) -> None:
        url = f"{self._ingest_url}/{msg_type}"
        data = json.dumps(payload).encode("utf-8")
        self._last_send_attempt = _now_iso()

        headers = {"Content-Type": "application/json"}
        if self.token:
            headers["Authorization"] = f"Bearer {self.token}"

        req = urllib.request.Request(url, data=data, headers=headers, method="POST")

        try:
            with urllib.request.urlopen(req, context=self._ssl_ctx, timeout=10) as resp:
                status_code = resp.getcode()
                if status_code == 200:
                    self._last_send_success = _now_iso()
                    self._last_connect_error = None
                    if not self._connected:
                        self._connected = True
                        self._notify_change(True)
                else:
                    self._last_connect_error = f"HTTP {status_code}"
                    log.warning("Ingest error: HTTP %d for %s", status_code, url)
        except urllib.error.HTTPError as e:
            self._last_connect_error = f"HTTP {e.code}: {e.reason}"
            log.warning("Ingest error: %s for %s", self._last_connect_error, url)
            if self._connected:
                self._connected = False
                self._notify_change(False)
        except Exception as e:
            self._last_connect_error = str(e)
            log.warning("Ingest error: %s for %s", self._last_connect_error, url)
            if self._connected:
                self._connected = False
                self._notify_change(False)

    def _heartbeat_loop(self) -> None:
        while not self._stop_event.is_set():
            try:
                self.send_heartbeat()
            except Exception:
                log.exception("Heartbeat error")
            self._stop_event.wait(self.heartbeat_interval)

    def _notify_change(self, connected: bool) -> None:
        if self._on_connection_change:
            try:
                self._on_connection_change(connected)
            except Exception:
                log.exception("Error in connection change callback")
