"""Bounded one-time pairing claim for the mTLS node gateway."""

from __future__ import annotations

import hashlib
import hmac
import re
import ssl
import threading
import time
from pathlib import Path
from typing import Any, Callable, Mapping

from .security import PeerIdentity, TrustPolicy


class PairingError(ValueError):
    def __init__(self, status: int, code: str, detail: str) -> None:
        super().__init__(detail)
        self.status = status
        self.code = code
        self.detail = detail


class PairingManager:
    def __init__(
        self,
        *,
        enabled: bool,
        one_time_code: str,
        sas_confirmed: bool,
        window_seconds: int,
        max_attempts: int,
        node_id: str,
        node_name: str,
        server_cert_path: Path | None,
        allowed_permissions: frozenset[str],
        expected_client_fingerprint_sha256: str,
    ) -> None:
        self.enabled = enabled
        self._code = one_time_code
        self._sas_confirmed = sas_confirmed
        self._window_seconds = min(120, max(10, window_seconds))
        self._max_attempts = min(10, max(1, max_attempts))
        self.node_id = node_id
        self.node_name = node_name
        self.server_cert_path = server_cert_path
        self.allowed_permissions = allowed_permissions
        self._expected_client_fingerprint = expected_client_fingerprint_sha256.lower()
        self._lock = threading.RLock()
        self._deadline = 0.0
        self._attempts = 0
        self._used = False

    def arm(self) -> None:
        with self._lock:
            self._deadline = time.monotonic() + self._window_seconds if self.enabled else 0.0

    @property
    def state(self) -> str:
        with self._lock:
            if self._used:
                return "paired"
            if not self.enabled:
                return "closed"
            if not self._sas_confirmed or self._attempts >= self._max_attempts:
                return "closed"
            return "open" if time.monotonic() < self._deadline else "closed"

    def claim(
        self,
        payload: Mapping[str, Any],
        *,
        client_certificate_der: bytes,
        endpoint: str,
        trust: TrustPolicy,
        capabilities: list[str],
        on_paired: Callable[[], None] | None = None,
    ) -> dict[str, object]:
        with self._lock:
            if self.state != "open":
                raise PairingError(403, "pairing_closed", "pairing window is not open")
            fingerprint = hashlib.sha256(client_certificate_der).hexdigest()
            if not hmac.compare_digest(
                fingerprint.encode("ascii"),
                self._expected_client_fingerprint.encode("ascii"),
            ):
                self._attempts += 1
                raise PairingError(
                    403,
                    "pairing_client_mismatch",
                    "client certificate does not match the locally confirmed identity",
                )
            supplied = str(payload.get("one_time_code") or "")
            if not hmac.compare_digest(supplied.encode("utf-8"), self._code.encode("utf-8")):
                self._attempts += 1
                raise PairingError(403, "pairing_code_invalid", "pairing code was rejected")
            conductor_id = str(payload.get("conductor_id") or "")
            if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_.:-]{0,159}", conductor_id):
                raise PairingError(400, "invalid_conductor_id", "conductor_id is invalid")
            requested = payload.get("requested_permissions", [])
            if not isinstance(requested, list) or any(not isinstance(item, str) for item in requested):
                raise PairingError(400, "invalid_permissions", "requested_permissions must be strings")
            requested_set = set(requested)
            disallowed = requested_set - self.allowed_permissions
            if disallowed:
                raise PairingError(403, "permission_not_pairable", "one or more permissions are not pairable")
            roles = frozenset(requested_set | {"desktop.node.client"})
            trust.add(
                PeerIdentity(
                    node_id=conductor_id,
                    name=conductor_id,
                    certificate_fingerprint_sha256=fingerprint,
                    roles=roles,
                )
            )
            self._used = True
            self._code = ""
        if on_paired is not None:
            on_paired()
        return {
            "node_id": self.node_id,
            "name": self.node_name,
            "endpoint": endpoint,
            "certificate_fingerprint_sha256": self._server_certificate_fingerprint(),
            "capabilities": sorted(capabilities),
            "granted_permissions": sorted(requested_set),
        }

    def _server_certificate_fingerprint(self) -> str:
        if self.server_cert_path is None:
            raise PairingError(503, "server_certificate_unavailable", "server certificate is unavailable")
        try:
            pem = self.server_cert_path.read_text(encoding="ascii")
            der = ssl.PEM_cert_to_DER_cert(pem)
            return hashlib.sha256(der).hexdigest()
        except (OSError, ValueError) as exc:
            raise PairingError(503, "server_certificate_unavailable", "server certificate is invalid") from exc
