"""The Control Room JWT and where it is cached between processes.

Expiry is read from the JWT's own `exp` claim when it parses, because that is
the server's opinion. The `ttlSeconds` field of the authentication response is
explicitly documented as ignored on initial authentication (it is meaningful
only on refresh), so it is used solely as a fallback.

The token is cached on disk so a second MCP host reuses this session instead of
opening a new one — the account only gets five.
"""

import base64
import binascii
import json
import time
from dataclasses import dataclass
from pathlib import Path
from typing import Optional

from ..utils.atomic_io import read_json, write_json_atomic
from ..utils.config import DEFAULT_TOKEN_TTL_SECONDS, TOKEN_REFRESH_SKEW_SECONDS
from ._paths import token_path

JWT_SEGMENT_COUNT = 3


@dataclass(frozen=True)
class Token:
    """An issued Control Room token. Immutable — refreshing yields a new one."""

    value: str
    expires_at: float
    username: str

    def is_expired(
        self, *, now: float | None = None, skew: float = TOKEN_REFRESH_SKEW_SECONDS
    ) -> bool:
        """True once the token is inside `skew` seconds of expiry.

        Renewing early is deliberate: waiting for a 401 wastes a round trip and,
        because a refresh invalidates the previous token, turns every in-flight
        request into a retry.
        """
        moment = time.time() if now is None else now
        return moment >= (self.expires_at - skew)

    def to_payload(self) -> dict:
        return {"value": self.value, "expires_at": self.expires_at, "username": self.username}

    @classmethod
    def from_payload(cls, payload: dict) -> Optional["Token"]:
        try:
            return cls(
                value=str(payload["value"]),
                expires_at=float(payload["expires_at"]),
                username=str(payload["username"]),
            )
        except (KeyError, TypeError, ValueError):
            return None

    @classmethod
    def from_auth_response(
        cls,
        body: dict,
        *,
        username: str,
        now: float | None = None,
        default_ttl: int = DEFAULT_TOKEN_TTL_SECONDS,
    ) -> "Token":
        """Build a Token from a `/v2/authentication` (or refresh) response body."""
        moment = time.time() if now is None else now
        value = body.get("token")
        if not isinstance(value, str) or not value.strip():
            raise ValueError("authentication response did not contain a 'token' field")

        expires_at = _jwt_expiry(value)
        if expires_at is None:
            expires_at = moment + _ttl_seconds(body, default_ttl)
        return cls(value=value, expires_at=expires_at, username=username)


class TokenStore:
    """Disk cache for one account's token."""

    def __init__(self, account_key: str) -> None:
        self._path: Path = token_path(account_key)

    @property
    def path(self) -> Path:
        return self._path

    def load(self) -> Token | None:
        payload = read_json(self._path)
        if not isinstance(payload, dict):
            return None
        return Token.from_payload(payload)

    def save(self, token: Token) -> None:
        write_json_atomic(self._path, token.to_payload())

    def clear(self, *, only_if: Token | None = None) -> None:
        """Remove the cached token.

        ``only_if`` makes the delete conditional on the cache still holding
        that exact token. The cache is shared across processes by design, so an
        unconditional unlink throws away a token a sibling may have published
        seconds ago and forces it to burn one of the account's five sessions
        re-authenticating.

        The read-then-unlink is not atomic; losing that race degrades to the
        unconditional behaviour (one extra re-auth), never to a wrong token.
        """
        if only_if is not None:
            current = self.load()
            if current is None or current.value != only_if.value:
                return
        try:
            self._path.unlink()
        except OSError:
            pass


def _ttl_seconds(body: dict, default_ttl: int) -> float:
    """Read `ttlSeconds` from a response body, falling back to the default.

    The field arrives as a string, and initial authentication returns "0" — both
    are treated as "no useful value here".
    """
    raw = body.get("ttlSeconds")
    try:
        ttl = float(raw)  # type: ignore[arg-type]
    except (TypeError, ValueError):
        return float(default_ttl)
    return ttl if ttl > 0 else float(default_ttl)


def _jwt_expiry(token: str) -> float | None:
    """Return the `exp` claim of a JWT, or None when it cannot be read.

    The signature is not verified — this is not an authorization decision, only
    a scheduling hint for when to renew. An unreadable token simply falls back
    to the nominal TTL.
    """
    parts = token.split(".")
    if len(parts) != JWT_SEGMENT_COUNT:
        return None
    try:
        padded = parts[1] + "=" * (-len(parts[1]) % 4)
        claims = json.loads(base64.urlsafe_b64decode(padded))
    except (binascii.Error, ValueError, TypeError):
        return None
    exp = claims.get("exp") if isinstance(claims, dict) else None
    try:
        return float(exp)  # type: ignore[arg-type]
    except (TypeError, ValueError):
        return None
