"""Rebuild the evidence-bounded STRUST v1 case-study outputs.

The input is a literal transcription of a historical PDF table. This script
does not execute COBOL or Java and does not treat the author-reported intended
classes as independent ground truth.
"""

from __future__ import annotations

import argparse
import csv
import hashlib
import io
import json
from pathlib import Path
from typing import Any
from evidence_graph import (
    build_claim_evidence,
    check_outputs as check_claim_evidence_outputs,
    write_outputs as write_claim_evidence_outputs,
)


HERE = Path(__file__).resolve().parent
CONFIG_PATH = HERE / "study_config.json"
RESULTS_DIR = HERE / "results"
FIGURES_DIR = HERE / "figures"

CASE_FIELDS = [
    "case_id",
    "scenario",
    "intended_class",
    "recorded_status",
    "source_note",
    "source_page",
    "source_table",
]

COLORS = {
    "ink": "#182333",
    "navy": "#143D59",
    "teal": "#0B6E69",
    "coral": "#C65D4B",
    "amber": "#D89B2B",
    "slate": "#667482",
    "line": "#D5DCE1",
    "pale_blue": "#E8F3F8",
    "pale_teal": "#E7F4F1",
    "pale_coral": "#FAEAE7",
    "pale_gray": "#F2F4F5",
}


def sha256(path: Path) -> str:
    return hashlib.sha256(path.read_bytes()).hexdigest()


def load_config(path: Path = CONFIG_PATH) -> dict[str, Any]:
    config = json.loads(path.read_text(encoding="utf-8"))
    source = HERE / config["source"]["path"]
    if not source.is_file():
        raise FileNotFoundError(f"historical source is missing: {source}")
    actual_hash = sha256(source)
    expected_hash = config["source"]["sha256"]
    if actual_hash != expected_hash:
        raise AssertionError(
            f"historical source hash mismatch: expected {expected_hash}, got {actual_hash}"
        )
    return config


def load_ledger(config: dict[str, Any]) -> list[dict[str, str]]:
    path = HERE / config["ledger"]["path"]
    with path.open(newline="", encoding="utf-8") as handle:
        reader = csv.DictReader(handle)
        if reader.fieldnames != CASE_FIELDS:
            raise AssertionError(
                f"ledger fields must be {CASE_FIELDS}, got {reader.fieldnames}"
            )
        rows = list(reader)

    if not rows:
        raise AssertionError("ledger must contain at least one case")
    ids = [row["case_id"] for row in rows]
    if len(ids) != len(set(ids)):
        raise AssertionError("case IDs must be unique")
    if ids != [f"T{index}" for index in range(1, len(rows) + 1)]:
        raise AssertionError("case IDs must form the ordered sequence T1..Tn")

    for row in rows:
        if row["intended_class"] not in {"Equivalent", "Non-equivalent"}:
            raise AssertionError(f"unsupported intended class in {row['case_id']}")
        if row["recorded_status"] not in {"Pass", "Fail"}:
            raise AssertionError(f"unsupported recorded status in {row['case_id']}")
        try:
            page = int(row["source_page"])
        except ValueError as exc:
            raise AssertionError(f"invalid source page in {row['case_id']}") from exc
        if page < 1:
            raise AssertionError(f"source page must be positive in {row['case_id']}")
    return rows


def derive_case_rows(rows: list[dict[str, str]]) -> list[dict[str, Any]]:
    return [
        {
            **row,
            "computed_success": int(row["recorded_status"] == "Pass"),
        }
        for row in rows
    ]


