"""Codeberg forge client (read-only) — the PAPER-110-budgeted surface.

Deploy-key coverage (doctor C5) is the only P1 forge use, and it is opt-in
(``--surface forge``). Codeberg is rate-limited, so this client is **serial,
≤1 req/s, single pass, disk-cached** — never parallel (SPEC §5 C5, §8). The HTTP
layer is injectable so C5's fixtures run offline. Token: ``$CODEBERG_TOKEN`` /
``$FORGE_TOKEN`` (extract per the house recipe — never ``cat`` a doc-form file);
absent → a :class:`CredentialError` that points at the recipe. No value printed.
"""

from __future__ import annotations

import json
import os
import time
import urllib.error
import urllib.request
from collections.abc import Callable
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, Protocol

from .errors import CredentialError

if TYPE_CHECKING:
    from pathlib import Path

DEFAULT_FORGE_URL = "https://codeberg.org"
DEFAULT_OWNER = "magogi-admin"

ApiFn = Callable[..., Any]


@dataclass(frozen=True)
class DeployKey:
    title: str
    fingerprint: str
    read_only: bool


class ForgeAPI(Protocol):
    def repo_deploy_keys(self, repo: str) -> list[DeployKey]: ...


def resolve_forge_token(explicit: str | None = None) -> str:
    if explicit:
        return explicit
    for var in ("CODEBERG_TOKEN", "FORGE_TOKEN"):
        val = os.environ.get(var)
        if val:
            return val
    raise CredentialError(
        "no Codeberg token — set $CODEBERG_TOKEN (extract per the house recipe, "
        "court/reviews/2026-07-16-mgf-uitest-web/ENSHRINE-RUNBOOK.md; never cat a "
        "doc-form token file)."
    )


def _default_api(url: str, token: str) -> Any:
    req = urllib.request.Request(url, headers={"Authorization": f"token {token}"})  # noqa: S310
    with urllib.request.urlopen(req, timeout=30) as resp:  # noqa: S310 (fixed https host)
        body = resp.read().decode()
        return json.loads(body) if body.strip() else []


class CodebergForgeClient:
    """Serial, rate-limited, disk-cached deploy-key reader (PAPER-110)."""

    def __init__(
        self,
        *,
        owner: str = DEFAULT_OWNER,
        token: str | None = None,
        base_url: str = DEFAULT_FORGE_URL,
        min_interval: float = 1.0,
        cache_dir: Path | None = None,
        api: ApiFn = _default_api,
    ) -> None:
        self.owner = owner
        self.base = base_url.rstrip("/")
        self._token = resolve_forge_token(token)
        self._min_interval = min_interval
        self._api = api
        self._last_call = 0.0
        self._mem: dict[str, list[DeployKey]] = {}
        self._cache_dir = cache_dir
        if cache_dir is not None:
            cache_dir.mkdir(parents=True, exist_ok=True)

    def _throttle(self) -> None:
        delta = time.monotonic() - self._last_call
        if delta < self._min_interval:
            time.sleep(self._min_interval - delta)
        self._last_call = time.monotonic()

    def repo_deploy_keys(self, repo: str) -> list[DeployKey]:
        if repo in self._mem:
            return self._mem[repo]
        cache_file = self._cache_dir / f"{repo}.json" if self._cache_dir else None
        if cache_file is not None and cache_file.is_file():
            raw = json.loads(cache_file.read_text(encoding="utf-8"))
            keys = [DeployKey(**k) for k in raw]
            self._mem[repo] = keys
            return keys
        self._throttle()
        try:
            data = self._api(f"{self.base}/api/v1/repos/{self.owner}/{repo}/keys", self._token)
        except urllib.error.HTTPError as exc:
            if exc.code == 401:
                raise CredentialError(
                    "Codeberg 401 — token invalid/expired (house recipe)."
                ) from exc
            if exc.code == 404:
                return []  # repo has no keys endpoint reachable / no keys
            raise CredentialError(f"Codeberg API HTTP {exc.code} for {repo}") from exc
        except urllib.error.URLError as exc:
            raise CredentialError(f"Codeberg unreachable ({exc.reason})") from exc
        keys = [
            DeployKey(
                title=str(k.get("title", "")),
                fingerprint=str(k.get("fingerprint", "")),
                read_only=bool(k.get("read_only", True)),
            )
            for k in (data or [])
        ]
        self._mem[repo] = keys
        if cache_file is not None:
            cache_file.write_text(
                json.dumps([k.__dict__ for k in keys], indent=2), encoding="utf-8"
            )
        return keys
