"""Compare the released CLI with a separate exact-rational source-XML analysis.

Run from any working directory with the same Python interpreter. Both analyses
write only to a temporary directory; this checker imports neither algorithm.
"""
import argparse
import json
import math
import os
from pathlib import Path
import platform
import subprocess
import sys
import tempfile

ROOT = Path(__file__).resolve().parents[1]
TOLERANCE = 1e-10
PRIMARY_VARIANTS = {
    "dpph_neutral_all", "dpph_author_exclusions", "dpph_neutral_monomers",
    "dpph_per_phenolic_oh", "dpph_carboxylate_diagnostic",
    "abts_neutral_diagnostic", "abts_carboxylate_all",
    "abts_carboxylate_author_exclusions", "abts_carboxylate_monomers",
    "abts_per_phenolic_oh",
}
RUNNER = r'''
import runpy, sys
sys.dont_write_bytecode = True
attempts = []
def guard(event, arguments):
    if event.startswith("socket."):
        attempts.append(event)
        raise RuntimeError("Independent-check socket API guard: " + event)
sys.addaudithook(guard)
import socket
try:
    socket.socket()
except RuntimeError:
    pass
else:
    raise AssertionError("Socket guard self-test failed")
attempts.clear()
root, mode, target = sys.argv[1:4]
sys.path.insert(0, root)
sys.argv = [target] + sys.argv[4:]
try:
    if mode == "module":
        runpy.run_module(target, run_name="__main__")
    else:
        runpy.run_path(target, run_name="__main__")
finally:
    print("INDEPENDENT_SOCKET_ATTEMPTS=" + str(len(attempts)), file=sys.stderr)
'''


def require(condition, message):
    if not condition:
        raise ValueError(message)


def run_guarded(mode, target, arguments, cwd):
    # Do not pass API credentials or Python environment path overrides.
    env = {key: os.environ[key] for key in ("PATH", "SYSTEMROOT", "WINDIR") if key in os.environ}
    process = subprocess.run(
        [sys.executable, "-I", "-S", "-B", "-c", RUNNER, str(ROOT), mode, str(target), *map(str, arguments)],
        cwd=cwd, env=env, text=True, capture_output=True, timeout=60,
    )
    require(process.returncode == 0, f"{mode} analysis failed: {process.stderr.strip()}")
    require("INDEPENDENT_SOCKET_ATTEMPTS=0" in process.stderr, "Analysis attempted a socket API operation")


