"""
Module: session_heartbeat
Description: Background heartbeat for dev session liveness tracking.

Implements:
    - docs/architecture/dev-sessions.md#heartbeat-protocol
    - docs/cli/reference.md#huitzo-pack-dev

See Also:
    - docs/architecture/system-monitor.md
"""

from __future__ import annotations

import logging
import threading
from typing import TYPE_CHECKING, Any

import httpx

from huitzo_cli.errors import AuthenticationError, NetworkError, ServerError

if TYPE_CHECKING:
    from huitzo_cli.auth import AuthClient

logger = logging.getLogger(__name__)

HEARTBEAT_INTERVAL = 30  # seconds
DEFAULT_TIMEOUT = 10  # seconds


class SessionHeartbeat:
    """Registers dev sessions and sends periodic heartbeats to the backend.

    Registration raises typed exceptions on failure (no silent fallback).
    The heartbeat loop auto-refreshes tokens on 401 and signals a freeze
    event when refresh fails.
    """

    def __init__(
        self,
        api_url: str,
        auth_token: str,
        timeout: int = DEFAULT_TIMEOUT,
        auth_client: AuthClient | None = None,
        freeze_event: threading.Event | None = None,
    ) -> None:
        self._api_url = api_url.rstrip("/")
        self._auth_token = auth_token
        self._timeout = timeout
        self._auth_client = auth_client
        self._freeze_event = freeze_event
        self._session_id: str | None = None
        self._stop_event = threading.Event()
        self._thread: threading.Thread | None = None
        self._refresh_lock = threading.Lock()
        self._base_path = "/api/v1/dev-sessions"
        self._client = httpx.Client(
            base_url=self._api_url,
            headers={"Authorization": f"Bearer {auth_token}", "Content-Type": "application/json"},
            timeout=timeout,
        )

    def register(
        self,
        pack_namespace: str,
        port: int | None = None,
        host: str | None = None,
        command_count: int = 0,
        metadata: dict[str, Any] | None = None,
    ) -> str:
        """Register a dev session with the backend.

        Returns:
            session_id string.

        Raises:
            AuthenticationError: On 401 response.
            NetworkError: On connection/timeout errors.
            ServerError: On 5xx responses.
        """
        body: dict[str, Any] = {
            "pack_namespace": pack_namespace,
            "sandbox_type": "local",
            "command_count": command_count,
        }
        if port is not None:
            body["local_port"] = port
        if host is not None:
            body["local_host"] = host
        if metadata is not None:
            body["metadata"] = metadata

        try:
            response = self._client.post(self._base_path, json=body)
        except (httpx.ConnectError, httpx.TimeoutException) as e:
            raise NetworkError(f"Could not reach backend: {e}") from e
        except httpx.HTTPError as e:
            raise NetworkError(f"HTTP error during registration: {e}") from e

        if response.status_code == 401:
            raise AuthenticationError("Authentication failed. Run: huitzo login")
        if response.status_code >= 500:
            raise ServerError(
                f"Backend returned {response.status_code}",
                user_message="Backend may be experiencing issues. Try again later.",
            )
        if response.status_code >= 400:
            raise ServerError(f"Registration failed with status {response.status_code}")

        data = response.json()
        self._session_id = data.get("data", {}).get("session_id")
        logger.info("Dev session registered: %s", self._session_id)
        return self._session_id

    def start(self) -> None:
        """Start the background heartbeat thread."""
        if self._session_id is None:
            logger.debug("No session registered, skipping heartbeat")
            return
        self._stop_event.clear()
        self._thread = threading.Thread(
            target=self._heartbeat_loop, daemon=True, name="huitzo-heartbeat"
        )
        self._thread.start()

    def stop(self) -> None:
        """Stop the heartbeat thread and notify the backend."""
        self._stop_event.set()
        if self._thread is not None:
            self._thread.join(timeout=5)
            self._thread = None

        if self._session_id is not None:
            try:
                self._client.post(f"{self._base_path}/{self._session_id}/stop")
            except httpx.HTTPError:
                logger.debug("Failed to send stop signal", exc_info=True)

        self._client.close()

    def _heartbeat_loop(self) -> None:
        """Send heartbeats every HEARTBEAT_INTERVAL seconds until stopped."""
        while not self._stop_event.wait(HEARTBEAT_INTERVAL):
            try:
                response = self._client.post(f"{self._base_path}/{self._session_id}/heartbeat")
                if response.status_code == 401:
                    self._try_refresh_token()
                    # Retry with new token
                    self._client.post(f"{self._base_path}/{self._session_id}/heartbeat")
                logger.debug("Heartbeat sent for session %s", self._session_id)
            except httpx.HTTPError:
                logger.warning("Heartbeat failed for session %s", self._session_id)

    def _try_refresh_token(self) -> None:
        """Attempt to refresh the access token. Sets freeze_event on failure."""
        if not self._auth_client:
            logger.warning("No auth client — cannot refresh token")
            if self._freeze_event:
                self._freeze_event.set()
            return

        with self._refresh_lock:
            try:
                new_token = self._auth_client.refresh_tokens()
                self._client.headers["Authorization"] = f"Bearer {new_token}"
                logger.info("Token refreshed successfully during heartbeat")
            except (AuthenticationError, NetworkError, ServerError):
                logger.warning("Token refresh failed")
                if self._freeze_event:
                    self._freeze_event.set()