def build_summary(
    config: dict[str, Any], rows: list[dict[str, Any]]
) -> dict[str, Any]:
    total = len(rows)
    passes = sum(row["computed_success"] for row in rows)
    failures = total - passes
    pass_rate = passes / total
    historical_claim = float(config["source"]["accuracy_claim"])
    inventory = config["evidence_inventory"]
    status_counts = {
        status: sum(item["status"] == status for item in inventory)
        for status in ("present", "missing")
    }
    return {
        "study_id": config["study_id"],
        "analysis_date": config["analysis_date"],
        "classification": config["classification"],
        "source": {
            "path": config["source"]["path"],
            "sha256": config["source"]["sha256"],
            "git_commit": config["source"]["git_commit"],
            "git_blob": config["source"]["git_blob"],
            "ledger_locator": config["source"]["ledger_locator"],
        },
        "ledger": {
            "case_count": total,
            "recorded_passes": passes,
            "recorded_failures": failures,
            "recorded_pass_rate": pass_rate,
            "intended_equivalent_cases": sum(
                row["intended_class"] == "Equivalent" for row in rows
            ),
            "intended_non_equivalent_cases": sum(
                row["intended_class"] == "Non-equivalent" for row in rows
            ),
            "failed_case_ids": [
                row["case_id"] for row in rows if not row["computed_success"]
            ],
        },
        "historical_text_claim": {
            "claimed_accuracy": historical_claim,
            "locators": config["source"]["accuracy_claim_locators"],
        },
        "internal_consistency_audit": {
            "claim_minus_table_rate": round(historical_claim - pass_rate, 12),
            "claim_minus_table_percentage_points": round(
                100.0 * (historical_claim - pass_rate), 10
            ),
            "consistent": historical_claim == pass_rate,
        },
        "evidence_inventory": {
            "present": status_counts["present"],
            "missing": status_counts["missing"],
            "total": len(inventory),
        },
        "interpretation": (
            "Document-level arithmetic and provenance audit only; not an "
            "independent accuracy, feasibility, performance, or scalability evaluation."
        ),
    }


def csv_text(fieldnames: list[str], rows: list[dict[str, Any]]) -> str:
    buffer = io.StringIO(newline="")
    writer = csv.DictWriter(buffer, fieldnames=fieldnames, lineterminator="\n")
    writer.writeheader()
    writer.writerows(rows)
    return buffer.getvalue()


def expected_text_outputs(
    config: dict[str, Any], rows: list[dict[str, Any]], summary: dict[str, Any]
) -> dict[Path, str]:
    case_fields = CASE_FIELDS + ["computed_success"]
    evidence_fields = ["artifact", "status", "implication"]
    return {
        RESULTS_DIR / "case_audit.csv": csv_text(case_fields, rows),
        RESULTS_DIR / "evidence_inventory.csv": csv_text(
            evidence_fields, config["evidence_inventory"]
        ),
        RESULTS_DIR / "summary.json": json.dumps(
            summary, indent=2, sort_keys=True
        )
        + "\n",
    }


def write_text_outputs(outputs: dict[Path, str]) -> None:
    RESULTS_DIR.mkdir(parents=True, exist_ok=True)
    for path, content in outputs.items():
        path.write_text(content, encoding="utf-8", newline="")


def wrap_label(value: str) -> str:
    replacements = {
        "String processing": "String\nprocessing",
        "Conditional logic": "Conditional\nlogic",
        "File I/O": "File I/O",
        "Numerical precision": "Numerical\nprecision",
        "Order dependency": "Order\ndependency",
        "Error handling": "Error\nhandling",
        "Rounding behavior": "Rounding\nbehavior",
        "Character encoding": "Character\nencoding",
    }
    return replacements.get(value, value)


def configure_matplotlib() -> None:
    import matplotlib as mpl

    mpl.rcParams.update(
        {
            "font.family": "DejaVu Sans",
            "font.size": 8.0,
            "axes.titlesize": 9.4,
            "axes.titleweight": "semibold",
            "axes.labelcolor": COLORS["ink"],
            "axes.edgecolor": COLORS["line"],
            "text.color": COLORS["ink"],
            "xtick.color": COLORS["slate"],
            "ytick.color": COLORS["slate"],
            "pdf.fonttype": 42,
            "ps.fonttype": 42,
        }
    )


