"""Registry domain model — kinds, surface types, states, and the frozen entry.

Zero values ever live in these objects (AG-18): an :class:`Entry` is *declared
truth* — where a credential must exist and how it is governed, never the secret
itself. The model is intentionally data-only; validation lives in
:mod:`mgf.secrets.registry` so a malformed registry fails closed there.
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import Any

# --------------------------------------------------------------------------- #
# Controlled vocabularies (P1). Growth is gated by the kind-extension meta-test:
# a new ``kind`` without a probe handler fails the suite (PROPOSAL §5.3).
# --------------------------------------------------------------------------- #

#: Credential kinds recognised in Phase 1. Each MUST have a probe handler.
KINDS: frozenset[str] = frozenset(
    {
        "ssh-deploy-key",
        "ssh-keypair",
        "api-token",
        "jwt-minted",
        "password",
        "oauth-app",
        "service-account",
        "openstack-appcred",
    }
)

#: Surface types a credential can be declared to occupy. ``store`` is declared
#: now but only validated from P2 (SPEC §3).
SURFACE_TYPES: frozenset[str] = frozenset(
    {
        "woodpecker-org-secret",
        "woodpecker-repo-secret",
        "forge-deploy-key",
        "forge-pat",
        "host-file",
        "local-file",
        "server-env",
        "store",
    }
)

#: The Woodpecker/forge surfaces whose presence means a credential is *live*.
#: A ``held`` entry may declare these (that is where it *would* live) but may
#: never mark one ``armed = true`` — that is the load-time held guard (SPEC §3).
ARMABLE_SURFACES: frozenset[str] = frozenset(
    {"woodpecker-org-secret", "woodpecker-repo-secret", "forge-deploy-key"}
)

#: Lifecycle states. ``held`` is first-class (the 13 website deploy secrets).
STATES: frozenset[str] = frozenset({"active", "held", "retiring", "tombstone"})

#: Owner families.
FAMILIES: frozenset[str] = frozenset({"mgf", "sooth"})

#: Woodpecker pipeline event classes.
EVENTS: frozenset[str] = frozenset({"push", "pull_request", "tag", "manual", "cron", "deployment"})

#: The sentinel meaning "every managed repo, forever" on a forge-deploy-key.
ALL_MANAGED = "ALL_MANAGED"


@dataclass(frozen=True)
class Surface:
    """One place a credential must exist, with its type-specific attributes.

    ``data`` holds the surface's declared fields verbatim (already validated by
    the loader). Never carries a secret value — only scopes, paths, modes,
    titles, fingerprints.
    """

    type: str
    data: dict[str, Any]

    def get(self, key: str, default: Any = None) -> Any:
        return self.data.get(key, default)

    def __getitem__(self, key: str) -> Any:
        return self.data[key]

    @property
    def events(self) -> frozenset[str]:
        return frozenset(self.data.get("events", ()) or ())

    @property
    def armed(self) -> bool:
        return bool(self.data.get("armed", False))


@dataclass(frozen=True)
class Policy:
    """Blast-radius + allowed-repos policy for an entry."""

    allowed_repos: Any  # ``"*"`` or a list[str] of repo names / globs
    blast_radius: str


@dataclass(frozen=True)
class Entry:
    """A single declared credential — the atom of the registry."""

    id: str
    family: str
    kind: str
    purpose: str
    surfaces: tuple[Surface, ...]
    consumers: tuple[str, ...]
    policy: Policy
    rotation: dict[str, Any]
    verify: dict[str, Any]
    recovery: str
    state: str

    def surfaces_of(self, *types: str) -> tuple[Surface, ...]:
        wanted = frozenset(types)
        return tuple(s for s in self.surfaces if s.type in wanted)

    def has_surface(self, *types: str) -> bool:
        return bool(self.surfaces_of(*types))

    @property
    def probe(self) -> str | None:
        p = self.verify.get("probe")
        return str(p) if p is not None else None
