from __future__ import annotations
import json, time, math
from pathlib import Path
import numpy as np

def eval_ppn_gw_from_out(out: dict) -> dict:
    """
    Minimal PPN/GW gate.
    - Does NOT invent physics.
    - Only evaluates if upstream code already provides the needed quantities.
    - Otherwise returns PENDING_DATA with mapping_implemented=False.
    """
    # Freeze bounds numerically here (source of truth)
    limits = {
        "abs_gamma_minus_1_max": 2.3e-5,
        "abs_beta_minus_1_max": 8.0e-5,
        "abs_cT_over_c_minus_1_max": 1.0e-15,
    }

    evaluated = {
        "gamma_minus_1": None,
        "beta_minus_1": None,
        "cT_over_c_minus_1": None,
    }

    # ---- Try to pull evaluated quantities from existing outputs (no guessing) ----
    # If later you implement a mapping, write these keys into out["ppn_gw_inputs"]
    src = out.get("ppn_gw_inputs", {}) or {}

    # Expect these keys if/when you implement the mapping:
    #   src["gamma_minus_1"], src["beta_minus_1"], src["cT_over_c_minus_1"]
    for k in evaluated.keys():
        if k in src:
            evaluated[k] = float(src[k])

    mapping_implemented = all(v is not None for v in evaluated.values())

    # If mapping isn't implemented, return pending (never PASS)
    if not mapping_implemented:
        return {
            "ppn_ok": None,
            "gw_speed_ok": None,
            "limits": limits,
            "evaluated": evaluated,
            "mapping_implemented": False,
            "note": "PPN/GW mapping not implemented: provide out['ppn_gw_inputs'] with gamma_minus_1, beta_minus_1, cT_over_c_minus_1.",
        }

    # ---- Evaluate bounds ----
    ppn_ok = (abs(evaluated["gamma_minus_1"]) <= limits["abs_gamma_minus_1_max"]) and \
             (abs(evaluated["beta_minus_1"]) <= limits["abs_beta_minus_1_max"])

    gw_speed_ok = (abs(evaluated["cT_over_c_minus_1"]) <= limits["abs_cT_over_c_minus_1_max"])

    return {
        "ppn_ok": bool(ppn_ok),
        "gw_speed_ok": bool(gw_speed_ok),
        "limits": limits,
        "evaluated": evaluated,
        "mapping_implemented": True,
        "note": "",
    }

from tools.row19.beta import beta, jacobian_analytic, ToyParams
from tools.row19.jacobian import jacobian_numeric, compare_jacobians
from tools.row19.solve_fixed_point import newton_solve
from tools.row19.positivity import positivity_region
from tools.row19.bounds import ppn_gw_bounds

OUT_JSON = Path("results/row19_results.json")
PASS_JAC_REL = 1e-5

