"""Append-only qualification and revocation authority for Task26 candidates.

Historical bundle PASS artifacts remain immutable statements about past
qualification.  Current authority is derived only from these paired registry and
ledger chains.
"""
from __future__ import annotations

import hashlib
import json
import os
import re
from pathlib import Path
from typing import Any, Literal, cast

AuthorityAction = Literal["qualify", "revoke"]
ZERO = "0" * 64
AUTHORITY_STATEMENT = (
    "historical immutable PASS describes past qualification; current authority "
    "comes only from this append-only registry and qualification ledger"
)
_DIGEST = re.compile(r"^[0-9a-f]{64}$")


def _canonical(value: object) -> bytes:
    return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"), allow_nan=False).encode()


def _digest(value: object) -> str:
    return hashlib.sha256(_canonical(value)).hexdigest()


def _object(value: object, label: str) -> dict[str, Any]:
    if not isinstance(value, dict) or any(not isinstance(key, str) for key in value):
        raise ValueError(f"{label} is invalid")
    return cast(dict[str, Any], value)


def _read(path: Path, label: str) -> dict[str, Any]:
    if path.is_symlink() or not path.is_file():
        raise ValueError(f"{label} is unavailable")
    return _object(json.loads(path.read_text(encoding="utf-8")), label)


def _write(path: Path, value: object) -> None:
    path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
    path.parent.chmod(0o700)
    temporary = path.with_name(f".{path.name}.{os.getpid()}.tmp")
    fd = os.open(temporary, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_CLOEXEC | os.O_NOFOLLOW, 0o600)
    try:
        os.write(fd, _canonical(value) + b"\n")
        os.fsync(fd)
    finally:
        os.close(fd)
    os.replace(temporary, path)
    path.chmod(0o600)


def _paths(root: Path) -> tuple[Path, Path]:
    authority = root / "candidate-authority"
    return authority / "registry.json", authority / "qualification-ledger.json"


def _empty() -> tuple[dict[str, object], dict[str, object]]:
    registry: dict[str, object] = {
        "schema": "task26-candidate-revocation-registry-v1",
        "authority_statement": AUTHORITY_STATEMENT,
        "events": [],
        "head_sha256": ZERO,
        "ledger_head_sha256": ZERO,
        "current_qualified_candidate": None,
        "invalidated_candidate_digests": [],
    }
    registry["document_sha256"] = _digest(registry)
    ledger: dict[str, object] = {
        "schema": "task26-qualification-ledger-v1",
        "authority_statement": AUTHORITY_STATEMENT,
        "rows": [],
        "head_sha256": ZERO,
        "registry_head_sha256": ZERO,
    }
    ledger["document_sha256"] = _digest(ledger)
    return registry, ledger


def append_candidate_authority(
    root: Path,
    *,
    candidate_digest: str,
    action: AuthorityAction,
    historical_pass_digest: str,
    reason: str,
) -> dict[str, object]:
    """Append one paired registry event and ledger row."""
    if _DIGEST.fullmatch(candidate_digest) is None or _DIGEST.fullmatch(historical_pass_digest) is None:
        raise ValueError("candidate authority digest is invalid")
    if action not in {"qualify", "revoke"} or not reason.strip() or len(reason) > 240:
        raise ValueError("candidate authority action is invalid")
    registry_path, ledger_path = _paths(root)
    if registry_path.exists() or ledger_path.exists():
        verify_candidate_authority(root, None)
        registry = _read(registry_path, "candidate revocation registry")
        ledger = _read(ledger_path, "qualification ledger")
    else:
        registry, ledger = _empty()
    events = list(cast(list[object], registry["events"]))
    rows = list(cast(list[object], ledger["rows"]))
    invalidated = list(cast(list[str], registry["invalidated_candidate_digests"]))
    qualified = {
        str(cast(dict[str, object], event)["candidate_digest"])
        for event in events
        if cast(dict[str, object], event).get("action") == "qualify"
    }
    if action == "revoke" and (candidate_digest not in qualified or candidate_digest in invalidated):
        raise ValueError("revocation has no bound predecessor qualification")
    if action == "qualify" and candidate_digest in invalidated:
        raise ValueError("a revoked digest cannot be requalified")
    sequence = len(events) + 1
    event: dict[str, object] = {
        "sequence": sequence,
        "previous_sha256": registry["head_sha256"],
        "ledger_predecessor_sha256": ledger["head_sha256"],
        "action": action,
        "candidate_digest": candidate_digest,
        "historical_pass_digest": historical_pass_digest,
        "historical_pass_scope": "past_qualification_only",
        "reason": reason.strip(),
    }
    event["event_sha256"] = _digest(event)
    row: dict[str, object] = {
        "sequence": sequence,
        "previous_sha256": ledger["head_sha256"],
        "registry_event_sha256": event["event_sha256"],
        "action": action,
        "candidate_digest": candidate_digest,
        "historical_pass_digest": historical_pass_digest,
        "historical_pass_scope": "past_qualification_only",
    }
    row["row_sha256"] = _digest(row)
    events.append(event)
    rows.append(row)
    if action == "revoke":
        invalidated.append(candidate_digest)
    current = candidate_digest if action == "qualify" else registry["current_qualified_candidate"]
    if action == "revoke" and current == candidate_digest:
        current = None
    next_registry: dict[str, object] = {
        "schema": "task26-candidate-revocation-registry-v1",
        "authority_statement": AUTHORITY_STATEMENT,
        "events": events,
        "head_sha256": event["event_sha256"],
        "ledger_head_sha256": row["row_sha256"],
        "current_qualified_candidate": current,
        "invalidated_candidate_digests": sorted(invalidated),
    }
    next_registry["document_sha256"] = _digest(next_registry)
    next_ledger: dict[str, object] = {
        "schema": "task26-qualification-ledger-v1",
        "authority_statement": AUTHORITY_STATEMENT,
        "rows": rows,
        "head_sha256": row["row_sha256"],
        "registry_head_sha256": event["event_sha256"],
    }
    next_ledger["document_sha256"] = _digest(next_ledger)
    _write(registry_path, next_registry)
    _write(ledger_path, next_ledger)
    return {"registry_head_sha256": event["event_sha256"], "ledger_head_sha256": row["row_sha256"], "current_qualified_candidate": current}


