"""Emergency & deviation paths — bending the process without breaking it.

A process that cannot bend under fire gets abandoned; one that bends without discipline
rots. Both paths here are **pre-agreed, signed, and reversible**.

Design: deviations live at the **enforcement boundary**, not in the verdict. Gates always
run honestly, so the verdict stays deterministic. A ``deviation`` is a
signed ledger entry that lets ``advance`` accept *specific named red gates* — recorded and
surfaced, never silent. It is also the **sanctioned way to accept a ``gate_gaming`` flag**:
a legitimate suppression is a recorded deviation, not an absorbed one.

* **Deviation:** relaxes named gates with a reason, a human approver, and a
  way back (an ``expires_at`` or an explicit revoke).
* **Emergency:** a constrained profile that may defer only **mutation** and
  **diff-coverage**, never the security/secret gates, and requires a **cleanup within one
  working day** — ``advance`` refuses while that cleanup is overdue.

Human sign-off and provenance are *not* deviatable: they are separate enforcement checks
that ``advance`` / ``deploy-gate`` always require.
"""

from __future__ import annotations

from datetime import datetime, timedelta, timezone
from typing import Any

# A named non-gate requirement a deviation MAY relax: model diversity is
# recommended, not forced — a same-family/same-model oracle proceeds only under a signed deviation.
# The git-discipline guards relax the same way: the clean-start guard, the
# mandatory per-stage commit, and the run-branch isolation at stage boundaries — each maps to
# exactly one named gate, and a relaxation is always a signed, revocable ledger entry.
MODEL_DIVERSITY = "model_diversity"
GIT_CLEAN_START = "git_clean_start"
GIT_STAGE_COMMIT = "git_stage_commit"
GIT_RUN_BRANCH = "git_run_branch"
DEVIATABLE_REQUIREMENTS: tuple[str, ...] = (
    MODEL_DIVERSITY,
    GIT_CLEAN_START,
    GIT_STAGE_COMMIT,
    GIT_RUN_BRANCH,
)

# Gates an emergency fast path MAY defer.
EMERGENCY_DEFERRABLE: tuple[str, ...] = ("mutation", "diff_coverage")
# Gates an emergency fast path shall NEVER relax — the deterministic security + secret gates.
EMERGENCY_FORBIDDEN: tuple[str, ...] = ("sast", "secret_scan", "dependency_scan")
# Default cleanup window for an emergency: one working day.
DEFAULT_CLEANUP_HOURS = 24


def now_utc() -> datetime:
    return datetime.now(timezone.utc)


def iso(dt: datetime) -> str:
    return dt.strftime("%Y-%m-%dT%H:%M:%SZ")


def parse_iso(value: str | None) -> datetime | None:
    """Parse an ISO-8601 timestamp (``...Z`` or with offset); None if absent/malformed.

    Always returns a timezone-aware datetime: a naive value (a date-only or offset-less
    timestamp) is taken as UTC, so every downstream comparison against ``now_utc()`` is
    aware-to-aware and can never raise. A value that will not parse stays ``None``
    (fail-safe: the deviation never expires)."""
    if not value:
        return None
    try:
        parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
    except ValueError:
        return None
    if parsed.tzinfo is None:
        parsed = parsed.replace(tzinfo=timezone.utc)
    return parsed


def emergency_payload(reason: str, approver: str, cleanup_hours: int) -> dict[str, Any]:
    """Build the constrained emergency-deviation payload."""
    return {
        "gates": list(EMERGENCY_DEFERRABLE),
        "reason": reason,
        "approver": approver,
        "emergency": True,
        "expires_at": None,
        "cleanup_due": iso(now_utc() + timedelta(hours=cleanup_hours)),
        "revokes": None,
    }


def deviation_payload(
    gates: list[str], reason: str, approver: str, expires_at: str | None
) -> dict[str, Any]:
    """Build a general deviation payload."""
    return {
        "gates": sorted(set(gates)),
        "reason": reason,
        "approver": approver,
        "emergency": False,
        "expires_at": expires_at,
        "cleanup_due": None,
        "revokes": None,
    }


