"""Unified feature builder for all cloak scenes.

Produces dimensionless features with consistent G-equivariant structure.
All scenes (Karman, steady, vortex, illusion) use this same builder.

Copy of analysis_cloak/common/feature_builder.py -- kept as canonical source.
"""
from __future__ import annotations

from typing import Dict, List, Tuple

import numpy as np


# -- Physical constants ------------------------------------------------------
U0 = 0.01       # inlet velocity (lattice units)
D_CYL = 20.0    # cylinder diameter (lattice)


# -- Dimensionless conversion ------------------------------------------------

def compute_dimensionless(
    sensors: np.ndarray,  # (T, 6) raw lattice [s0_ux,s0_uy, s1_ux,s1_uy, s2_ux,s2_uy]
    forces: np.ndarray,   # (T, 6) raw lattice [f0_fx,f0_fy, f1_fx,f1_fy, f2_fx,f2_fy]
    u0: float = U0,
    d: float = D_CYL,
    rho: float = 1.0,
) -> Dict[str, np.ndarray]:
    """Convert raw lattice CFD data to dimensionless quantities.

    Sensor order: [s0_ux,s0_uy, s1_ux,s1_uy, s2_ux,s2_uy]
        where s0=top(y=+2L0), s1=mid(y=0), s2=bottom(y=-2L0)
    Force order: [front_fx,front_fy, bottom_fx,bottom_fy, top_fx,top_fy]

    Returns:
        u_hat_B, u_hat_C, u_hat_T: nondim streamwise velocity (bottom/centre/top)
        v_hat_B, v_hat_C, v_hat_T: nondim crosswise velocity
        Cd_F, Cd_T, Cd_B: drag coefficient per cylinder
        Cl_F, Cl_T, Cl_B: lift coefficient per cylinder
    """
    s = np.asarray(sensors, dtype=np.float64)
    f = np.asarray(forces, dtype=np.float64)

    # Sensor positions: s0=top, s1=centre, s2=bottom
    # Convention: B=bottom=s2, C=centre=s1, T=top=s0
    return {
        "u_hat_T": s[:, 0] / u0,
        "v_hat_T": s[:, 1] / u0,
        "u_hat_C": s[:, 2] / u0,
        "v_hat_C": s[:, 3] / u0,
        "u_hat_B": s[:, 4] / u0,
        "v_hat_B": s[:, 5] / u0,
        "Cd_F": 2.0 * f[:, 0] / (rho * u0**2 * d),
        "Cl_F": 2.0 * f[:, 1] / (rho * u0**2 * d),
        "Cd_B": 2.0 * f[:, 2] / (rho * u0**2 * d),
        "Cl_B": 2.0 * f[:, 3] / (rho * u0**2 * d),
        "Cd_T": 2.0 * f[:, 4] / (rho * u0**2 * d),
        "Cl_T": 2.0 * f[:, 5] / (rho * u0**2 * d),
    }


# -- G operator (corrected) --------------------------------------------------

def apply_G_alpha(alpha: np.ndarray) -> np.ndarray:
    """Apply mirror G to action: [aF, aT, aB] -> [-aF, -aB, -aT]."""
    return np.array([-alpha[0], -alpha[2], -alpha[1]], dtype=alpha.dtype)


def apply_G_x(dim: Dict[str, np.ndarray],
              a_prev: np.ndarray,
              a_prev2: np.ndarray) -> Tuple[Dict, np.ndarray, np.ndarray]:
    """Apply G to dimensionless state.

    Returns (G_dim, G_a_prev, G_a_prev2) with corrected sign rules.
    """
    G_dim = {
        "u_hat_B": dim["u_hat_T"], "u_hat_C": dim["u_hat_C"], "u_hat_T": dim["u_hat_B"],
        "v_hat_B": -dim["v_hat_T"], "v_hat_C": -dim["v_hat_C"], "v_hat_T": -dim["v_hat_B"],
        "Cd_F": dim["Cd_F"], "Cd_T": dim["Cd_B"], "Cd_B": dim["Cd_T"],
        "Cl_F": -dim["Cl_F"], "Cl_T": -dim["Cl_B"], "Cl_B": -dim["Cl_T"],
    }
    G_a_prev = np.column_stack([-a_prev[:, 0], -a_prev[:, 2], -a_prev[:, 1]])
    G_a_prev2 = np.column_stack([-a_prev2[:, 0], -a_prev2[:, 2], -a_prev2[:, 1]])
    return G_dim, G_a_prev, G_a_prev2


# -- Feature key definitions -------------------------------------------------

CORE_FEAT_KEYS = [
    "u_m", "u_a", "u_c",
    "v_a",
    "Cd_tot", "Cd_rear",
    "Cl_tot", "Cl_diff",
    "sin_ua", "cos_ua",
    "aF_lag1", "aB_lag1", "aT_lag1",
    "daF", "daB", "daT",
]