def plot_audit(
    config: dict[str, Any], rows: list[dict[str, Any]], summary: dict[str, Any]
) -> None:
    import matplotlib.pyplot as plt
    from matplotlib.patches import FancyBboxPatch, Rectangle

    configure_matplotlib()
    FIGURES_DIR.mkdir(parents=True, exist_ok=True)

    figure = plt.figure(figsize=(7.10, 4.35), facecolor="white")
    grid = figure.add_gridspec(
        2,
        2,
        height_ratios=(1.0, 1.55),
        width_ratios=(3.2, 1.0),
        hspace=0.50,
        wspace=0.32,
    )
    cases_axis = figure.add_subplot(grid[0, 0])
    claim_axis = figure.add_subplot(grid[0, 1])
    inventory_axis = figure.add_subplot(grid[1, :])

    # Panel a: retain the source vocabulary and expose every row.
    for index, row in enumerate(rows):
        passed = bool(row["computed_success"])
        fill = COLORS["pale_teal"] if passed else COLORS["pale_coral"]
        edge = COLORS["teal"] if passed else COLORS["coral"]
        card = FancyBboxPatch(
            (index - 0.42, 0.12),
            0.84,
            0.70,
            boxstyle="round,pad=0.02,rounding_size=0.06",
            linewidth=1.25,
            edgecolor=edge,
            facecolor=fill,
        )
        cases_axis.add_patch(card)
        cases_axis.text(
            index,
            0.62,
            row["case_id"],
            ha="center",
            va="center",
            fontsize=7.4,
            fontweight="bold",
            color=COLORS["ink"],
        )
        cases_axis.text(
            index,
            0.34,
            row["recorded_status"].upper(),
            ha="center",
            va="center",
            fontsize=6.4,
            fontweight="semibold",
            color=edge,
        )
        intended = "EQ" if row["intended_class"] == "Equivalent" else "NEQ"
        cases_axis.text(
            index,
            0.93,
            intended,
            ha="center",
            va="bottom",
            fontsize=5.8,
            color=COLORS["slate"],
        )

    cases_axis.set_xlim(-0.55, len(rows) - 0.45)
    cases_axis.set_ylim(-0.28, 1.12)
    cases_axis.set_xticks(
        range(len(rows)), [wrap_label(row["scenario"]) for row in rows]
    )
    cases_axis.tick_params(axis="x", labelsize=5.7, length=0, pad=2)
    cases_axis.set_yticks([])
    cases_axis.set_title("a  The historical table, row by row", loc="left", pad=4)
    for spine in cases_axis.spines.values():
        spine.set_visible(False)

    # Panel b: show the source's internal arithmetic contradiction directly.
    table_rate = summary["ledger"]["recorded_pass_rate"]
    text_claim = summary["historical_text_claim"]["claimed_accuracy"]
    claim_axis.hlines(0, table_rate, text_claim, color=COLORS["line"], linewidth=3)
    claim_axis.scatter(
        [table_rate, text_claim],
        [0, 0],
        s=[70, 70],
        color=[COLORS["navy"], COLORS["coral"]],
        zorder=3,
    )
    claim_axis.text(
        table_rate,
        0.12,
        "TABLE\n80%",
        ha="center",
        va="bottom",
        fontsize=6.8,
        color=COLORS["navy"],
        fontweight="bold",
    )
    claim_axis.text(
        text_claim,
        0.12,
        "PROSE\n100%",
        ha="center",
        va="bottom",
        fontsize=6.8,
        color=COLORS["coral"],
        fontweight="bold",
    )
    claim_axis.text(
        (table_rate + text_claim) / 2,
        -0.13,
        "+20 pp",
        ha="center",
        va="top",
        fontsize=7.0,
        color=COLORS["ink"],
        fontweight="semibold",
    )
    claim_axis.set_xlim(0.72, 1.08)
    claim_axis.set_ylim(-0.30, 0.45)
    claim_axis.set_xticks([0.8, 0.9, 1.0], ["80%", "90%", "100%"])
    claim_axis.tick_params(axis="x", labelsize=6, length=0)
    claim_axis.set_yticks([])
    claim_axis.set_title("b  Internal consistency", loc="left", pad=4)
    for spine in claim_axis.spines.values():
        spine.set_visible(False)

    # Panel c: the missing controls are part of the result, not a footnote.
    inventory = config["evidence_inventory"]
    for index, item in enumerate(inventory):
        y = len(inventory) - 1 - index
        present = item["status"] == "present"
        fill = COLORS["pale_blue"] if present else COLORS["pale_gray"]
        edge = COLORS["navy"] if present else COLORS["line"]
        inventory_axis.add_patch(
            Rectangle(
                (0.01, y - 0.34),
                0.98,
                0.68,
                facecolor=fill,
                edgecolor=edge,
                linewidth=0.8,
            )
        )
        status_color = COLORS["navy"] if present else COLORS["coral"]
        inventory_axis.text(
            0.035,
            y,
            "PRESENT" if present else "MISSING",
            va="center",
            ha="left",
            fontsize=6.2,
            fontweight="bold",
            color=status_color,
        )
        inventory_axis.text(
            0.19,
            y,
            item["artifact"],
            va="center",
            ha="left",
            fontsize=7.1,
            fontweight="medium",
            color=COLORS["ink"],
        )
        inventory_axis.text(
            0.56,
            y,
            item["implication"],
            va="center",
            ha="left",
            fontsize=6.2,
            color=COLORS["slate"],
        )

    inventory_axis.set_xlim(0, 1)
    inventory_axis.set_ylim(-0.55, len(inventory) - 0.45)
    inventory_axis.set_xticks([])
    inventory_axis.set_yticks([])
    inventory_axis.set_title(
        "c  Preserved artifact inventory: arithmetic is reproducible; execution is not",
        loc="left",
        pad=5,
    )
    for spine in inventory_axis.spines.values():
        spine.set_visible(False)

    figure.subplots_adjust(left=0.03, right=0.99, top=0.94, bottom=0.08)
    pdf_path = FIGURES_DIR / "ledger_evidence_audit.pdf"
    png_path = FIGURES_DIR / "ledger_evidence_audit.png"
    figure.savefig(
        pdf_path,
        bbox_inches="tight",
        metadata={"Creator": "STRUST v1 evidence audit", "CreationDate": None, "ModDate": None},
    )
    figure.savefig(
        png_path,
        dpi=320,
        bbox_inches="tight",
        metadata={"Software": "STRUST v1 evidence audit"},
    )
    plt.close(figure)


