"""The manifest — the sync anchor over ``generated/**``.

Records, at generation time, the source commit + a sha256 for every generated
file, so a shallow check (hashes match + source_commit == HEAD) can give a fast
verdict and ``TAMPERED`` is detectable without regenerating.
"""

from __future__ import annotations

import hashlib
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any

if TYPE_CHECKING:
    from collections.abc import Mapping

#: ``generated/`` basename (or ``runs/`` prefix) -> schema tag recorded in the manifest.
SCHEMA_TAG: dict[str, str] = {
    "status.json": "status@0.1",
    "roadmap.json": "roadmap@0.1",
    "architecture.json": "architecture@0.1",
    "focus.json": "focus@0.1",
    "suggestions.json": "suggestions@0.1",
    "federation.json": "federation@0.1",
    "test_catalog.json": "test_catalog@0.1",
}


def sha256_hex(data: bytes) -> str:
    return hashlib.sha256(data).hexdigest()


def schema_tag_for(relpath: str, kind: str | None = None) -> str:
    """Schema tag for a generated file; ``runs/*`` are derived from the record kind."""
    if relpath in SCHEMA_TAG:
        return SCHEMA_TAG[relpath]
    if relpath.startswith("runs/"):
        return f"{kind or 'run_record'}@0.1"
    return f"{kind or 'unknown'}@0.1"


def build_manifest(
    *,
    project: dict[str, str],
    source_commit: str,
    generated_at: str,
    generator: dict[str, str],
    files: Mapping[str, bytes],
    tags: Mapping[str, str] | None = None,
) -> dict[str, Any]:
    """Build a manifest dict from the generated files' bytes (entries sorted by path)."""
    tags = tags or {}
    entries = [
        {
            "path": path,
            "sha256": sha256_hex(content),
            "schema": tags.get(path, schema_tag_for(path)),
        }
        for path, content in sorted(files.items())
    ]
    return {
        "schema_version": "0.1",
        "kind": "manifest",
        "project": project,
        "generated_at": generated_at,
        "source_commit": source_commit,
        "generator": generator,
        "files": entries,
    }


@dataclass(frozen=True)
class IntegrityResult:
    ok: bool
    missing: tuple[str, ...]
    mismatched: tuple[str, ...]
    extra: tuple[str, ...]


def check_integrity(manifest: dict[str, Any], present: Mapping[str, bytes]) -> IntegrityResult:
    """Compare on-disk ``generated/`` bytes against the manifest's recorded sha256s.

    ``mismatched`` non-empty -> TAMPERED (files hand-edited away from the manifest).
    """
    recorded = {entry["path"]: entry["sha256"] for entry in manifest.get("files", [])}
    missing = tuple(sorted(p for p in recorded if p not in present))
    extra = tuple(sorted(p for p in present if p not in recorded))
    mismatched = tuple(
        sorted(p for p, h in recorded.items() if p in present and sha256_hex(present[p]) != h)
    )
    return IntegrityResult(
        ok=not (missing or mismatched or extra),
        missing=missing,
        mismatched=mismatched,
        extra=extra,
    )
