"""Layer 2 temporal structure discovery."""

from __future__ import annotations

from typing import Callable

import numpy as np
from scipy.stats import linregress

from cognitive_signal.models.types import SignalVector, TemporalSignal

SIGNAL_FIELD_MAP: dict[str, Callable[[SignalVector], float]] = {
    "valence": lambda value: value.valence,
    "arousal": lambda value: value.arousal,
    "agency": lambda value: value.agency,
    "self_focus": lambda value: value.self_focus,
    "social_orientation": lambda value: value.social_orientation,
    "action_ratio": lambda value: value.action_ratio,
}


def _clamp(value: float, lo: float, hi: float) -> float:
    return max(lo, min(hi, value))


class TemporalAnalyzer:
    def compute_deltas(self, signals: list[float]) -> list[float]:
        if not signals:
            return []
        deltas = [0.0]
        deltas.extend(float(signals[index] - signals[index - 1]) for index in range(1, len(signals)))
        return deltas

    def compute_trend(self, signals: list[float], window: int = 7) -> float:
        if not signals:
            return 0.0
        window_size = max(1, min(window, len(signals)))
        segment = np.asarray(signals[-window_size:], dtype=float)
        if segment.size < 2:
            return 0.0
        if np.allclose(segment, segment[0]):
            return 0.0
        x_axis = np.arange(segment.size, dtype=float)
        try:
            slope = float(linregress(x_axis, segment).slope)
        except Exception:
            return 0.0
        if np.isnan(slope):
            return 0.0
        return slope

    def compute_volatility(self, signals: list[float], window: int = 7) -> float:
        if not signals:
            return 0.0
        window_size = max(1, min(window, len(signals)))
        segment = np.asarray(signals[-window_size:], dtype=float)
        if segment.size < 2:
            return 0.0
        return float(np.std(segment, ddof=0))

    def compute_persistence(self, signals: list[float]) -> float:
        if len(signals) < 2:
            return 1.0
        array = np.asarray(signals, dtype=float)
        x_prev = array[:-1]
        x_next = array[1:]
        if x_prev.size == 0 or x_next.size == 0:
            return 1.0
        if np.var(x_prev) == 0.0 or np.var(x_next) == 0.0:
            return 1.0
        corr = float(np.corrcoef(x_next, x_prev)[0, 1])
        if np.isnan(corr):
            return 1.0
        return _clamp(corr, -1.0, 1.0)

    def analyze_signal(self, values: list[float], window: int = 7) -> list[TemporalSignal]:
        if not values:
            return []
        deltas = self.compute_deltas(values)
        temporal_values: list[TemporalSignal] = []
        for index in range(len(values)):
            segment = values[max(0, index - window + 1) : index + 1]
            temporal_values.append(
                TemporalSignal(
                    delta=float(deltas[index]),
                    trend=float(self.compute_trend(segment, window=window)),
                    volatility=float(self.compute_volatility(segment, window=window)),
                    persistence=float(self.compute_persistence(segment)),
                )
            )
        return temporal_values

    def analyze_all(
        self, signal_vectors: list[SignalVector], window: int = 7
    ) -> dict[str, list[TemporalSignal]]:
        if not signal_vectors:
            return {name: [] for name in SIGNAL_FIELD_MAP}

        output: dict[str, list[TemporalSignal]] = {}
        for name, getter in SIGNAL_FIELD_MAP.items():
            values = [float(getter(vector)) for vector in signal_vectors]
            output[name] = self.analyze_signal(values, window=window)
        return output


__all__ = ["TemporalAnalyzer", "SIGNAL_FIELD_MAP"]