def verify_candidate_authority(root: Path, candidate_digest: str | None) -> dict[str, object]:
    """Verify both chains and, when supplied, require current non-revoked authority."""
    registry_path, ledger_path = _paths(root)
    registry = _read(registry_path, "candidate revocation registry")
    ledger = _read(ledger_path, "qualification ledger")
    if registry.get("schema") != "task26-candidate-revocation-registry-v1" or ledger.get("schema") != "task26-qualification-ledger-v1":
        raise ValueError("candidate authority schema is invalid")
    if registry.get("authority_statement") != AUTHORITY_STATEMENT or ledger.get("authority_statement") != AUTHORITY_STATEMENT:
        raise ValueError("candidate authority scope is invalid")
    if registry.get("document_sha256") != _digest({key: value for key, value in registry.items() if key != "document_sha256"}):
        raise ValueError("candidate registry document digest is invalid")
    if ledger.get("document_sha256") != _digest({key: value for key, value in ledger.items() if key != "document_sha256"}):
        raise ValueError("qualification ledger document digest is invalid")
    events = registry.get("events")
    rows = ledger.get("rows")
    if not isinstance(events, list) or not isinstance(rows, list) or len(events) != len(rows) or not events:
        raise ValueError("candidate authority chain cardinality is invalid")
    registry_previous = ZERO
    ledger_previous = ZERO
    current: str | None = None
    invalidated: set[str] = set()
    qualified: set[str] = set()
    for sequence, (raw_event, raw_row) in enumerate(zip(events, rows, strict=True), 1):
        event = _object(raw_event, "candidate registry event")
        row = _object(raw_row, "qualification ledger row")
        event_hash = event.get("event_sha256")
        row_hash = row.get("row_sha256")
        if (
            event.get("sequence") != sequence
            or row.get("sequence") != sequence
            or event.get("previous_sha256") != registry_previous
            or event.get("ledger_predecessor_sha256") != ledger_previous
            or row.get("previous_sha256") != ledger_previous
            or row.get("registry_event_sha256") != event_hash
            or event_hash != _digest({key: value for key, value in event.items() if key != "event_sha256"})
            or row_hash != _digest({key: value for key, value in row.items() if key != "row_sha256"})
            or any(event.get(key) != row.get(key) for key in ("action", "candidate_digest", "historical_pass_digest", "historical_pass_scope"))
            or event.get("historical_pass_scope") != "past_qualification_only"
        ):
            raise ValueError("candidate authority predecessor or digest binding is invalid")
        digest = str(event.get("candidate_digest"))
        if event.get("action") == "qualify":
            if digest in invalidated:
                raise ValueError("revoked candidate was requalified")
            qualified.add(digest)
            current = digest
        elif event.get("action") == "revoke":
            if digest not in qualified or digest in invalidated:
                raise ValueError("revocation has no bound predecessor qualification")
            invalidated.add(digest)
            if current == digest:
                current = None
        else:
            raise ValueError("candidate authority action is invalid")
        registry_previous = str(event_hash)
        ledger_previous = str(row_hash)
    if (
        registry.get("head_sha256") != registry_previous
        or registry.get("ledger_head_sha256") != ledger_previous
        or ledger.get("head_sha256") != ledger_previous
        or ledger.get("registry_head_sha256") != registry_previous
        or registry.get("current_qualified_candidate") != current
        or registry.get("invalidated_candidate_digests") != sorted(invalidated)
    ):
        raise ValueError("latest registry and ledger binding is stale")
    if candidate_digest is not None:
        if candidate_digest in invalidated:
            raise ValueError("candidate digest is revoked")
        if current != candidate_digest:
            raise ValueError("candidate is not the latest qualified candidate")
    return {"registry_head_sha256": registry_previous, "ledger_head_sha256": ledger_previous, "current_qualified_candidate": current, "invalidated_candidate_digests": sorted(invalidated)}
