"""Frozen published-input case definitions and independently implemented source checks."""
import json
import math
import re
from pathlib import Path
from xml.etree import ElementTree as ET
from . import __version__
from .analysis import InputError, compare_table, digest, number, read_csv, summarize

ROOT = Path(__file__).resolve().parent.parent
DATA = ROOT / "data"


def text(element):
    return " ".join("".join(element.itertext()).split())


def source_numbers(element):
    return [float(v) for v in re.findall(r"(?<![\w.])\d+(?:\.\d+)?", text(element))]


def source_check():
    """Re-read publisher JATS independently of the CSV extraction implementation."""
    source = ET.parse(DATA / "sources/PMC6539611.xml")
    table = source.find(".//table-wrap[@id='molecules-24-01794-t001']/table/tbody")
    rows = read_csv(DATA / "lauberte2019/table1.csv")
    if table is None or len(rows) != 19 or len(table) != 19:
        raise InputError("Lauberte source/CSV does not have the expected 19 model-compound rows.")
    for original, row in zip(table, rows):
        cells = list(original)
        identity = text(cells[0])
        if row["compound_id"] != identity or row["compound"] != text(cells[1]):
            raise InputError(f"Lauberte compound identity/name mismatch at row {identity}.")
        bde = source_numbers(cells[2])
        checks = {"bde_first_kcal_mol": bde[0], "bde_last_kcal_mol": bde[-1]}
        for cell, key, ci in ((cells[3], "dpph_rdi", "dpph_ci95_half_width"), (cells[5], "abts_rdi", "abts_ci95_half_width")):
            values = source_numbers(cell)
            checks[key] = values[0] if values else None
            checks[ci] = values[1] if len(values) > 1 else None
        for key, expected in checks.items():
            if number(row[key], key) != expected:
                raise InputError(f"Lauberte source transcription mismatch: compound {identity}, {key}.")
    source2 = ET.parse(DATA / "sources/PMC9331396.xml")
    table2 = source2.find(".//table-wrap[@id='polymers-14-03000-t002']/table")
    temperatures = [text(e) for e in table2.findall("./thead/tr")[1]]
    data_rows = table2.findall("./tbody/tr")
    thermal = read_csv(DATA / "kazachenko2022/table2.csv")
    if len(thermal) != 13 or len(temperatures) != 13:
        raise InputError("Thermal table does not have 13 temperatures.")
    for i, row in enumerate(thermal):
        expected = {"temperature_c": float(temperatures[i]),
                    "initial_mass_loss_pct": float(text(list(data_rows[0])[i + 1])),
                    "sulfated_mass_loss_pct": float(text(list(data_rows[1])[i + 1]))}
        for key, value in expected.items():
            if number(row[key], key) != value:
                raise InputError(f"Kazachenko source transcription mismatch: temperature {temperatures[i]}, {key}.")
    paragraphs = [text(e) for e in source2.findall(".//p")]
    claim800 = next(c for c in json.loads((DATA / "kazachenko2022/claims.json").read_text(encoding="utf-8"))["claims"] if c["id"] == "thermal_800_prose_values")
    paragraph800 = next((p for p in paragraphs if p.startswith("The final stage of passive pyrolysis")), "")
    match800 = re.search(r"was ([\d.]+) and ([\d.]+)% for the initial and sulfated lignin, respectively, by ([\d.]+) °C", paragraph800)
    if not match800 or [float(v) for v in match800.groups()] != [claim800["values"]["initial_mass_loss_pct"], claim800["values"]["sulfated_mass_loss_pct"], claim800["values"]["temperature_c"]]:
        raise InputError("Thermal prose assignment no longer matches its source paragraph.")
    abts_paragraph = next((text(e) for e in source.findall(".//p") if text(e).startswith("Surprisingly, both 5 and 6")), "")
    abts_match = re.search(r"RDI values ([\d.]+) and ([\d.]+) for 5 and 6", abts_paragraph)
    if not abts_match or [float(v) for v in abts_match.groups()] != [1.45, 1.44]:
        raise InputError("ABTS prose values no longer match the checked source paragraph.")
    return {"status": "pass", "lauberte_model_rows": 19, "thermal_temperature_rows": 13,
            "thermal_mass_loss_values": 26, "explicit_prose_value_checks": ["thermal_800_assignments", "abts_compounds_5_6"],
            "method": "Independent parser compares identities, BDE alternatives, assay means/intervals and thermal values with bundled publisher JATS. This is a transcription check, not independent human review."}