def check_outputs(expected: dict[Path, str]) -> None:
    problems: list[str] = []
    for path, content in expected.items():
        if not path.is_file():
            problems.append(f"missing {path.relative_to(HERE)}")
        elif path.read_text(encoding="utf-8") != content:
            problems.append(f"stale {path.relative_to(HERE)}")

    figure_pdf = FIGURES_DIR / "ledger_evidence_audit.pdf"
    figure_png = FIGURES_DIR / "ledger_evidence_audit.png"
    if not figure_pdf.is_file() or not figure_pdf.read_bytes().startswith(b"%PDF"):
        problems.append("missing or invalid figures/ledger_evidence_audit.pdf")
    if not figure_png.is_file() or not figure_png.read_bytes().startswith(b"\x89PNG"):
        problems.append("missing or invalid figures/ledger_evidence_audit.png")
    if problems:
        raise AssertionError("output check failed: " + "; ".join(problems))


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument(
        "--check",
        action="store_true",
        help="verify source hash and generated outputs without writing",
    )
    args = parser.parse_args()

    config = load_config()
    source_rows = load_ledger(config)
    case_rows = derive_case_rows(source_rows)
    summary = build_summary(config, case_rows)
    claim_bundle = build_claim_evidence(config)
    summary["claim_evidence"] = claim_bundle["summary"]
    outputs = expected_text_outputs(config, case_rows, summary)

    if args.check:
        check_outputs(outputs)
        check_claim_evidence_outputs(claim_bundle)
    else:
        write_text_outputs(outputs)
        plot_audit(config, case_rows, summary)
        write_claim_evidence_outputs(claim_bundle)
    print(json.dumps(summary, indent=2, sort_keys=True))


if __name__ == "__main__":
    main()
