"""API key rotation handler for qRemoteX.

Supports:
  - Gateway KEY_ROTATION_NOTICE message handling (server notifies client)
  - Hot-reload from key file (QRAPTOR_API_KEY_FILE env var)
  - SIGHUP signal to trigger key reload
  - Re-initializes AuthManager with the new key
  - Logs warnings during grace period

Client-side logic — the server rotates keys and keeps the old key hash
valid for a 24h grace period.  This module ensures the executor can pick
up the new key without a full restart.
"""

from __future__ import annotations

import logging
import os
import signal
import time
from pathlib import Path
from typing import Any, Callable

from qremotex.config import ExecutorConfig
from qremotex.connection.auth import AuthManager

logger = logging.getLogger("qremotex.key_rotation")


class KeyRotationHandler:
    """Manages API key rotation on the client side.

    Watches for:
      1. KEY_ROTATION_NOTICE messages from the gateway
      2. SIGHUP signals (trigger reload from key file)
    """

    def __init__(
        self,
        config: ExecutorConfig,
        auth_manager: AuthManager,
        on_key_rotated: Callable[[AuthManager], None] | None = None,
    ) -> None:
        self._config = config
        self._auth = auth_manager
        self._on_key_rotated = on_key_rotated

        # Key file path for hot-reload (optional)
        self._key_file: str | None = os.environ.get("QRAPTOR_API_KEY_FILE")

        # Rotation state
        self._grace_deadline: float | None = None
        self._pending_rotation: bool = False
        self._last_notice_time: float = 0.0
        self._rotation_count: int = 0

    @property
    def pending_rotation(self) -> bool:
        return self._pending_rotation

    @property
    def in_grace_period(self) -> bool:
        if self._grace_deadline is None:
            return False
        return time.time() < self._grace_deadline

    @property
    def rotation_count(self) -> int:
        return self._rotation_count

    # ------------------------------------------------------------------ #
    # SIGHUP registration
    # ------------------------------------------------------------------ #
    def register_signal_handler(self) -> None:
        """Register SIGHUP to trigger key reload from file."""
        try:
            signal.signal(signal.SIGHUP, self._sighup_handler)
            logger.info("Registered SIGHUP handler for key rotation")
        except (OSError, AttributeError):
            # SIGHUP not available on Windows
            logger.debug("SIGHUP not available on this platform")

    def _sighup_handler(self, signum: int, frame: Any) -> None:
        """Signal handler — set flag for async reload."""
        logger.info("SIGHUP received — scheduling API key reload")
        self._pending_rotation = True

    # ------------------------------------------------------------------ #
    # Gateway notification handler
    # ------------------------------------------------------------------ #
    def handle_rotation_notice(self, msg: dict[str, Any]) -> dict[str, Any]:
        """Process a KEY_ROTATION_NOTICE from the gateway.

        Expected message fields:
          - grace_period_until: ISO 8601 timestamp (old key valid until)
          - key_id: new key ID (for logging)
          - reason: "manual_rotation" | "scheduled" | "security"

        Returns:
          {"ok": True/False, "action": str, "error": str|None}
        """
        grace_until = msg.get("grace_period_until")
        new_key_id = msg.get("key_id", "unknown")
        reason = msg.get("reason", "manual_rotation")

        self._last_notice_time = time.time()

        if grace_until:
            self._parse_grace_deadline(grace_until)

        self._pending_rotation = True

        logger.warning(
            "Key rotation notice received: new_key_id=%s reason=%s grace_until=%s. "
            "Update the API key (via key file or env var) and send SIGHUP or restart.",
            new_key_id,
            reason,
            grace_until or "none",
        )

        # Attempt auto-reload from key file
        reloaded = self._try_reload_from_file()
        if reloaded:
            return {"ok": True, "action": "auto_reloaded"}

        return {
            "ok": True,
            "action": "pending_manual_update",
            "message": "Key rotation notice acknowledged. Awaiting new key via file or restart.",
        }

    # ------------------------------------------------------------------ #
    # Key reload
    # ------------------------------------------------------------------ #
    def try_reload(self) -> bool:
        """Attempt to reload the API key. Called from the event loop.

        Returns True if key was successfully rotated.
        """
        if not self._pending_rotation:
            return False

        reloaded = self._try_reload_from_file()
        if reloaded:
            self._pending_rotation = False
        return reloaded

    def _try_reload_from_file(self) -> bool:
        """Read a new API key from the key file if configured."""
        if not self._key_file:
            logger.debug("No QRAPTOR_API_KEY_FILE configured — cannot auto-reload")
            return False

        key_path = Path(self._key_file)
        if not key_path.is_file():
            logger.warning("Key file not found: %s", self._key_file)
            return False

        try:
            new_key = key_path.read_text().strip()
        except OSError as e:
            logger.error("Failed to read key file %s: %s", self._key_file, e)
            return False

        if not new_key:
            logger.warning("Key file is empty: %s", self._key_file)
            return False

        # Don't reload if same as current key
        if new_key == self._config.api_key:
            logger.debug("Key file contains the same key — no rotation needed")
            return False

        return self._apply_new_key(new_key)

    def _apply_new_key(self, new_key: str) -> bool:
        """Apply a new API key to config and auth manager."""
        old_prefix = self._config.api_key_prefix()

        # Update the config
        self._config.api_key = new_key

        # Build a new AuthManager with the updated key
        new_auth = AuthManager(self._config)

        self._auth = new_auth
        self._rotation_count += 1
        self._pending_rotation = False

        # Clear grace period — rotation complete
        self._grace_deadline = None

        logger.info(
            "API key rotated successfully: old=%s new=%s (rotation #%d)",
            old_prefix,
            self._config.api_key_prefix(),
            self._rotation_count,
        )

        # Notify callers so they can update their auth reference
        if self._on_key_rotated:
            self._on_key_rotated(new_auth)

        return True

    @property
    def auth_manager(self) -> AuthManager:
        """Return the current (possibly rotated) AuthManager."""
        return self._auth

    # ------------------------------------------------------------------ #
    # Grace period
    # ------------------------------------------------------------------ #
    def _parse_grace_deadline(self, grace_until: str) -> None:
        """Parse ISO 8601 grace_period_until into a UNIX timestamp."""
        from datetime import datetime

        try:
            # Handle ISO 8601 with timezone
            dt = datetime.fromisoformat(grace_until.replace("Z", "+00:00"))
            self._grace_deadline = dt.timestamp()
            remaining = self._grace_deadline - time.time()
            logger.info(
                "Grace period set: old key valid for %.1f more hours",
                remaining / 3600,
            )
        except (ValueError, AttributeError) as e:
            logger.warning("Could not parse grace_period_until '%s': %s", grace_until, e)
            # Default to 24h grace period
            self._grace_deadline = time.time() + 86400
            logger.info("Defaulting to 24h grace period")

    def check_grace_expiry(self) -> bool:
        """Check if grace period has expired. Returns True if expired."""
        if self._grace_deadline is None:
            return False
        if time.time() >= self._grace_deadline:
            logger.error(
                "Grace period EXPIRED. The old API key is no longer valid. "
                "Update the key immediately via QRAPTOR_API_KEY_FILE or restart with new QRAPTOR_API_KEY."
            )
            return True
        remaining = self._grace_deadline - time.time()
        if remaining < 3600:
            logger.warning("Grace period expires in %.0f minutes!", remaining / 60)
        return False