def reproduce_cases():
    checked = source_check()
    rows = read_csv(DATA / "lauberte2019/table1.csv")
    for row in rows:
        oh = number(row["phenolic_oh_count"], "phenolic_oh_count")
        if oh is None or oh <= 0:
            raise InputError("Phenolic OH count must be positive.")
        for assay in ("dpph", "abts"):
            value = number(row[f"{assay}_rdi"], f"{assay}_rdi")
            row[f"{assay}_per_phenolic_oh"] = "" if value is None else str(value / oh)
    all_rows = rows
    monomers = [r for r in rows if int(r["compound_id"]) <= 14]
    exclusions = [r for r in rows if r["compound_id"] not in {"5", "16"}]
    definitions = [
        ("dpph_neutral_all", "DPPH: neutral forms, all available", all_rows, "bde_first_kcal_mol", "dpph_rdi"),
        ("dpph_author_exclusions", "DPPH: neutral, omit author-named 5 and 16", exclusions, "bde_first_kcal_mol", "dpph_rdi"),
        ("dpph_neutral_monomers", "DPPH: neutral, monomers 1–14", monomers, "bde_first_kcal_mol", "dpph_rdi"),
        ("dpph_per_phenolic_oh", "DPPH: neutral, RDI per phenolic OH (reanalysis)", all_rows, "bde_first_kcal_mol", "dpph_per_phenolic_oh"),
        ("dpph_carboxylate_diagnostic", "DPPH: last-listed BDE, chemical-mismatch sensitivity only", all_rows, "bde_last_kcal_mol", "dpph_rdi"),
        ("abts_neutral_diagnostic", "ABTS: neutral BDE, chemical-mismatch sensitivity only", all_rows, "bde_first_kcal_mol", "abts_rdi"),
        ("abts_carboxylate_all", "ABTS: carboxylate-matched BDE, all available", all_rows, "bde_last_kcal_mol", "abts_rdi"),
        ("abts_carboxylate_author_exclusions", "ABTS: matched forms, omit author-named 5 and 16", exclusions, "bde_last_kcal_mol", "abts_rdi"),
        ("abts_carboxylate_monomers", "ABTS: matched forms, monomers 1–14", monomers, "bde_last_kcal_mol", "abts_rdi"),
        ("abts_per_phenolic_oh", "ABTS: matched forms, RDI per phenolic OH (reanalysis)", all_rows, "bde_last_kcal_mol", "abts_per_phenolic_oh"),
    ]
    analyses = {}
    for identity, label, selected, x, y in definitions:
        result = summarize(selected, x, y)
        result["label"] = label
        result["selected_ids_before_missingness"] = [r["compound_id"] for r in selected]
        analyses[identity] = result
    alternative_rows = [dict(r) for r in all_rows]
    replacements = {"5": 1.45, "6": 1.44}
    for row in alternative_rows:
        if row["compound_id"] in replacements:
            row["abts_rdi"] = str(replacements[row["compound_id"]])
    posthoc = summarize(alternative_rows, "bde_last_kcal_mol", "abts_rdi")
    posthoc["label"] = "Post-hoc source sensitivity: ABTS prose values for 5/6 only"
    posthoc["selection_status"] = "Added after discovering the source conflict; not one of the ten predefined variants."
    posthoc["replacements"] = [{"id": i, "table_rdi": next(float(r["abts_rdi"]) for r in all_rows if r["compound_id"] == i), "prose_rdi": v} for i, v in replacements.items()]
    posthoc["interpretation"] = "Both source representations are retained. This does not decide experimental truth or reconstruct the authors' undocumented regression."
    thermal = read_csv(DATA / "kazachenko2022/table2.csv")
    cols = ("temperature_c", "initial_mass_loss_pct", "sulfated_mass_loss_pct")
    full = compare_table(thermal, *cols)
    from250 = compare_table(thermal, *cols, minimum=250)
    at800 = next(r for r in full["rows"] if r["index"] == 800)
    prose800 = next(c for c in json.loads((DATA / "kazachenko2022/claims.json").read_text(encoding="utf-8"))["claims"] if c["id"] == "thermal_800_prose_values")["values"]
    return {
        "schema_version": 1, "software_version": __version__, "task": "published_lignin_reanalysis",
        "source_verification": checked,
        "input_sha256": {str(p.relative_to(ROOT)): digest(p) for p in sorted(DATA.rglob("*")) if p.is_file()},
        "lauberte2019": {"doi": "10.3390/molecules24091794", "source_table": "Table 1",
                         "analyses": analyses, "posthoc_source_sensitivity": posthoc, "input_data": rows},
        "kazachenko2022": {"doi": "10.3390/polym14153000", "source_table": "Table 2",
                           "full_table": full, "prose_interval_from250": from250,
                           "at800": {"table_initial_pct": at800["left"], "table_sulfated_pct": at800["right"],
                                     "prose_initial_pct": prose800["initial_mass_loss_pct"], "prose_sulfated_pct": prose800["sulfated_mass_loss_pct"],
                                     "verdict": "The prose reverses the Table 2 sample assignments at 800 °C; raw experimental truth is not determined by this check."}},
        "limits": ["Retrospectively selected cases; no estimate of general error-detection accuracy.",
                   "Published rounded means, not raw experimental replicates or a reproduction of DFT calculations.",
                   "LOO is an internal diagnostic on small chemically related samples, not external validation.",
                   "Agent-assisted extraction and separate computational verification, not independent human peer review."]}


