import os
import json
import datetime
import math
import pandas as pd

MSUN_SEC = 4.92549095e-6  # G M_sun / c^3 in seconds


def kerr_qnm_220(mass_final_msun: float, spin_final: float) -> tuple[float, float]:
    """
    Fundamental Kerr QNM fit for (l,m,n)=(2,2,0).

    Returns:
        f_qnm_Hz, tau_ms
    """
    a = max(0.0, min(float(spin_final), 0.999999))
    M_sec = float(mass_final_msun) * MSUN_SEC

    # Standard fit coefficients for the 220 Kerr mode
    f1, f2, f3 = 1.5251, -1.1568, 0.1292
    q1, q2, q3 = 0.7000, 1.4187, -0.4990

    omega_M = f1 + f2 * (1.0 - a) ** f3
    Q = q1 + q2 * (1.0 - a) ** q3

    f_qnm_Hz = omega_M / (2.0 * math.pi * M_sec)
    tau_s = Q / (math.pi * f_qnm_Hz)
    tau_ms = 1.0e3 * tau_s

    return f_qnm_Hz, tau_ms


def gate_ringdown_echoes(dataset_dir: str) -> dict:
    data_path = os.path.join(dataset_dir, "bh_ringdown_data.csv")
    repo_root = os.path.abspath(os.path.join(dataset_dir, ".."))
    row19_path = os.path.join(repo_root, "results", "row19_results.json")
    row19 = {}
    if os.path.exists(row19_path):
        with open(row19_path, "r", encoding="utf-8") as f:
            row19 = json.load(f)
    canonical_map = row19.get("gravity_scalar_gw", {}).get("canonical_tensor_map", {})
    canonical_boundary = canonical_map.get("row23_boundary", {})
    canonical_tensor = canonical_map.get("tensor_quantities", {})

    if not os.path.exists(data_path):
        return {
            "row": 23,
            "gate": "bh_ringdown_echoes",
            "context": "LIVE",
            "tier": "Diagnostic",
            "timestamp_utc": datetime.datetime.now(datetime.UTC).isoformat(timespec="seconds").replace("+00:00", "Z"),
            "verdict": "PENDING_DATA",
            "blocking": False,
            "notes": "No ringdown/echo benchmark dataset shipped in this zip. Add datasets/bh_ringdown_data.csv and re-run."
        }

    df = pd.read_csv(data_path)

    required_cols = [
        "event",
        "mass_final_msun",
        "spin_final",
        "f_qnm_obs_Hz",
        "tau_obs_ms",
    ]

    missing = [c for c in required_cols if c not in df.columns]
    if missing:
        return {
            "row": 23,
            "gate": "bh_ringdown_echoes",
            "context": "LIVE",
            "tier": "Diagnostic",
            "timestamp_utc": datetime.datetime.now(datetime.UTC).isoformat(timespec="seconds").replace("+00:00", "Z"),
            "verdict": "PENDING_DATA",
            "blocking": False,
            "notes": f"Dataset is missing required columns: {missing}"
        }

    pred_freqs = []
    pred_taus = []
    for _, row in df.iterrows():
        f_pred, tau_pred = kerr_qnm_220(row["mass_final_msun"], row["spin_final"])
        pred_freqs.append(f_pred)
        pred_taus.append(tau_pred)

    df["f_qnm_pred_Hz"] = pred_freqs
    df["tau_pred_ms"] = pred_taus

    df["freq_frac_err"] = (df["f_qnm_pred_Hz"] - df["f_qnm_obs_Hz"]).abs() / df["f_qnm_obs_Hz"].abs()
    df["tau_frac_err"] = (df["tau_pred_ms"] - df["tau_obs_ms"]).abs() / df["tau_obs_ms"].abs()

    max_freq_frac_err = float(df["freq_frac_err"].max())
    max_tau_frac_err = float(df["tau_frac_err"].max())
    n_events = int(len(df))
    
    canonical_map_pass = bool(
        canonical_tensor.get("cT_squared") == 1.0
        and canonical_tensor.get("mT_squared") == 0.0
        and canonical_boundary.get("R_NZH_amplitude") == 0.0
        and canonical_boundary.get("Upsilon") == 0.0
    )
    pass_rule = (max_freq_frac_err <= 0.15) and (max_tau_frac_err <= 0.10) and canonical_map_pass
    verdict = "PASS" if pass_rule else "FAIL"

    print(df[[
        "event",
        "mass_final_msun",
        "spin_final",
        "f_qnm_obs_Hz",
        "f_qnm_pred_Hz",
        "tau_obs_ms",
        "tau_pred_ms",
        "freq_frac_err",
        "tau_frac_err",
    ]].to_string(index=False))

    return {
        "row": 23,
        "gate": "bh_ringdown_echoes",
        "context": "LIVE",
        "tier": "Diagnostic",
        "timestamp_utc": datetime.datetime.now(datetime.UTC).isoformat(timespec="seconds").replace("+00:00", "Z"),
        "verdict": verdict,
        "status": "PASS_KERR_QNM_BENCHMARK__CANONICAL_ZERO_ECHO_AMPLITUDE_CLOSED" if verdict == "PASS" else "FAIL_KERR_QNM_OR_CANONICAL_GW_HANDOFF",
        "blocking": False,
        "n_events": n_events,
        "max_freq_frac_err": max_freq_frac_err,
        "max_tau_frac_err": max_tau_frac_err,
        "executed_scope": {
            "kerr_qnm_frequency_and_damping_benchmark": True,
            "canonical_echo_amplitude_from_row19_action": True,
            "canonical_echo_amplitude_zero": canonical_boundary.get("R_NZH_amplitude") == 0.0,
            "nonzero_echo_branch_executed": False,
            "echo_axion_back_prediction": False,
            "legacy_direct_axion_echo_scalar_equality_retired": True
        },
        "claim_boundary": {
            "row23_pass_means_echo_detected_or_required": False,
            "echo_search_prior_0p6_to_0p8_fQNM_executed_here": False,
            "near_horizon_echo_delay_examples_are_universal_predictions": False,
            "canonical_echo_channel_status": "CLOSED_ZERO_AMPLITUDE",
            "nonzero_echo_extension_status": "OPEN_NOT_CANONICAL",
            "axion_echo_cross_sector_status": "OPERATOR_PROJECTION_UNIVERSALITY__NO_DIRECT_SCALAR_EQUALITY_WITHOUT_DERIVED_INTERTWINER"
        },
        "row19_canonical_gw_handoff": {
            "present": bool(canonical_map),
            "cT_squared": canonical_tensor.get("cT_squared"),
            "mT_squared": canonical_tensor.get("mT_squared"),
            "R_NZH_amplitude": canonical_boundary.get("R_NZH_amplitude"),
            "Upsilon": canonical_boundary.get("Upsilon"),
        },
        "notes": "Kerr-QNM benchmark plus canonical Row-19 action handoff. The canonical stationary R+R^2 constant-scalar branch has zero horizon reflectivity and zero echo content. Any nonzero reflective extension remains outside the canonical branch. The legacy direct scalar axion=echo recycling equality is replaced by a common response-operator law with channel projections; Row 21 remains independent."
    }


if __name__ == "__main__":
    here = os.path.dirname(__file__)
    out_dir = os.path.abspath(os.path.join(here, "..", "results", "row23_ringdown"))
    os.makedirs(out_dir, exist_ok=True)
    res = gate_ringdown_echoes(os.path.abspath(os.path.join(here, "..", "datasets")))
    with open(os.path.join(out_dir, "result.json"), "w", encoding="utf-8") as f:
        json.dump(res, f, indent=2, sort_keys=True)
    print(res["verdict"])