"""Woodpecker API client (read-only) — the unmetered surface (poll freely).

Ported from ``audit_secret_scopes.py``: paginated ``GET /api/repos?all=true``,
org/repo secret scopes, org lookup. The API is injectable (a plain callable) so
the doctor and its replay fixtures run with zero network. Token resolution is the
house fallback chain: ``$WOODPECKER_TOKEN`` → ``mgf-standard/my_stuff/
woodpecker_token.txt``; a 401 raises a :class:`CredentialError` pointing at the
MGF_INFRA §2 on-host mint. The token value is never logged or printed.
"""

from __future__ import annotations

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

from .errors import CredentialError

DEFAULT_WP_URL = "https://195.15.241.48.sslip.io"
DEFAULT_OWNER = "magogi-admin"
PER_PAGE = 50

ApiFn = Callable[..., Any]


@dataclass(frozen=True)
class RepoInfo:
    id: int
    name: str  # the bare repo name (last path segment)
    full_name: str
    active: bool


class WoodpeckerAPI(Protocol):
    """The read-only surface the doctor consumes (real client + fakes match it)."""

    def org_id(self) -> int: ...
    def org_secrets(self) -> dict[str, frozenset[str]]: ...
    def repo_secrets(self, repo_id: int) -> dict[str, frozenset[str]]: ...
    def activated_repos(self) -> list[RepoInfo]: ...


def resolve_token(explicit: str | None = None, *, fed_root: Path | None = None) -> str:
    """Return a Woodpecker token by the house fallback chain, or raise.

    Order: explicit arg → ``$WOODPECKER_TOKEN`` → ``mgf-standard/my_stuff/
    woodpecker_token.txt``. The value is read straight from disk into the request
    header — never returned into a transcript by the caller.
    """
    if explicit:
        return explicit
    env = os.environ.get("WOODPECKER_TOKEN")
    if env:
        return env
    root = fed_root or (Path.home() / "PycharmProjects")
    stored = Path(root) / "mgf-standard" / "my_stuff" / "woodpecker_token.txt"
    if stored.is_file():
        return stored.read_text(encoding="utf-8").strip()
    raise CredentialError(
        "no Woodpecker token — set $WOODPECKER_TOKEN or provide "
        "mgf-standard/my_stuff/woodpecker_token.txt (mint per MGF_INFRA §2)."
    )


def _default_api(
    url: str, token: str, method: str = "GET", payload: dict[str, Any] | None = None
) -> Any:
    data = json.dumps(payload).encode() if payload is not None else None
    req = urllib.request.Request(  # noqa: S310 (fixed https host)
        url,
        data=data,
        method=method,
        headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
    )
    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 {}


def _scopes(raw: Any) -> dict[str, frozenset[str]]:
    return {s["name"]: frozenset(s.get("events") or []) for s in (raw or [])}


class WoodpeckerClient:
    """Live read-only client. All calls go through the injectable ``api``."""

    def __init__(
        self,
        *,
        base_url: str = DEFAULT_WP_URL,
        owner: str = DEFAULT_OWNER,
        token: str | None = None,
        fed_root: Path | None = None,
        api: ApiFn = _default_api,
    ) -> None:
        self.base = base_url.rstrip("/")
        self.owner = owner
        self._token = resolve_token(token, fed_root=fed_root)
        self._api = api
        self._org_id: int | None = None

    def _get(self, path: str) -> Any:
        try:
            return self._api(f"{self.base}{path}", self._token)
        except urllib.error.HTTPError as exc:
            if exc.code == 401:
                raise CredentialError(
                    "Woodpecker 401 — token expired; re-mint per MGF_INFRA §2 "
                    "(on-host DB-hash JWT). This is owned infra, not a wall."
                ) from exc
            raise CredentialError(f"Woodpecker API HTTP {exc.code} on {path}") from exc
        except urllib.error.URLError as exc:
            raise CredentialError(f"Woodpecker unreachable ({exc.reason})") from exc

    def org_id(self) -> int:
        if self._org_id is None:
            org = self._get(f"/api/orgs/lookup/{self.owner}")
            self._org_id = int(org["id"])
        return self._org_id

    def org_secrets(self) -> dict[str, frozenset[str]]:
        return _scopes(self._get(f"/api/orgs/{self.org_id()}/secrets"))

    def repo_secrets(self, repo_id: int) -> dict[str, frozenset[str]]:
        return _scopes(self._get(f"/api/repos/{repo_id}/secrets"))

    def activated_repos(self) -> list[RepoInfo]:
        collected: list[dict[str, Any]] = []
        page = 1
        while True:
            batch = self._get(f"/api/repos?all=true&page={page}&perPage={PER_PAGE}")
            if not batch:
                break
            collected.extend(batch)
            page += 1
        out: list[RepoInfo] = []
        for r in collected:
            full = str(r.get("full_name", ""))
            if self.owner and full.split("/")[0] != self.owner:
                continue
            if not r.get("active"):
                continue
            out.append(
                RepoInfo(id=int(r["id"]), name=full.split("/")[-1], full_name=full, active=True)
            )
        return sorted(out, key=lambda r: r.full_name)