def active_deviations(
    entries: list[dict[str, Any]], now: datetime | None = None
) -> list[dict[str, Any]]:
    """Currently-active deviations: not expired and not revoked by a later entry."""
    now = now or now_utc()
    revoked: set[int] = set()
    candidates: list[dict[str, Any]] = []
    for e in entries:
        if e.get("type") != "deviation":
            continue
        payload = e.get("payload", {})
        revokes = payload.get("revokes")
        if revokes is not None:
            revoked.add(int(revokes))
            continue
        candidates.append({**payload, "seq": e.get("seq"), "spec_id": e.get("spec_id", "")})
    active: list[dict[str, Any]] = []
    for dev in candidates:
        if dev.get("seq") in revoked:
            continue
        expires = parse_iso(dev.get("expires_at"))
        if expires is not None and now >= expires:
            continue
        active.append(dev)
    return active


def covered_gates(active: list[dict[str, Any]], spec_id: str | None = None) -> set[str]:
    """Gates covered by the active deviations, scoped to ``spec_id`` (empty = global)."""
    gates: set[str] = set()
    for dev in active:
        dev_spec = dev.get("spec_id") or ""
        if spec_id and dev_spec and dev_spec != spec_id:
            continue  # a spec-scoped deviation does not leak to another spec
        gates.update(dev.get("gates", []))
    return gates


def red_gates(verdict_payload: dict[str, Any]) -> set[str]:
    """The names of the failed gates in a recorded verdict payload."""
    return {g["gate"] for g in verdict_payload.get("gates", []) if g.get("status") == "fail"}


def uncovered_red_gates(
    verdict_payload: dict[str, Any],
    active: list[dict[str, Any]],
    spec_id: str | None = None,
) -> set[str]:
    """The verdict's red gates NOT covered by an active signed deviation.

    The single coverage decision both ``advance`` and ``run`` consume, so the two
    enforcement points cannot drift. A deviation scoped to a spec id covers only that
    spec's gates; a global deviation (empty spec id) covers every spec. An empty result
    means every red gate is covered and the enforcement point may proceed — the recorded
    verdict itself stays honestly red (deviations never touch the verdict)."""
    return red_gates(verdict_payload) - covered_gates(active, spec_id)


def covering_deviation(
    gate: str, active: list[dict[str, Any]], spec_id: str | None = None
) -> dict[str, Any] | None:
    """The first active deviation covering ``gate`` for ``spec_id`` (global applies), else None.

    Read-only over the active set — used to annotate a waived red gate with the deviation's
    seq and approver wherever the failure is shown."""
    for dev in active:
        dev_spec = dev.get("spec_id") or ""
        if spec_id and dev_spec and dev_spec != spec_id:
            continue
        if gate in dev.get("gates", []):
            return dev
    return None


def diversity_deviation(active: list[dict[str, Any]], spec_id: str | None = None) -> int | None:
    """Seq of an active deviation relaxing model diversity for ``spec_id`` (global applies too),
    else None. Model diversity is recommended; a same-family setup needs a signed deviation."""
    for dev in active:
        dev_spec = dev.get("spec_id") or ""
        if spec_id and dev_spec and dev_spec != spec_id:
            continue
        if MODEL_DIVERSITY in dev.get("gates", []):
            return dev.get("seq")
    return None


def covers_model_diversity(active: list[dict[str, Any]], spec_id: str | None = None) -> bool:
    """True iff an active deviation relaxes model diversity."""
    return diversity_deviation(active, spec_id) is not None


def overdue_emergencies(
    entries: list[dict[str, Any]], now: datetime | None = None
) -> list[dict[str, Any]]:
    """Active emergency deviations whose one-day cleanup deadline has passed."""
    now = now or now_utc()
    out: list[dict[str, Any]] = []
    for dev in active_deviations(entries, now):
        if not dev.get("emergency"):
            continue
        due = parse_iso(dev.get("cleanup_due"))
        if due is not None and now >= due:
            out.append(dev)
    return out
