"""CPU-only analysis utilities for CCD pipeline.

No pycuda or LegacyCelerisLab dependency — can run with plain python3.
"""
from __future__ import annotations

from typing import Any, Dict, List, Optional, Tuple

import numpy as np


# ---------------------------------------------------------------------------
# Period detection helpers
# ---------------------------------------------------------------------------

def detect_dominant_frequency(signal: np.ndarray, sample_dt: float) -> Tuple[float, float, float]:
    """Detect dominant frequency via FFT. Returns (f_dom, period, peak_power)."""
    n = len(signal)
    if n < 16:
        return 0.0, 0.0, 0.0
    y = signal - np.mean(signal)
    window = np.hanning(n)
    spec = np.abs(np.fft.rfft(y * window)) ** 2
    freqs = np.fft.rfftfreq(n, d=sample_dt)
    idx = 1 + np.argmax(spec[1:])
    f_dom = float(freqs[idx])
    period = 1.0 / f_dom if f_dom > 0 else 0.0
    return f_dom, period, float(spec[idx])


def detect_cycle_stability(signal: np.ndarray, sample_dt: float) -> Tuple[float, float, list]:
    """Detect cycle lengths using rising zero-crossings. Returns (CV_T, mean_T, lengths)."""
    y = signal - np.mean(signal)
    crossings = np.where((np.sign(y[:-1]) < 0) & (np.sign(y[1:]) > 0))[0]
    if len(crossings) < 2:
        return 0.0, 0.0, []
    cycle_lengths = np.diff(crossings).astype(float) * sample_dt
    if len(cycle_lengths) < 2:
        return 0.0, float(cycle_lengths[0]) if len(cycle_lengths) > 0 else 0.0, cycle_lengths.tolist()
    mean_T = float(np.mean(cycle_lengths))
    std_T = float(np.std(cycle_lengths))
    cv_T = std_T / mean_T if mean_T > 0 else 0.0
    return cv_T, mean_T, cycle_lengths.tolist()


# ---------------------------------------------------------------------------
# Phase resampling
# ---------------------------------------------------------------------------

def phase_resample(data: np.ndarray, cycle_starts: List[int], n_pts: int = 24) -> np.ndarray:
    """Resample a multi-channel signal to uniform phase points per cycle.

    Uses piecewise linear interpolation (no scipy dependency).

    Parameters
    ----------
    data : (T, C) ndarray — multi-channel time series.
    cycle_starts : list of int — indices where each cycle starts.
    n_pts : int — number of phase points per cycle.

    Returns
    -------
    resampled : (n_cycles, n_pts, C) ndarray
    """
    n_cycles = len(cycle_starts) - 1
    if n_cycles < 1:
        raise ValueError("Need at least 2 cycle starts")

    if data.ndim == 1:
        data = data[:, None]
    C = data.shape[1]
    out = np.zeros((n_cycles, n_pts, C), dtype=np.float64)

    for c in range(n_cycles):
        i_start = cycle_starts[c]
        i_end = cycle_starts[c + 1]
        segment = data[i_start:i_end + 1]
        seg_len = len(segment)
        if seg_len < 2:
            continue

        old_idx = np.linspace(0, 1, seg_len)
        new_idx = np.linspace(0, 1, n_pts, endpoint=False)
        for ch in range(C):
            out[c, :, ch] = np.interp(new_idx, old_idx, segment[:, ch])

    return out


# ---------------------------------------------------------------------------
# POD
# ---------------------------------------------------------------------------

def compute_pod(snapshot_matrix: np.ndarray) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
    """Compute POD from snapshot matrix (n_points, n_snapshots).

    Returns (mean_field, modes, singular_values, coefficients).
    """
    mean_field = np.mean(snapshot_matrix, axis=1)
    Q = snapshot_matrix - mean_field[:, None]
    U, s, Vt = np.linalg.svd(Q, full_matrices=False)
    coefficients = np.diag(s) @ Vt
    return mean_field, U, s, coefficients


def cumulative_energy(singular_values: np.ndarray) -> np.ndarray:
    e = singular_values ** 2
    return np.cumsum(e) / np.sum(e)


def e95_index(cumulative_energy: np.ndarray) -> int:
    return int(np.searchsorted(cumulative_energy, 0.95) + 1)


# ---------------------------------------------------------------------------
# CCD (reduced, Lyu23-inspired)
# ---------------------------------------------------------------------------

