"""Validate the vendored upstream JSON against our strict pydantic models.

This is the drift detector: after bumping the upstream pin, run

    poetry run python -m dnd5e_db.validate

and any schema change upstream shows up as record failures, missing
collections, or unexpected new collections. Exit code 0 means the corpus
matches the models exactly.
"""

from __future__ import annotations

import argparse
import json
import sys
from dataclasses import dataclass, field
from pathlib import Path

from pydantic import ValidationError

from dnd5e_db.fetch import DATA_DIR
from dnd5e_db.models import srd2014, srd2024
from dnd5e_db.models.base import SRDModel

EDITIONS: dict[str, tuple[str, dict[str, type[SRDModel]]]] = {
    "2014": ("2014/en", srd2014.COLLECTIONS),
    "2024": ("2024/en", srd2024.COLLECTIONS),
}


@dataclass
class RecordFailure:
    ident: str  # the record's "index" value, or its position as "#N"
    message: str


@dataclass
class CollectionResult:
    filename: str
    records: int
    failures: list[RecordFailure]


@dataclass
class CorpusReport:
    results: list[CollectionResult] = field(default_factory=list)
    missing_files: list[str] = field(default_factory=list)
    unexpected_files: list[str] = field(default_factory=list)

    @property
    def total_records(self) -> int:
        return sum(r.records for r in self.results)

    @property
    def total_failures(self) -> int:
        return sum(len(r.failures) for r in self.results)

    @property
    def ok(self) -> bool:
        return not (self.total_failures or self.missing_files or self.unexpected_files)


def validate_collection(path: Path, model: type[SRDModel]) -> CollectionResult:
    records = json.loads(path.read_text(encoding="utf-8"))
    if not isinstance(records, list):
        return CollectionResult(path.name, 0, [RecordFailure("-", "expected a top-level JSON array")])

    failures = []
    for pos, record in enumerate(records):
        try:
            model.model_validate(record)
        except ValidationError as exc:
            ident = record.get("index") if isinstance(record, dict) else None
            failures.append(RecordFailure(str(ident or f"#{pos}"), str(exc)))
    return CollectionResult(path.name, len(records), failures)


def validate_corpus(data_dir: Path, registry: dict[str, type[SRDModel]]) -> CorpusReport:
    """Validate every registered collection in an edition/language directory."""
    report = CorpusReport()
    for fname, model in sorted(registry.items()):
        path = data_dir / fname
        if not path.is_file():
            report.missing_files.append(fname)
            continue
        report.results.append(validate_collection(path, model))
    on_disk = {p.name for p in data_dir.glob("*.json")} if data_dir.is_dir() else set()
    report.unexpected_files = sorted(on_disk - set(registry))
    return report


def print_report(report: CorpusReport) -> None:
    for result in report.results:
        status = "OK  " if not result.failures else "FAIL"
        print(f"{status} {result.filename}: {result.records - len(result.failures)}/{result.records} records")
        for failure in result.failures:
            print(f"    [{failure.ident}]")
            for line in failure.message.splitlines():
                print(f"      {line}")
    for fname in report.missing_files:
        print(f"MISSING {fname} (in registry, not on disk)")
    for fname in report.unexpected_files:
        print(f"UNEXPECTED {fname} (on disk, not in registry)")
    verdict = "corpus matches models" if report.ok else "DRIFT DETECTED"
    print(f"\n{report.total_records} records, {report.total_failures} failures — {verdict}")


def run(edition: str = "all") -> int:
    selected = list(EDITIONS) if edition == "all" else [edition]
    all_ok = True
    for name in selected:
        subdir, registry = EDITIONS[name]
        print(f"=== SRD {name} ({subdir}) ===")
        report = validate_corpus(DATA_DIR / subdir, registry)
        print_report(report)
        print()
        all_ok &= report.ok
    return 0 if all_ok else 1


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument(
        "--edition",
        choices=[*EDITIONS, "all"],
        default="all",
        help="which SRD edition to validate (stable release gate uses 2014)",
    )
    args = parser.parse_args()
    sys.exit(run(edition=args.edition))


if __name__ == "__main__":
    main()
