"""Audit this corpus snapshot without changing bibliographic or review state.

Writes reproducible follow-up inventories, not relevance or identity decisions.
Run after explorer-data; the register hash must match the current builder input.
"""
from __future__ import annotations

import csv
import argparse
import hashlib
import json
from collections import Counter, defaultdict
from datetime import date
from pathlib import Path

from scripts.build_explorer import REGISTER, ROOT

AUDIT_DATE = date.today().isoformat()
OUTPUT = ROOT / f"reviews/quality-{AUDIT_DATE}"
TITLE_ONLY_PHRASES = ("title alone", "no cached abstract", "title-only", "lacks accessible")


def title_key(value):
    return "".join(c for c in value.casefold() if c.isalnum())


def reason_needs_refresh(record):
    return bool(record["abstract"]) and any(
        phrase in record["reason"].lower() for phrase in TITLE_ONLY_PHRASES
    )


def write_csv(name, fields, rows):
    with (OUTPUT / name).open("w") as stream:
        writer = csv.DictWriter(stream, fieldnames=fields)
        writer.writeheader()
        writer.writerows(rows)


def identity(record):
    return {"canonical_work_id": record["id"], "title": record["title"],
            "decision": record["decision"], "doi": record["doi"], "arxiv_id": record["arxiv"],
            "source_identifiers": ";".join(record["sourceIdentifiers"])}


def audit():
    snapshot_path = ROOT / "data/explorer/corpus.json"
    raw = snapshot_path.read_bytes()
    snapshot = json.loads(raw)
    register_hash = hashlib.sha256(REGISTER.read_bytes()).hexdigest()
    if snapshot["meta"]["registerSha256"] != register_hash:
        raise ValueError("Rebuild explorer-data: snapshot and review register differ")
    if not snapshot["meta"]["enriched"]:
        raise ValueError("This audit needs the enriched local snapshot")
    previous_summary = OUTPUT / "summary.json"
    if previous_summary.exists() and json.loads(previous_summary.read_text())["register_sha256"] != register_hash:
        raise ValueError("An audit for another register exists here; preserve it and choose a new --output directory")
    records = snapshot["records"]
    with REGISTER.open() as stream:
        original_reviews = {int(r["canonical_work_id"]): r for r in csv.DictReader(stream)}
    for record in records:
        for field in ("reason", "evidence"):
            record[field] = original_reviews[record["id"]][field]
    # Audits use private metadata, never the public projection as an abstract store.
    from patchscholar.config import AppPaths
    from patchscholar.inspect import inspect_canonical_works
    for start in range(0, len(records), 400):
        batch = records[start:start + 400]
        rows = inspect_canonical_works(paths=AppPaths(root=ROOT), canonical_work_ids=[r["id"] for r in batch], limit=len(batch), include_abstract=True)
        abstracts = {r["id"]: r.get("abstract") or "" for r in rows}
        for record in batch:
            record["abstract"] = abstracts[record["id"]]
    bibliography = json.loads((ROOT / "data/explorer/bibliography.json").read_text())
    if bibliography["registerSha256"] != register_hash:
        raise ValueError("Rebuild explorer-data: bibliography and review register differ")
    deferred = [r for r in records if r["decision"] == "defer"]
    accepted_missing = [r for r in records if r["decision"] == "include" and not r["abstract"]]
    groups = defaultdict(list)
    for record in records:
        if len(record["abstract"]) > 100:
            groups[" ".join(record["abstract"].casefold().split())].append(record)
    duplicate_groups = [group for group in groups.values()
                        if len({title_key(r["title"]) for r in group}) > 1]
    duplicate_groups.sort(key=lambda group: min(r["id"] for r in group))
    OUTPUT.mkdir(parents=True, exist_ok=True)
    base_fields = list(identity(records[0]))
    write_csv("deferred.csv", base_fields + ["has_abstract", "reason_needs_refresh", "reason", "evidence", "review_register"], [
        {**identity(r), "has_abstract": bool(r["abstract"]), "reason_needs_refresh": reason_needs_refresh(r),
         "reason": r["reason"], "evidence": r["evidence"], "review_register": r["reviewRegister"]}
        for r in sorted(deferred, key=lambda r: (not reason_needs_refresh(r), not bool(r["abstract"]), r["id"]))
    ])
    write_csv("accepted-missing-abstract.csv", base_fields + ["discovery_batch", "review_register"], [
        {**identity(r), "discovery_batch": r["discoveryBatch"], "review_register": r["reviewRegister"]}
        for r in accepted_missing
    ])
    write_csv("shared-abstract.csv", ["group"] + base_fields + ["existing_identity_flag", "abstract_sha256"], [
        {"group": i, **identity(r), "existing_identity_flag": r["flagged"],
         "abstract_sha256": hashlib.sha256(" ".join(r["abstract"].casefold().split()).encode()).hexdigest()}
        for i, group in enumerate(duplicate_groups, 1) for r in group
    ])
    linked = {e[k] for e in snapshot["edges"] for k in ("source", "target")}
    summary = {
        "audit_date": AUDIT_DATE, "snapshot": snapshot["meta"]["snapshot"],
        "register": str(REGISTER.relative_to(ROOT)), "register_sha256": register_hash,
        "snapshot_sha256": hashlib.sha256(raw).hexdigest(),
        "decisions": dict(Counter(r["decision"] for r in records)),
        "deferred": {"total": len(deferred), "with_abstract": sum(bool(r["abstract"]) for r in deferred),
                     "reason_needs_refresh": sum(reason_needs_refresh(r) for r in deferred),
                     "abstract_mentions_andreev": sum("andreev" in r["abstract"].lower() for r in deferred)},
        "accepted_missing": {field: sum(not r[field] for r in records if r["decision"] == "include")
                             for field in ("abstract", "authors", "doi", "arxiv", "venue", "date")},
        "accepted_identity_flag": sum(r["flagged"] for r in records if r["decision"] == "include"),
        "bibtex_unavailable": bibliography["unavailable"],
        "shared_abstract": {"groups": len(duplicate_groups), "records": sum(map(len, duplicate_groups)),
                            "groups_with_multiple_inclusions": sum(sum(r["decision"] == "include" for r in g) > 1 for g in duplicate_groups),
                            "accepted_without_identity_flag": sum(r["decision"] == "include" and not r["flagged"] for g in duplicate_groups for r in g)},
        "graph": {"edges": len(snapshot["edges"]), "unlinked": sum(r["id"] not in linked for r in records),
                  "accepted_unlinked": sum(r["id"] not in linked and r["decision"] == "include" for r in records)},
        "batches": {batch: {"decisions": dict(Counter(r["decision"] for r in records if r["discoveryBatch"] == batch)),
                            "missing_abstracts_by_decision": dict(Counter(r["decision"] for r in records if r["discoveryBatch"] == batch and not r["abstract"]))}
                    for batch in sorted({r["discoveryBatch"] for r in records})},
        "interpretation": "Follow-up indicators only. Shared abstracts do not establish identity; title-only phrase matches do not establish wrong decisions. No judgments changed.",
    }
    (OUTPUT / "summary.json").write_text(json.dumps(summary, indent=2) + "\n")
    print(json.dumps(summary, indent=2))


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--output", type=Path, default=OUTPUT, help="New audit directory; existing different-register audits are protected")
    OUTPUT = parser.parse_args().output
    audit()