MU_FEAT_KEYS = ["mu", "mu_u_a", "mu_v_a", "mu_Cd_tot", "mu_Cl_diff"]

ALL_FEAT_KEYS = CORE_FEAT_KEYS + MU_FEAT_KEYS


# -- Feature computation -----------------------------------------------------

def compute_features(
    dim: Dict[str, np.ndarray],
    actions_prev: np.ndarray,   # (T, 3) physical omega(t-1) or nondim alpha(t-1)
    actions_prev2: np.ndarray,  # (T, 3) physical omega(t-2)
    mu: float,
    alpha_mode: bool = False,  # if True, actions_prev are already nondim alpha
    include_mu: bool = True,
    u0: float = U0,            # inlet velocity for omega->alpha conversion
) -> Dict[str, np.ndarray]:
    """Compute unified feature dictionary from dimensionless primitives.

    Args:
        dim: from compute_dimensionless()
        actions_prev: lagged actions (physical omega or nondim alpha)
        actions_prev2: twice-lagged actions
        mu: 1/Re_D
        alpha_mode: if True, actions are already nondim; else convert
        include_mu: include mu modulation terms
        u0: inlet velocity (lattice), used only when alpha_mode=False

    Returns dict with all features as (T,) or (T,3) arrays.
    """
    T = actions_prev.shape[0]
    u_B, u_C, u_T = dim["u_hat_B"], dim["u_hat_C"], dim["u_hat_T"]
    v_B, v_C, v_T = dim["v_hat_B"], dim["v_hat_C"], dim["v_hat_T"]
    Cd_F, Cd_T, Cd_B = dim["Cd_F"], dim["Cd_T"], dim["Cd_B"]
    Cl_F, Cl_T, Cl_B = dim["Cl_F"], dim["Cl_T"], dim["Cl_B"]

    # If actions are in physical omega, convert to nondim alpha
    if alpha_mode:
        a = actions_prev.astype(np.float64)
        a2 = actions_prev2.astype(np.float64)
    else:
        a = actions_prev.astype(np.float64) / u0
        a2 = actions_prev2.astype(np.float64) / u0

    sym = {}

    # Sensor combinations (nondim)
    sym["u_m"] = (u_B + u_C + u_T) / 3.0
    sym["u_a"] = (u_T - u_B) / 2.0
    sym["u_c"] = u_C.copy()
    sym["v_a"] = (v_T - v_B) / 2.0

    # Force combinations (dimensionless Cd/Cl)
    sym["Cd_tot"] = Cd_F + Cd_T + Cd_B
    sym["Cd_rear"] = Cd_T + Cd_B
    sym["Cl_tot"] = Cl_F + Cl_T + Cl_B
    sym["Cl_diff"] = Cl_T - Cl_B

    # Phase
    sym["sin_ua"] = np.sin(np.pi * sym["u_a"])
    sym["cos_ua"] = np.cos(np.pi * sym["u_a"])

    # Memory (nondim alpha)
    sym["aF_lag1"] = a[:, 0]
    sym["aB_lag1"] = a[:, 1]
    sym["aT_lag1"] = a[:, 2]
    sym["daF"] = a[:, 0] - a2[:, 0]
    sym["daB"] = a[:, 1] - a2[:, 1]
    sym["daT"] = a[:, 2] - a2[:, 2]

    # Mu modulation
    if include_mu:
        sym["mu"] = np.full(T, mu, dtype=np.float64)
        sym["mu_u_a"] = sym["u_a"] * mu
        sym["mu_v_a"] = sym["v_a"] * mu
        sym["mu_Cd_tot"] = sym["Cd_tot"] * mu
        sym["mu_Cl_diff"] = sym["Cl_diff"] * mu

    return sym


def build_feature_matrix(
    sym: Dict[str, np.ndarray],
    feat_keys: List[str],
    add_bias: bool = True,
) -> np.ndarray:
    """Build feature matrix (T, N) from symbol dict."""
    cols = []
    if add_bias:
        cols.append(np.ones(sym[feat_keys[0]].shape[0], dtype=np.float64))
    for k in feat_keys:
        if k in sym:
            cols.append(np.asarray(sym[k], dtype=np.float64))
        else:
            # Missing key -> zero
            T = sym.get("u_m", np.ones(1)).shape[0]
            cols.append(np.zeros(T, dtype=np.float64))
    return np.column_stack(cols)


def get_feature_names(feat_keys: List[str], add_bias: bool = True) -> List[str]:
    """Get feature names matching build_feature_matrix output."""
    names = []
    if add_bias:
        names.append("bias")
    names.extend(feat_keys)
    return names