def compare_nested(actual, expected, path="result"):
    if isinstance(expected, dict):
        if not isinstance(actual, dict) or actual.keys() != expected.keys():
            raise InputError(f"Frozen result keys differ: {path}")
        for key, value in expected.items():
            compare_nested(actual[key], value, f"{path}.{key}")
    elif isinstance(expected, list):
        if not isinstance(actual, list) or len(actual) != len(expected):
            raise InputError(f"Frozen result list differs: {path}")
        for i, value in enumerate(expected):
            compare_nested(actual[i], value, f"{path}[{i}]")
    elif isinstance(expected, (int, float)) and not isinstance(expected, bool):
        if not isinstance(actual, (int, float)) or not math.isclose(actual, expected, rel_tol=1e-10, abs_tol=1e-10):
            raise InputError(f"Frozen numeric result differs: {path}")
    elif actual != expected:
        raise InputError(f"Frozen result differs: {path}")


def verify_release():
    manifest = json.loads((ROOT / "manifest.json").read_text(encoding="utf-8"))
    for name, expected in manifest["sha256"].items():
        path = (ROOT / name).resolve()
        if not path.is_relative_to(ROOT) or digest(path) != expected:
            raise InputError(f"Packaged file hash mismatch: {name}")
    result = reproduce_cases()
    expected = json.loads((ROOT / "reference/results.json").read_text(encoding="utf-8"))
    compare_nested(result, expected)
    return {"status": "pass", "software_version": __version__, "file_hashes_checked": len(manifest["sha256"]),
            "source_verification": result["source_verification"], "frozen_results_match": True,
            "numeric_tolerance": {"relative": 1e-10, "absolute": 1e-10}, "network_required": False}
