#!/usr/bin/env python3
"""Run reproducible structural and value-level checks on the tidy outputs."""

from __future__ import annotations

import argparse
import hashlib
import json
import math
from pathlib import Path

import pandas as pd


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser()
    parser.add_argument("--project-root", type=Path, default=Path(__file__).resolve().parents[1])
    return parser.parse_args()


def check(condition: bool, message: str) -> None:
    if not condition:
        raise AssertionError(message)


def close(value: float, expected: float, tolerance: float = 1e-8) -> bool:
    return math.isclose(float(value), expected, rel_tol=tolerance, abs_tol=tolerance)


def main() -> None:
    project = parse_args().project_root
    source = pd.read_csv(project / "data" / "raw" / "source_manifest.csv")
    profile = pd.read_csv(project / "data" / "interim" / "workbook_profile.csv")
    facts = pd.read_csv(project / "data" / "processed" / "macstats_tidy_long.csv", low_memory=False)
    latest = pd.read_csv(project / "data" / "processed" / "macstats_latest_vintage.csv", low_memory=False)
    catalog = pd.read_csv(project / "data" / "processed" / "table_catalog.csv")
    issues = pd.read_csv(project / "qa" / "extraction_issues.csv")

    excel_available = source["excel_available"].astype(str).str.lower().eq("true")
    check(len(source) == 69, f"Expected 69 published-source records, found {len(source)}")
    check((source["source_kind"] == "archive_zip").sum() == 18, "Archive edition count changed")
    check((source["source_kind"] == "current_exhibit").sum() == 51, "Current exhibit count changed")
    check(excel_available.sum() == 61, "Downloadable Excel/ZIP source count changed")
    check((~excel_available).sum() == 8, "PDF-only current exhibit count changed")

    for row in source[excel_available].itertuples():
        path = project / row.local_path
        check(path.exists() and path.stat().st_size > 0, f"Missing source file: {row.local_path}")
        digest = hashlib.sha256(path.read_bytes()).hexdigest()
        check(digest == row.sha256, f"Checksum mismatch: {row.local_path}")

    check(profile["workbook_path"].nunique() == 499, "Workbook count changed")
    check(len(profile) == 864, "Worksheet profile count changed")
    check(not profile["workbook_path"].astype(str).str.startswith("/").any(), "Workbook paths must be portable")
    check(len(issues) == 0, f"Unresolved extraction issues: {len(issues)}")
    check(catalog["manual_review"].sum() == 0, "Unreviewed series mappings remain")
    check(catalog["observation_count"].gt(0).all(), "A retained table has no observations")

    check(len(facts) > 250_000, "Unexpectedly low fact count")
    check(facts["series_id"].nunique() == 67, "Series count changed")
    check(facts["table_key"].notna().all(), "Missing table key")
    check(facts.duplicated().sum() == 0, "Duplicate full fact rows")
    check(not facts["source_path"].astype(str).str.startswith("/").any(), "Fact source paths must be portable")
    check(facts["column_path"].astype(str).str.match(r"^column_\d+$").sum() == 0, "Generic column labels remain")
    check(facts["source_cell"].astype(str).str.startswith("xl/charts/").sum() > 0, "Chart-cache extraction is missing")
    check(0 < len(latest) <= len(facts), "Latest-vintage output has an invalid row count")

    current_downloadable = set(source.loc[(source["source_kind"] == "current_exhibit") & excel_available, "exhibit_number"].astype(int))
    current_catalog = set(catalog.loc[catalog["report_edition"] == "2026-02", "source_exhibit_number"].astype(int))
    check(current_downloadable <= current_catalog, "A downloadable current exhibit is absent from the table catalog")

    early = facts[
        (facts["series_id"] == "macstats_exhibit_16")
        & (facts["report_edition"] == "2011-03")
        & (facts["row_label"] == "Alabama")
        & (facts["column_path"] == "Benefits | Total")
    ]
    check(len(early) == 1 and close(early.iloc[0]["value_numeric"], 4836), "2011 Alabama benefit-spending spot check failed")

    current_spending = facts[
        (facts["series_id"] == "macstats_exhibit_16")
        & (facts["report_edition"] == "2026-02")
        & (facts["state"] == "Alabama")
        & (facts["column_path"] == "Benefits | Total")
    ]
    check(len(current_spending) == 1 and close(current_spending.iloc[0]["value_numeric"], 8109.366769), "Current Alabama benefit-spending spot check failed")

    fmap = facts[
        (facts["series_id"] == "macstats_exhibit_06")
        & (facts["report_edition"] == "2026-02")
        & (facts["state"] == "Alabama")
        & (facts["column_path"].str.contains("FMAPs for Medicaid", na=False))
        & (facts["column_path"].str.contains("FY 2026", na=False))
    ]
    check(len(fmap) == 1 and close(fmap.iloc[0]["value_numeric"], 0.7263), "FY 2026 Alabama FMAP raw-value check failed")
    check(close(fmap.iloc[0]["value_normalized"], 72.63), "FY 2026 Alabama FMAP normalization check failed")

    crosswalk = facts[
        (facts["series_id"] == "macstats_exhibit_51")
        & (facts["report_edition"] == "2018-12")
        & (facts["row_label"] == "Hospital")
        & (facts["value_raw"].str.contains("Inpatient hospital non-DSH", na=False))
    ]
    check(len(crosswalk) >= 1, "Text-only service-category crosswalk extraction failed")

    # Mixed amount/share columns must both survive latest-vintage selection.
    for sid in ["macstats_exhibit_03", "macstats_exhibit_12"]:
        current = latest[(latest["series_id"] == sid) & (latest["report_edition"] == "2026-02")]
        units = current.groupby(["row_label", "column_path"])["source_unit"].agg(set)
        check(any("percent" in u and ("millions" in u or "billions" in u) for u in units), f"Amount/share collision returned in {sid}")
    counts = facts[(facts["series_id"] == "macstats_exhibit_29") & facts["column_path"].str.contains("Total Medicaid Enrollees", case=False, na=False)]
    check(not counts["source_unit"].eq("percent").any(), "Managed-care enrollment counts cannot be percentages")
    fye = facts[(facts["series_id"] == "macstats_exhibit_15") & (facts["report_edition"] == "2026-02") & facts["column_path"].str.contains("All enrollees", case=False, na=False)]
    check(fye["source_unit"].eq("thousands").all(), "FYE thousands scale changed")
    nhe = facts[(facts["series_id"] == "macstats_exhibit_12") & ~facts["source_table_title"].str.contains(r'\bFY', case=False, regex=True, na=False)]
    check(not nhe["period_type"].eq("fiscal_year").any(), "Year-only NHE rows cannot be inferred to be fiscal years; explicit historical FY source labels are preserved for review")
    subtotals = facts[facts["row_label"].str.match(r"(?i)^subtotal\b", na=False)]
    check(subtotals["include_in_reporting"].eq(0).all(), "Subtotals cannot be detail rows")
    current_categories = facts[(facts["series_id"] == "macstats_exhibit_17") & (facts["report_edition"] == "2026-02")]
    check(current_categories["data_source"].eq("CMS-64").all(), "Category source must follow the CMS-64 source note")

    summary = {
        "status": "passed",
        "published_source_records": int(len(source)),
        "downloaded_files": int(excel_available.sum()),
        "archive_editions": 18,
        "current_exhibits": 51,
        "pdf_only_current_exhibits": int((~excel_available).sum()),
        "workbooks": int(profile["workbook_path"].nunique()),
        "worksheets_profiled": int(len(profile)),
        "retained_tables": int(len(catalog)),
        "series": int(facts["series_id"].nunique()),
        "all_vintage_observations": int(len(facts)),
        "latest_vintage_observations": int(len(latest)),
        "chart_cache_observations": int(facts["source_cell"].astype(str).str.startswith("xl/charts/").sum()),
        "unresolved_extraction_issues": int(len(issues)),
        "unreviewed_series_mappings": int(catalog["manual_review"].sum()),
    }
    output = project / "qa" / "validation_summary.json"
    output.write_text(json.dumps(summary, indent=2) + "\n", encoding="utf-8")
    print(json.dumps(summary, indent=2))


if __name__ == "__main__":
    main()