def compute_reduced_ccd(pod_coeffs: np.ndarray, observable: np.ndarray, Q_delay: int = 12) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
    """Compute reduced CCD in POD coefficient space.

    Parameters
    ----------
    pod_coeffs : (r, N) ndarray — standardized POD coefficients.
    observable : (m, N) ndarray — standardized observable.
    Q_delay : int — number of delay steps.

    Returns
    -------
    W : (r, min(r, m*Q_delay)) — CCD directions.
    sigma : (min_dim,) — singular values.
    z : (min_dim, N) — CCD temporal coefficients.
    """
    N = pod_coeffs.shape[1]
    m = observable.shape[0]

    half = Q_delay // 2
    rows = []
    for shift in range(-half, half + 1):
        shifted = np.roll(observable, -shift, axis=1)
        if shift < 0:
            shifted[:, shift:] = 0.0
        elif shift > 0:
            shifted[:, :-shift] = 0.0
        rows.append(shifted)
    P = np.vstack(rows)  # (m*Q_delay, N)

    # Standardize
    P_mean = np.mean(P, axis=1, keepdims=True)
    P_std = np.std(P, axis=1, keepdims=True) + 1e-12
    P_z = (P - P_mean) / P_std

    A_mean = np.mean(pod_coeffs, axis=1, keepdims=True)
    A_std = np.std(pod_coeffs, axis=1, keepdims=True) + 1e-12
    A_z = (pod_coeffs - A_mean) / A_std

    C = P_z @ A_z.T / (N * np.sqrt(float(Q_delay)))
    R, s, Wt = np.linalg.svd(C, full_matrices=False)
    W = Wt.T
    z = W.T @ A_z
    return W, s, z


# ---------------------------------------------------------------------------
# Field stacking helpers
# ---------------------------------------------------------------------------

def stack_velocity_fields(ux_fields: List[np.ndarray], uy_fields: List[np.ndarray]) -> np.ndarray:
    """Stack list of (ux, uy) field pairs into snapshot matrix (2*nx*ny, N)."""
    snapshots = []
    for ux, uy in zip(ux_fields, uy_fields):
        q = np.concatenate([ux.ravel(), uy.ravel()])
        snapshots.append(q)
    return np.column_stack(snapshots)


def unstack_velocity_modes(modes: np.ndarray, ny: int, nx: int, n_modes: int = 6) -> Tuple[List[np.ndarray], List[np.ndarray]]:
    """Unstack POD/CCD modes back into ux, uy fields."""
    ux_list, uy_list = [], []
    half = nx * ny
    for i in range(min(n_modes, modes.shape[1])):
        mode = modes[:, i]
        ux_list.append(mode[:half].reshape(ny, nx))
        uy_list.append(mode[half:].reshape(ny, nx))
    return ux_list, uy_list


# ---------------------------------------------------------------------------
# Harmonics analysis for illusion
# ---------------------------------------------------------------------------

def analyze_harmonics(states: np.ndarray, n_harmonics: int = 5) -> list:
    """FFT harmonics analysis. Returns list of dicts per channel."""
    N, D = states.shape
    result = []
    for d in range(D):
        y = states[:, d]
        fft_coef = np.fft.rfft(y)
        freqs = np.fft.rfftfreq(N, d=1)
        amps = 2.0 * np.abs(fft_coef) / N
        phases = np.angle(fft_coef)
        idx = np.argsort(amps[1:])[::-1][:n_harmonics] + 1
        harmonics = {
            'dc': float(np.real(fft_coef[0]) / N),
            'amps': amps[idx].tolist(),
            'freqs': freqs[idx].tolist(),
            'phases': phases[idx].tolist(),
        }
        result.append(harmonics)
    return result


def gen_target_states_at(t, harmonics):
    """Reconstruct target observable at step index t from harmonics."""
    t = np.asarray(t)
    D = len(harmonics)
    result = np.zeros((t.size, D), dtype=np.float32)
    for d, h in enumerate(harmonics):
        val = np.full(t.shape, h['dc'], dtype=np.float32)
        for amp, freq, phase in zip(h['amps'], h['freqs'], h['phases']):
            val += amp * np.cos(2 * np.pi * freq * t + phase)
        result[:, d] = val
    if result.shape[0] == 1:
        return result[0]
    return result
