"""CCD analysis pipeline: POD + force/action/signature CCD.

Usage:
    python ccd/run_ccd.py

Requires resampled data from scripts/resample.py.
"""
from __future__ import annotations

import json
import os
import sys

import numpy as np

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

from CCD_analysis.configs import DATA_DIR
from CCD_analysis.utils.resampling import (
    compute_pod, cumulative_energy, e95_index,
    compute_reduced_ccd, stack_velocity_fields,
)

R_CANDIDATES = [6, 8, 10]
CCD_Q = 12


def load_resampled(name: str):
    p = os.path.join(DATA_DIR, "resampled", name, "resampled.npz")
    if not os.path.isfile(p):
        return None
    return np.load(p)


def main():
    print("=== CCD Pipeline ===\n")

    # Identify which cases have resampled data
    resampled_dir = os.path.join(DATA_DIR, "resampled")
    if not os.path.isdir(resampled_dir):
        print("ERROR: run scripts/resample.py first")
        return 1

    cases = sorted(os.listdir(resampled_dir))
    print(f"Resampled cases: {cases}")

    # --- POD ---
    print("\n--- POD ---")
    snapshots = []
    case_ranges = {}
    idx = 0

    for name in cases:
        d = load_resampled(name)
        if d is None:
            continue
        ux, uy = d.get("ux"), d.get("uy")
        if ux is None:
            print(f"  {name}: no field data, skip POD")
            continue
        n_cyc, n_pt = ux.shape[0], ux.shape[1]
        for c in range(n_cyc):
            for p in range(n_pt):
                q = np.concatenate([ux[c, p].ravel(), uy[c, p].ravel()])
                snapshots.append(q)
        case_ranges[name] = (idx, idx + n_cyc * n_pt)
        idx += n_cyc * n_pt
        print(f"  {name}: {n_cyc}x{n_pt} snapshots")

    if not snapshots:
        print("No field data for POD")
        return 1

    Q = np.column_stack(snapshots)
    mean_field, modes, s, coeffs = compute_pod(Q)
    energy = cumulative_energy(s)
    e95 = e95_index(energy)
    print(f"  POD: {len(s)} modes, E95={e95}")
    for i in range(min(6, len(s))):
        print(f"    mode {i+1}: energy={energy[i]:.4f}")

    # --- CCD for each case ---
    print("\n--- CCD ---")
    all_results = {}
    W_dict = {}

    for r in R_CANDIDATES:
        print(f"\n  POD truncation r={r}")
        for name in cases:
            d = load_resampled(name)
            if d is None:
                continue

            # POD coefficients for this case
            if name in case_ranges:
                start, end = case_ranges[name]
                a_r = coeffs[:r, start:end]
            else:
                # Projection case (not in POD basis)
                ux, uy = d.get("ux"), d.get("uy")
                if ux is None:
                    continue
                proj_snapshots = []
                for c in range(ux.shape[0]):
                    for p in range(ux.shape[1]):
                        q = np.concatenate([ux[c, p].ravel(), uy[c, p].ravel()])
                        proj_snapshots.append(q)
                Q_proj = np.column_stack(proj_snapshots)
                Q_centered = Q_proj - mean_field[:, None]
                a_r = (modes[:, :r].T @ Q_centered)

            N = a_r.shape[1]
            if N < 24:
                print(f"  {name}: too few samples ({N})")
                continue

            # Force CCD
            forces = d.get("forces")
            if forces is not None:
                f = forces.reshape(-1, forces.shape[-1])
                Fx = f[:, 0] + f[:, 2] + f[:, 4]
                Fy = f[:, 1] + f[:, 3] + f[:, 5]
                y_force = np.vstack([Fx, Fy])

                if y_force.shape[1] >= N:
                    y_f = y_force[:, :N]
                else:
                    y_f = y_force

                W, sigma, z = compute_reduced_ccd(a_r[:, :y_f.shape[1]], y_f, Q_delay=CCD_Q)
                ccd_ene = cumulative_energy(sigma)
                m80 = int(np.searchsorted(ccd_ene, 0.80) + 1) if len(ccd_ene) > 0 else 0
                key = f"{name}_force_r{r}"
                W_dict[key] = W
                all_results[key] = {"case": name, "observable": "force", "r": r,
                                    "m80": m80, "sigma_top3": [float(sigma[i]) for i in range(min(3, len(sigma)))]}
                print(f"    {key}: m80={m80}")

            # Action CCD (for controlled cases)
            actions = d.get("actions")
            if actions is not None:
                y_act = actions.reshape(-1, actions.shape[-1]).T
                if y_act.shape[1] >= N:
                    y_a = y_act[:, :N]
                else:
                    y_a = y_act
                W, sigma, z = compute_reduced_ccd(a_r[:, :y_a.shape[1]], y_a, Q_delay=CCD_Q)
                ccd_ene = cumulative_energy(sigma)
                m80 = int(np.searchsorted(ccd_ene, 0.80) + 1) if len(ccd_ene) > 0 else 0
                key = f"{name}_action_r{r}"
                W_dict[key] = W
                all_results[key] = {"case": name, "observable": "action", "r": r,
                                    "m80": m80, "sigma_top3": [float(sigma[i]) for i in range(min(3, len(sigma)))]}
                print(f"    {key}: m80={m80}")

    # --- Modal overlap ---
    print("\n--- Modal Overlap ---")
    force_keys = [k for k in W_dict if "force" in k]
    for i, ka in enumerate(force_keys):
        for kb in force_keys[i+1:]:
            Wa, Wb = W_dict[ka], W_dict[kb]
            n = min(Wa.shape[1], Wb.shape[1], 5)
            ov = []
            for k in range(n):
                ak = Wa[:, k] / (np.linalg.norm(Wa[:, k]) + 1e-12)
                bk = Wb[:, k] / (np.linalg.norm(Wb[:, k]) + 1e-12)
                ov.append(float(abs(ak @ bk)))
            print(f"    O({ka}, {kb}): O1={ov[0]:.4f}, O2={ov[1]:.4f}")

    # Save
    out_dir = os.path.join(DATA_DIR, "ccd")
    os.makedirs(out_dir, exist_ok=True)
    with open(os.path.join(out_dir, "ccd_results.json"), "w") as f:
        json.dump(all_results, f, indent=2)
    print(f"\nSaved to {out_dir}/ccd_results.json")
    return 0


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