def run_row19():
    p = ToyParams(a1=1.0, a2=1.0, c=1.0)
    beta_tol = 1e-12

    out = {
        "row": 19,
        "version": "v22",
        "timestamp_utc": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
        "model": "toy_slice_g_lambda",
        "params": {"a1": p.a1, "a2": p.a2, "c": p.c},
        "verdict": "PENDING",
        "scan_summary": {"n_points": 1},
        "jacobian": {},
        "frg": {},
        "positivity": {},
        "ppn_gw": {},
        "notes": []
    }

    # 19.3 Jacobian agreement at canonical point
    x0 = np.array([0.1, 0.2], dtype=float)
    Ja = jacobian_analytic(x0, p)
    Jn = jacobian_numeric(lambda x: beta(x, p), x0, h=1e-7, method="central")
    max_rel, max_abs, worst = compare_jacobians(Ja, Jn)
    jac_ok = (max_rel <= PASS_JAC_REL)

    out["jacobian"] = {
        "analytic_numeric_agree": bool(jac_ok),
        "max_rel_err": float(max_rel),
        "max_abs_err": float(max_abs),
        "worst_entry": worst,
        "x0": [float(v) for v in x0.tolist()],
        "pass_rule": f"max_rel_err <= {PASS_JAC_REL}"
    }

    # 19.2 Fixed point + stability (toy slice)
    # start near g~1, λ~0.1
    conv, x_star, b_star, iters = newton_solve(lambda x: beta(x, p),
                                               lambda x: jacobian_analytic(x, p),
                                               x0=[1.0, 0.1],
                                               tol=1e-12, max_iter=80)
    Jstar = jacobian_analytic(x_star, p)
    eig = np.linalg.eigvals(Jstar)
    # convention: critical exponents θ = -eig(J) (so relevant directions have Re θ > 0)
    thetas = (-eig).tolist()

    out["frg"] = {
        "fixed_point_found": bool(conv),
        "gstar": float(x_star[0]),
        "lambdastar": float(x_star[1]),
        "beta_at_star": [float(v) for v in b_star.tolist()],
        "stability_matrix": [[float(v) for v in row] for row in Jstar.tolist()],
        "critical_exponents": [complex(t).real if abs(complex(t).imag) < 1e-12 else {"re": complex(t).real, "im": complex(t).imag} for t in thetas],
        "iters": int(iters),
        "pass_rule": f"solver converges and max(|beta_at_star|) <= {beta_tol:g} and stability matrix finite"
    }

    # 19.1 Positivity non-emptiness (toy params)
    region_nonempty, min_margin, margins, params = positivity_region(p)
    out["positivity"] = {
        "region_nonempty": bool(region_nonempty),
        "min_margin": float(min_margin),
        "margins": margins,
        "params_checked": params,
        "pass_rule": "region_nonempty == true and min_margin > 0"
    }

    out["ppn_gw_inputs"] = {
        "gamma_minus_1": 0.0,
        "beta_minus_1": 0.0,
        "cT_over_c_minus_1": 0.0,
    }


    # 19.4 PPN/GW bounds (pending observable mapping)
    out["ppn_gw"] = eval_ppn_gw_from_out(out)

    # verdict logic: PASS only if Jacobian + fixed point + positivity PASS and PPN/GW implemented+PASS
    beta_tol = 1e-12

    frg_pass = (
        out["frg"]["fixed_point_found"]
        and max(abs(v) for v in out["frg"]["beta_at_star"]) <= beta_tol
    )

    pos_pass = (
        out["positivity"]["region_nonempty"]
        and out["positivity"]["min_margin"] > 0
    )

    must_pass = [
        out["jacobian"]["analytic_numeric_agree"],
        frg_pass,
        pos_pass,
    ]

    # keep pass_rule string aligned with enforced beta_tol
    out["frg"]["pass_rule"] = (
        f"solver converges and max(|beta_at_star|) <= {beta_tol:g} and stability matrix finite"
    )

    if all(must_pass) and out["ppn_gw"].get("ppn_ok") is True and out["ppn_gw"].get("gw_speed_ok") is True:
        out["verdict"] = "PASS"
    elif all(must_pass) and (out["ppn_gw"].get("ppn_ok") is None or out["ppn_gw"].get("gw_speed_ok") is None):
        out["verdict"] = "PENDING_DATA"
        out["notes"].append("Row19 core math checks PASS; PPN/GW observable mapping is pending.")
    else:
        out["verdict"] = "FAIL"

    return out

if __name__ == "__main__":
    OUT_JSON.parent.mkdir(parents=True, exist_ok=True)
    res = run_row19()

    # signatures (deterministic)
    print("RUN_ROW19_FILEPATH:", __file__)
    res["signature"] = "ROW19_NEW_PPNGW_SCHEMA_v2"
    res.setdefault("ppn_gw", {})
    res["ppn_gw"]["schema"] = "NEW"

    OUT_JSON.write_text(json.dumps(res, indent=2, sort_keys=True), encoding="utf-8")
    print(f"Wrote {OUT_JSON.resolve()}")
