#!/usr/bin/env python3
"""Acceptance-oriented validation for the complete F01 evidence package."""

from __future__ import annotations

import csv
import hashlib
import json
import xml.etree.ElementTree as ET
from collections import Counter
from pathlib import Path


ROOT = Path(__file__).resolve().parent.parent
PROCESSED = ROOT / "ANALYSIS" / "processed"
FIGURES = ROOT / "FIGURES"


def read_csv(path: Path) -> list[dict[str, str]]:
    with path.open("r", encoding="utf-8", newline="") as handle:
        return list(csv.DictReader(handle))


def sha256(path: Path) -> str:
    digest = hashlib.sha256()
    with path.open("rb") as handle:
        for chunk in iter(lambda: handle.read(1024 * 1024), b""):
            digest.update(chunk)
    return digest.hexdigest()


def check(condition: bool, label: str, checks: list[dict], detail: str = "") -> None:
    checks.append({"check": label, "status": "pass" if condition else "fail", "detail": detail})


def main() -> None:
    checks: list[dict] = []
    required = [
        "RESEARCH_PLAN.md", "METHODOLOGY.md", "PLATFORM_MODEL.md", "SOURCE_LEDGER.csv",
        "CLAIM_LEDGER.ndjson", "HYPOTHESES.csv", "FINDINGS.md", "COUNTEREVIDENCE.md",
        "EXPERIMENT_BACKLOG.md", "PRODUCT_TICKETS.md", "WORKLOG.md", "CHECKPOINT.json", "FINAL_REPORT.md",
    ]
    check(all((ROOT / name).is_file() for name in required), "required_top_level_artifacts", checks, ", ".join(name for name in required if not (ROOT / name).is_file()))

    manifest = read_csv(PROCESSED / "RAW_MANIFEST.csv")
    check(len(manifest) == 6, "raw_manifest_count", checks, str(len(manifest)))
    check(all(row["verification_status"] == "verified" and row["expected_sha256"] == row["observed_sha256"] for row in manifest), "raw_hashes_match", checks)

    observations = read_csv(PROCESSED / "canonical_observations.csv")
    ids = [row["observation_id"] for row in observations]
    counts = Counter(row["dataset_id"] for row in observations)
    check(len(observations) == 710, "canonical_observation_count", checks, str(len(observations)))
    check(len(ids) == len(set(ids)), "canonical_ids_unique", checks)
    check(counts == {"bea_gdp_vintages": 40, "bls_qcew_revisions": 72, "census_population_vintages": 520, "eia_aeo2026_scenarios": 78}, "dataset_row_counts", checks, str(dict(counts)))
    check(all(row["schema_id"] == "f01-public-data-v1.0.0" for row in observations), "schema_id_frozen", checks)
    check(all(row["source_file_sha256"] and row["source_row_locator"] and row["value_numeric"] != "" for row in observations), "canonical_provenance_and_numeric_coverage", checks)
    check(not any(row["dataset_id"] == "eia_aeo2026_scenarios" and row["observation_period"] == "2024" for row in observations), "eia_absent_2024_not_fabricated", checks)

    pairs = read_csv(PROCESSED / "revision_pairs.csv")
    check(len(pairs) == 311, "revision_pair_count", checks, str(len(pairs)))
    check(all(row["earlier_observation_id"] in set(ids) and row["later_observation_id"] in set(ids) for row in pairs), "revision_pair_parents_exist", checks)
    check(len({row["pair_id"] for row in pairs}) == len(pairs), "revision_pair_ids_unique", checks)

    lineage = read_csv(PROCESSED / "lineage_edges.csv")
    check(len(lineage) == 742, "lineage_edge_count", checks, str(len(lineage)))
    check(all(row["child_id"] and row["parent_id"] and row["transform_id"] for row in lineage), "lineage_edges_complete", checks)

    rebasing = read_csv(PROCESSED / "rebasing_check.csv")
    max_rebase_diff = max(abs(float(row["growth_difference_pp"])) for row in rebasing if row["growth_difference_pp"])
    check(max_rebase_diff < 1e-10, "rebasing_growth_invariance", checks, f"max_pp={max_rebase_diff:.3g}")
    missing = read_csv(PROCESSED / "missingness_sensitivity.csv")[0]
    check(missing["zero_fill_below_documented_lower_bound"] == "true", "missingness_sensitivity_counterexample", checks)

    figures = read_csv(FIGURES / "FIGURE_MANIFEST.csv")
    check(len(figures) == 6, "figure_manifest_count", checks, str(len(figures)))
    figure_valid = True
    figure_details = []
    for row in figures:
        path = ROOT / row["file"]
        try:
            tree = ET.parse(path)
            element_names = {element.tag.rsplit("}", 1)[-1] for element in tree.getroot().iter()}
            valid = sha256(path) == row["sha256"] and {"title", "desc"}.issubset(element_names)
        except Exception as error:  # validation must report, not hide, parser failures
            valid = False
            figure_details.append(f"{path.name}:{error}")
        figure_valid &= valid
    check(figure_valid, "svg_parse_hash_accessibility", checks, "; ".join(figure_details))

    source_rows = read_csv(ROOT / "SOURCE_LEDGER.csv")
    source_ids = {row["source_id"] for row in source_rows}
    check(len(source_rows) >= 17 and len(source_ids) == len(source_rows), "source_ledger_unique_and_sufficient", checks, str(len(source_rows)))
    check(all(row["source_type"] in {"official_disclosure", "primary_technical", "direct_observation", "controlled_fixture"} for row in source_rows), "source_claim_types_allowed", checks)

    claim_records = []
    claim_error = ""
    try:
        for line in (ROOT / "CLAIM_LEDGER.ndjson").read_text(encoding="utf-8").splitlines():
            if line.strip():
                claim_records.append(json.loads(line))
    except Exception as error:
        claim_error = str(error)
    check(not claim_error and len(claim_records) >= 15, "claim_ledger_parses_and_is_sufficient", checks, claim_error or str(len(claim_records)))
    check(all(set(record.get("source_ids", [])) <= source_ids for record in claim_records), "claim_source_ids_resolve", checks)
    check(all(record.get("claim_type") and record.get("confidence") and record.get("falsifier") for record in claim_records), "claim_epistemic_fields_complete", checks)

    packets = sorted(path for path in (ROOT / "ARTICLES").iterdir() if path.is_dir()) if (ROOT / "ARTICLES").is_dir() else []
    packet_files = {"ARTICLE.md", "CLAIMS.ndjson", "SOURCES.csv", "METHOD_NOTE.md", "PUBLICATION_STATUS.json"}
    packet_complete = len(packets) == 5
    packet_statuses = []
    for packet in packets:
        packet_complete &= all((packet / filename).is_file() for filename in packet_files)
        try:
            status = json.loads((packet / "PUBLICATION_STATUS.json").read_text(encoding="utf-8"))["status"]
        except Exception:
            status = "invalid"
        packet_statuses.append(status)
        packet_complete &= status in {"publishable", "not_publishable"}
    check(packet_complete, "five_complete_article_packets", checks, str(packet_statuses))

    hypotheses = read_csv(ROOT / "HYPOTHESES.csv")
    check(len(hypotheses) == 12 and all(row["test_status"] != "preregistered" and row["result"] for row in hypotheses), "hypotheses_adjudicated", checks)
    tickets_text = (ROOT / "PRODUCT_TICKETS.md").read_text(encoding="utf-8") if (ROOT / "PRODUCT_TICKETS.md").exists() else ""
    experiments_text = (ROOT / "EXPERIMENT_BACKLOG.md").read_text(encoding="utf-8") if (ROOT / "EXPERIMENT_BACKLOG.md").exists() else ""
    check(tickets_text.count("## T") >= 5, "product_ticket_count", checks, str(tickets_text.count("## T")))
    check(experiments_text.count("## E") >= 10, "experiment_count", checks, str(experiments_text.count("## E")))

    determinism_path = ANALYSIS_VALIDATION = ROOT / "ANALYSIS" / "validation" / "DETERMINISM_REPORT.json"
    determinism = json.loads(determinism_path.read_text(encoding="utf-8")) if determinism_path.exists() else {}
    check(determinism.get("status") == "pass" and determinism.get("runs") == 2, "two_run_determinism", checks, str(determinism))

    failures = [row for row in checks if row["status"] == "fail"]
    report = {
        "status": "pass" if not failures else "fail",
        "checks_total": len(checks),
        "checks_passed": len(checks) - len(failures),
        "checks_failed": len(failures),
        "checks": checks,
    }
    output = ROOT / "ANALYSIS" / "validation" / "VALIDATION_REPORT.json"
    output.parent.mkdir(parents=True, exist_ok=True)
    output.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8")
    print(json.dumps(report, indent=2, sort_keys=True))
    if failures:
        raise SystemExit(1)


if __name__ == "__main__":
    main()