def compare(actual, oracle):
    count = 0
    maximum_difference = 0.0

    def equal(value, expected, label):
        nonlocal count, maximum_difference
        count += 1
        if isinstance(expected, float):
            require(isinstance(value, (int, float)) and math.isfinite(value), f"Nonfinite/missing number: {label}")
            maximum_difference = max(maximum_difference, abs(value - expected))
            require(math.isclose(value, expected, rel_tol=TOLERANCE, abs_tol=TOLERANCE),
                    f"Numerical mismatch at {label}: {value!r} vs {expected!r}")
        else:
            require(value == expected, f"Identity/count mismatch at {label}: {value!r} vs {expected!r}")

    analyses = dict(actual["lauberte2019"]["analyses"])
    expected_analyses = dict(oracle["lauberte2019"]["variants"])
    equal(set(analyses), PRIMARY_VARIANTS, "primary_variant_names")
    analyses["posthoc_source_sensitivity"] = actual["lauberte2019"]["posthoc_source_sensitivity"]
    expected_analyses["posthoc_source_sensitivity"] = oracle["lauberte2019"]["post_hoc_abts_prose_substitution"]["result"]
    baseline = {point["id"]: point for point in analyses["abts_carboxylate_all"]["points"]}
    alternative = analyses["posthoc_source_sensitivity"]
    changed = []
    for point in alternative["points"]:
        identity = point["id"]
        equal(point["x"], baseline[identity]["x"], f"posthoc.fixed_bde.{identity}")
        if point["y"] != baseline[identity]["y"]:
            changed.append(identity)
        equal(point["y"], {"5": 1.45, "6": 1.44}.get(identity, baseline[identity]["y"]),
              f"posthoc.response.{identity}")
    equal(changed, ["5", "6"], "posthoc.only_two_changed")
    equal(alternative["replacements"], [
        {"id": "5", "table_rdi": 0.67, "prose_rdi": 1.45},
        {"id": "6", "table_rdi": 0.96, "prose_rdi": 1.44},
    ], "posthoc.replacement_provenance")

    for identity, a in analyses.items():
        b = expected_analyses[identity]
        require(len(a["points"]) == len(a["leave_one_out"]["folds"]) == a["leave_one_out"]["total_folds"] == b["n"],
                f"Incomplete point/fold arrays: {identity}")
        for left, right in [("n", "n"), ("pearson_r", "pearson_r"), ("r_squared", "r2"),
                            ("slope", "slope"), ("intercept", "intercept"), ("spearman_rho", "spearman_rho")]:
            equal(a[left], b[right], f"{identity}.{left}")
        equal(list(map(int, a["included_ids"])), b["compound_ids"], f"{identity}.included_ids")
        for left, right in [("total_pairs", "total"), ("concordant", "consistent_with_lower_bde_higher_rdi"),
                            ("discordant", "discordant_with_lower_bde_higher_rdi"),
                            ("discordant_fraction_of_untied", "discordance_fraction_among_untied")]:
            equal(a["ranking"][left], b["rank_pairs"][right], f"{identity}.ranking.{left}")
        equal(a["ranking"]["tied"], sum(b["rank_pairs"][key] for key in ("tie_bde_only", "tie_rdi_only", "tie_both")),
              f"{identity}.ranking.ties")
        for model, prefix in [("linear", "linear"), ("training_mean_baseline", "training_mean")]:
            for metric in ("mae", "rmse"):
                equal(a["leave_one_out"][model][metric], b["loo"][prefix + "_" + metric],
                      f"{identity}.loo.{model}.{metric}")
        equal(a["leave_one_out"]["evaluated_folds"], b["n"], f"{identity}.loo_count")
        require(len(b["loo"]["folds"]) == b["n"], f"Incomplete oracle fold array: {identity}")
        for index, (af, bf) in enumerate(zip(a["leave_one_out"]["folds"], b["loo"]["folds"])):
            for left, right in [("held_out_id", "held_out_compound_id"), ("observed", "observed_rdi"),
                                ("linear_prediction", "linear_prediction"), ("mean_prediction", "training_mean_prediction")]:
                value = int(af[left]) if left == "held_out_id" else af[left]
                equal(value, bf[right], f"{identity}.fold{index}.{left}")

    thermal, expected = actual["kazachenko2022"], oracle["kazachenko2022"]
    for left, right in [("full_table", "full_table"), ("prose_interval_from250", "prose_from_250")]:
        equal(thermal[left]["n"], expected[right]["temperature_count"], f"thermal.{left}.n")
        equal(thermal[left]["agreeing"], expected[right]["supporting_count"], f"thermal.{left}.agreeing")
        equal([row["index"] for row in thermal[left]["counterexamples"]], expected[right]["counterexample_temperatures_c"],
              f"thermal.{left}.counterexamples")
    require(len(thermal["full_table"]["rows"]) == len(expected["thermal"]) == 13, "Incomplete thermal row array")
    for index, (a, b) in enumerate(zip(thermal["full_table"]["rows"], expected["thermal"])):
        for left, right in [("index", "temperature_c"), ("left", "initial_mass_loss_pct"),
                            ("right", "sulfated_mass_loss_pct"), ("right_minus_left", "sulfated_minus_initial_percentage_points")]:
            equal(a[left], b[right], f"thermal.row{index}.{left}")
    for left, right in [("table_initial_pct", "table_initial"), ("table_sulfated_pct", "table_sulfated"),
                        ("prose_initial_pct", "prose_initial"), ("prose_sulfated_pct", "prose_sulfated")]:
        equal(thermal["at800"][left], expected["prose_800_value_conflict"][right], f"at800.{left}")
    for name, digest in oracle["source_hashes"].items():
        require(actual["input_sha256"]["data/sources/" + name] == digest, f"Different source XML: {name}")
    return count, maximum_difference


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--out", type=Path, help="Optional JSON summary; analyses themselves use temporary files")
    args = parser.parse_args()
    try:
        with tempfile.TemporaryDirectory(prefix="ligninqc independent check ") as temporary:
            directory = Path(temporary)
            run_guarded("module", "ligninqc", ["reproduce", "--out", directory / "cli"], directory)
            run_guarded("script", ROOT / "tools/independent_source_analysis.py", ["--out", directory / "oracle.json"], directory)
            actual = json.loads((directory / "cli/results.json").read_text(encoding="utf-8"))
            oracle = json.loads((directory / "oracle.json").read_text(encoding="utf-8"))
            count, maximum = compare(actual, oracle)
        report = {
            "status": "pass", "python": platform.python_version(),
            "numerical_and_identity_checks": count, "additional_structure_and_source_identity_checks": "pass",
            "maximum_absolute_numeric_difference": maximum,
            "tolerance": {"relative": TOLERANCE, "absolute": TOLERANCE},
            "primary_variants": 10, "separate_posthoc_source_sensitivity": 1,
            "source_sha256": oracle["source_hashes"],
            "network_boundary": "Both subprocesses: isolated Python without site packages; self-tested socket API guard; zero socket API attempts. Not an OS firewall.",
            "scope": "Independent computational comparison on two published cases; not experimental validation, a human usability study or general detection accuracy.",
        }
        content = json.dumps(report, ensure_ascii=False, indent=2, allow_nan=False) + "\n"
        if args.out:
            args.out.write_text(content, encoding="utf-8")
        print(content, end="")
        return 0
    except (OSError, ValueError, KeyError, TypeError, subprocess.TimeoutExpired) as error:
        print(json.dumps({"status": "fail", "error": str(error)}, ensure_ascii=False), file=sys.stderr)
        return 1


if __name__ == "__main__":
    sys.exit(main())
