"""The sync truth model — shallow (manifest-only) and deep (regenerate -> diff).

Sync state is a first-class value and the dashboard never guesses. Deep check is
authoritative and **non-mutating**: it regenerates into memory and diffs against the
committed ``generated/`` state files (manifest + ``runs/`` are excluded from the
reproduce assertion — the manifest carries a timestamp; ``runs/`` is append-only
history). Integrity (sha256 vs the manifest) catches hand-edits as ``TAMPERED``.
"""

from __future__ import annotations

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

from .generate import GenerateError, generate
from .manifest import check_integrity
from .states import SyncState

if TYPE_CHECKING:
    from .provider import DiskProvider


@dataclass(frozen=True)
class SyncReport:
    state: SyncState
    detail: str
    changed_files: tuple[str, ...] = ()
    reason: str | None = None
    error: str | None = None


def _read_manifest(provider: DiskProvider) -> tuple[dict[str, Any] | None, SyncReport | None]:
    """Return (manifest, None) or (None, fault-report) for UNKNOWN/UNREADABLE."""
    if not provider.has_fedres():
        return None, SyncReport(SyncState.UNKNOWN, "no fed-resources/ — wire this project")
    raw = provider.manifest_bytes()
    if raw is None:
        return None, SyncReport(
            SyncState.UNREADABLE, "fed-resources/ present but generated/manifest.json is missing"
        )
    try:
        manifest = json.loads(raw)
    except json.JSONDecodeError as exc:
        return None, SyncReport(SyncState.UNREADABLE, f"manifest.json is not valid JSON: {exc}")
    if not isinstance(manifest, dict) or manifest.get("kind") != "manifest":
        return None, SyncReport(SyncState.UNREADABLE, "manifest.json is not a manifest document")
    if manifest.get("schema_version") != "0.1":
        return None, SyncReport(
            SyncState.UNREADABLE,
            f"manifest schema_version {manifest.get('schema_version')!r} unrecognized",
        )
    return manifest, None


def _committed_generated(provider: DiskProvider) -> dict[str, bytes]:
    out: dict[str, bytes] = {}
    for relpath in provider.list_generated():
        data = provider.generated_bytes(relpath)
        if data is not None:
            out[relpath] = data
    return out


def shallow_check(provider: DiskProvider) -> SyncReport:
    """Instant verdict from the manifest alone (hashes + source_commit == HEAD). No generate."""
    manifest, fault = _read_manifest(provider)
    if fault is not None:
        return fault
    assert manifest is not None

    integrity = check_integrity(manifest, _committed_generated(provider))
    if not integrity.ok:
        bits = []
        if integrity.mismatched:
            bits.append(f"{len(integrity.mismatched)} edited")
        if integrity.missing:
            bits.append(f"{len(integrity.missing)} missing")
        if integrity.extra:
            bits.append(f"{len(integrity.extra)} unexpected")
        changed = integrity.mismatched + integrity.missing + integrity.extra
        return SyncReport(
            SyncState.TAMPERED,
            f"generated/ does not match its manifest ({', '.join(bits)}) — regenerate",
            changed_files=changed,
        )

    head = provider.head_commit()
    source_commit = manifest.get("source_commit")
    if head and source_commit and source_commit != head:
        return SyncReport(
            SyncState.STALE,
            f"source_commit ({source_commit[:8]}) != HEAD ({head[:8]}): "
            "generated/ may be built from an older commit — deep-check to confirm",
            reason="source_commit != HEAD",
        )
    return SyncReport(SyncState.IN_SYNC, "manifest hashes match and source_commit == HEAD")


def deep_check(provider: DiskProvider) -> SyncReport:
    """Authoritative, non-mutating: regenerate into memory and diff against committed state."""
    manifest, fault = _read_manifest(provider)
    if fault is not None:
        return fault
    assert manifest is not None

    committed = _committed_generated(provider)

    # TAMPERED takes precedence — a hand-edit of generated/ is the real fault.
    integrity = check_integrity(manifest, committed)
    if integrity.mismatched:
        return SyncReport(
            SyncState.TAMPERED,
            f"{len(integrity.mismatched)} generated file(s) were hand-edited away from the manifest",
            changed_files=integrity.mismatched,
        )

    try:
        regen = generate(provider, generated_at="1970-01-01T00:00:00Z", write=False).files
    except GenerateError as exc:
        return SyncReport(SyncState.ERROR, "generator failed", error=str(exc))

    committed_state = {p: b for p, b in committed.items() if not p.startswith("runs/")}
    keys = sorted(set(committed_state) | set(regen))
    changed = tuple(k for k in keys if committed_state.get(k) != regen.get(k))
    if changed:
        return SyncReport(
            SyncState.STALE,
            f"{len(changed)} of {len(regen)} generated file(s) would change on regenerate",
            changed_files=changed,
        )
    return SyncReport(SyncState.IN_SYNC, "regenerate yields byte-identical generated/ state")
