# OID_analysis/analysis/phase7_whitebox.py
"""
Phase 7: White-box control chain comparison.

Compares how well different state representations predict the action:
  Model A: obs (raw sensor) -> act
  Model B: POD coord -> act
  Model C: OID coord -> act
  Model D: OID coord + force -> act

Usage:
    python3 src/OID_analysis/analysis/phase7_whitebox.py
"""
from __future__ import annotations

import argparse
import json
import os
import sys

import numpy as np

_REPO = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", ".."))
if _REPO not in sys.path:
    sys.path.insert(0, _REPO)

from OID_analysis.configs import DATA_DIR  # noqa: E402
from OID_analysis.utils.analysis import standardize  # noqa: E402

try:
    from sklearn.linear_model import LinearRegression
    from sklearn.metrics import r2_score
    HAS_SKLEARN = True
except ImportError:
    HAS_SKLEARN = False

SCENES = ["steady_cloak", "karman_re100",
          "illusion_0.75L", "illusion_1.0L", "illusion_1.5L"]


def run_whitebox(scene_key: str):
    print(f"\n--- Phase 7: White-box for {scene_key} ---")

    # Check for controlled.npz (PPO scenes) or forces (open-loop scenes)
    data_dir_base = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
                                "data")

    if scene_key == "steady_cloak":
        dd = os.path.join(data_dir_base, "steady_cloak", "steady_cloak")
    else:
        sid = {"karman_re100": "karman_re100"}.get(scene_key, scene_key)
        dd = os.path.join(data_dir_base, scene_key.replace("steady_", ""),
                          scene_key) if "illusion" in scene_key else \
             os.path.join(data_dir_base, "karman_cloak", scene_key)

    controlled_fp = os.path.join(dd, "controlled.npz")
    forces_fp = os.path.join(dd, "forces.npz")

    if os.path.isfile(controlled_fp):
        data = np.load(controlled_fp)
        actions = data["actions"]
        sensors = data["sensors"]
    elif os.path.isfile(forces_fp):
        # Open-loop steady cloak: no actions available from controlled.npz
        # But we know the steady cloak action: [0, -5.1*U0, 5.1*U0]
        from OID_analysis.configs import get_scene
        cfg = get_scene(scene_key)
        u0 = cfg["u0"]
        sensors_n = np.load(os.path.join(dd, "sensors.npz"))["sensors"]
        N = len(sensors_n)
        sensors = sensors_n
        omega_rear = cfg.get("omega_rear_scale", 5.1)
        actions = np.tile([0.0, -omega_rear * u0 / 0.01, omega_rear * u0 / 0.01], (N, 1))
        # Actually these should be in normalized [-1,1] range
        # rear = 5.1 -> normalized = (5.1 - bias)/scale where bias=5.1, scale=8
        # Actually for steady: bias=[0,-5.1,5.1], scale=8
        # So action = (omega/u0 - bias)/scale
        actions = np.tile([0.0, 0.0, 0.0], (N, 1))  # zero action = bias actions
    else:
        print(f"  SKIPPED: no action data found")
        return

    N = min(len(sensors), len(actions))
    sensors = sensors[:N]
    actions = actions[:N]

    # Normalize actions per channel
    actions_std, act_mean, act_std = standardize(actions)

    # POD coefs
    pod_dir = os.path.join(DATA_DIR, "derived", "pod", scene_key)
    coefs = None
    pod_fp = os.path.join(pod_dir, "pod_coefs_r10.npy.npz")
    if os.path.isfile(pod_fp):
        pod_npz = np.load(pod_fp, allow_pickle=True)
        pod_n = pod_npz["coefs"].shape[0]
        N = min(N, pod_n)
    
    # Re-apply truncation based on final N
    sensors = sensors[:N]
    actions = actions[:N]
    actions_std, act_mean, act_std = standardize(actions)

    # OID coords
    oid_dir = os.path.join(DATA_DIR, "derived", "oid")
    oid_coords = None
    oid_fp = os.path.join(oid_dir, "force", scene_key, "force_oid.npz")
    if os.path.isfile(oid_fp):
        oid_coords = np.load(oid_fp)["z"][:N]

    # Force observable
    obs_dir = os.path.join(DATA_DIR, "derived", "observables", scene_key)
    force_obs = None
    force_fp = os.path.join(obs_dir, "force_total.npz")
    if os.path.isfile(force_fp):
        force_obs = np.load(force_fp)["standardized"][:N]

    if not HAS_SKLEARN:
        print("  sklearn not available")
        return

    split = int(N * 0.7)

    # Model A: raw sensor -> act
    X_A = sensors[:split]
    Y_train = actions_std[:split]
    # Test on last segment
    X_A_test = sensors[split:N]

    results = {}

    # Model A
    if X_A.shape[1] > 0:
        reg = LinearRegression().fit(X_A, Y_train)
        r2_a = r2_score(Y_train, reg.predict(X_A))
        results["obs_act_train"] = float(r2_a)

    # Model B: POD coord -> act
    if coefs is not None:
        for m in [3, 5]:
            X = standardize(coefs[:split, :m])[0]
            reg = LinearRegression().fit(X, Y_train)
            r2 = r2_score(Y_train, reg.predict(X))
            results[f"pod_m{m}_act_train"] = float(r2)

    # Model C: OID coord -> act
    if oid_coords is not None:
        for m in [3, 5]:
            X = oid_coords[:split, :m]
            reg = LinearRegression().fit(X, Y_train)
            r2 = r2_score(Y_train, reg.predict(X))
            results[f"oid_m{m}_act_train"] = float(r2)

    # Model D: OID + force -> act
    if oid_coords is not None and force_obs is not None:
        X = np.hstack([oid_coords[:split, :3], force_obs[:split, :2]])
        reg = LinearRegression().fit(X, Y_train)
        r2 = r2_score(Y_train, reg.predict(X))
        results["oid_force_act_train"] = float(r2)

    print(f"  Results: {json.dumps(results, indent=2)}")

    # Save
    out_dir = os.path.join(DATA_DIR, "derived", "whitebox")
    os.makedirs(out_dir, exist_ok=True)
    with open(os.path.join(out_dir, f"{scene_key}.json"), "w") as f:
        json.dump(results, f, indent=2)


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--scene", type=str, default=None)
    args = ap.parse_args()

    targets = [args.scene] if args.scene and args.scene in SCENES else SCENES

    for sn in targets:
        run_whitebox(sn)

    print("\nPhase 7 complete.")


if __name__ == "__main__":
    main()
