"""Per-kind liveness probes (P1 — read-only, non-destructive, value-free).

Every registered ``kind`` MUST have a probe handler here — the kind-extension
meta-test (SPEC §7.6) fails the suite otherwise, so the library cannot grow an
unverifiable branch. P1 probes are conservative: they confirm a credential is
present *where declared* at the metadata level (a live-file exists at its mode,
a JWT is unexpired, a Woodpecker/forge surface exists), never reading or emitting
a value. Network-dependent liveness deepens in P2; with no client a probe returns
``skipped`` rather than a false finding.
"""

from __future__ import annotations

import time
from collections.abc import Callable
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any

from .context import Context
from .fsutil import jwt_exp, octal_mode
from .model import KINDS, Entry

if TYPE_CHECKING:
    from pathlib import Path

_OK = "ok"
_FINDING = "finding"
_SKIPPED = "skipped"


@dataclass
class ProbeResult:
    id: str
    kind: str
    status: str  # ok | finding | skipped
    message: str
    evidence: dict[str, Any] = field(default_factory=dict)

    def exit_code(self) -> int:
        return 1 if self.status == _FINDING else 0

    def to_json(self) -> dict[str, Any]:
        return {
            "id": self.id,
            "kind": self.kind,
            "status": self.status,
            "message": self.message,
            "evidence": self.evidence,
        }


Handler = Callable[[Entry, Context], ProbeResult]


# --------------------------------------------------------------------------- #
# Shared building blocks (metadata only).
# --------------------------------------------------------------------------- #


def _local_files(entry: Entry, ctx: Context) -> list[tuple[Path, str]]:
    out: list[tuple[Path, str]] = []
    for s in entry.surfaces_of("local-file"):
        full = ctx.fed_root / str(s["repo"]) / "my_stuff" / str(s["path"])
        out.append((full, str(s["mode"])))
    return out


def _probe_local_presence(entry: Entry, ctx: Context) -> ProbeResult:
    files = _local_files(entry, ctx)
    if not files:
        return ProbeResult(
            entry.id, entry.kind, _SKIPPED, "no local-file surface to check (P1 metadata probe)"
        )
    missing = [str(p) for p, _ in files if not p.exists()]
    if missing:
        return ProbeResult(
            entry.id, entry.kind, _FINDING, "declared local file(s) missing", {"missing": missing}
        )
    return ProbeResult(
        entry.id,
        entry.kind,
        _OK,
        "declared local file(s) present",
        {"files": [str(p) for p, _ in files]},
    )


# --------------------------------------------------------------------------- #
# Per-kind handlers.
# --------------------------------------------------------------------------- #


def probe_ssh_deploy_key(entry: Entry, ctx: Context) -> ProbeResult:
    for s in entry.surfaces_of("forge-deploy-key"):
        title = str(s["title"])
        if ctx.forge is None:
            return ProbeResult(
                entry.id,
                entry.kind,
                _SKIPPED,
                f"forge deploy-key {title!r}: need --surface forge / forge client",
            )
        return ProbeResult(
            entry.id,
            entry.kind,
            _OK,
            f"forge deploy-key {title!r} declared; coverage via doctor C5",
            {"title": title},
        )
    return _probe_local_presence(entry, ctx)


def probe_ssh_keypair(entry: Entry, ctx: Context) -> ProbeResult:
    return _probe_local_presence(entry, ctx)


def probe_api_token(entry: Entry, ctx: Context) -> ProbeResult:
    # No non-destructive value-free liveness call in P1 — presence + hygiene only.
    return _probe_local_presence(entry, ctx)


def probe_jwt_minted(entry: Entry, ctx: Context) -> ProbeResult:
    for path, _mode in _local_files(entry, ctx):
        if not path.exists():
            continue
        exp = jwt_exp(path)
        if exp is None:
            return ProbeResult(
                entry.id,
                entry.kind,
                _SKIPPED,
                "local file present but not a decodable JWT (may be a minter)",
            )
        now = time.time()
        if exp < now:
            return ProbeResult(entry.id, entry.kind, _FINDING, "JWT is EXPIRED", {"exp": int(exp)})
        return ProbeResult(
            entry.id,
            entry.kind,
            _OK,
            "JWT valid",
            {"exp": int(exp), "days_left": int((exp - now) // 86400)},
        )
    return ProbeResult(
        entry.id, entry.kind, _SKIPPED, "jwt-minted: minted on demand; no static file to decode"
    )


def probe_password(entry: Entry, ctx: Context) -> ProbeResult:
    return _probe_local_presence(entry, ctx)


def probe_oauth_app(entry: Entry, ctx: Context) -> ProbeResult:
    return _probe_host_or_local(entry, ctx)


def probe_service_account(entry: Entry, ctx: Context) -> ProbeResult:
    return _probe_host_or_local(entry, ctx)


def probe_openstack_appcred(entry: Entry, ctx: Context) -> ProbeResult:
    return _probe_local_presence(entry, ctx)


def _probe_host_or_local(entry: Entry, ctx: Context) -> ProbeResult:
    host_surfaces = entry.surfaces_of("host-file", "server-env")
    if host_surfaces and ctx.hosts is not None:
        missing: list[str] = []
        unverifiable: list[str] = []
        for s in host_surfaces:
            st = ctx.hosts.stat(str(s["host"]), str(s["path"]))
            if st.status == "missing":
                missing.append(f"{s['host']}:{s['path']}")
            elif st.status == "unreachable":
                unverifiable.append(f"{s['host']}:{s['path']}")
        if missing:
            return ProbeResult(
                entry.id,
                entry.kind,
                _FINDING,
                "declared host file(s) MISSING",
                {"missing": missing},
            )
        if unverifiable:
            return ProbeResult(
                entry.id,
                entry.kind,
                _SKIPPED,
                "declared host file(s) present but unverifiable (permission denied)",
                {"unverifiable": unverifiable},
            )
        return ProbeResult(entry.id, entry.kind, _OK, "declared host file(s) present")
    if host_surfaces:
        return ProbeResult(
            entry.id, entry.kind, _SKIPPED, "host-file surface: need --surface hosts / SSH client"
        )
    return _probe_local_presence(entry, ctx)


#: The registry the meta-test checks: every kind → a handler.
PROBES: dict[str, Handler] = {
    "ssh-deploy-key": probe_ssh_deploy_key,
    "ssh-keypair": probe_ssh_keypair,
    "api-token": probe_api_token,
    "jwt-minted": probe_jwt_minted,
    "password": probe_password,
    "oauth-app": probe_oauth_app,
    "service-account": probe_service_account,
    "openstack-appcred": probe_openstack_appcred,
}

assert set(PROBES) == set(KINDS), "every kind must have a probe handler"


def run_probe(entry: Entry, ctx: Context) -> ProbeResult:
    handler = PROBES.get(entry.kind)
    if handler is None:  # pragma: no cover - guarded by the module-level assert
        return ProbeResult(entry.id, entry.kind, _SKIPPED, f"no probe handler for {entry.kind!r}")
    # Optionally sanity-check declared local modes (metadata; no value read).
    result = handler(entry, ctx)
    if result.status == _OK:
        drift = _mode_drift(entry, ctx)
        if drift:
            result.evidence["mode_note"] = drift
    return result


def _mode_drift(entry: Entry, ctx: Context) -> str | None:
    for path, mode in _local_files(entry, ctx):
        actual = octal_mode(path) if path.exists() else None
        if actual and actual.lstrip("0") != mode.lstrip("0"):
            return f"{path.name}: mode {actual} != declared {mode}"
    return None
