"""Pipeline orchestrator for the signal detection engine."""

from __future__ import annotations

from time import perf_counter

import numpy as np

from cognitive_signal.changepoint.detector import SCALAR_SIGNALS, ChangePointDetector
from cognitive_signal.dissonance.detector import DissonanceDetector, DissonanceVector
from cognitive_signal.hmm.segmenter import OBSERVATION_FEATURE_NAMES, StateSegmenter
from cognitive_signal.models.types import (
    ChangePointOutput,
    JournalEntry,
    Phase,
    SegmentationOutput,
    SignalDetectionOutput,
    SignalVector,
)
from cognitive_signal.semantic.motion import SemanticMotionAnalyzer, SemanticMotionOutput
from cognitive_signal.signals.extractors import SignalExtractor
from cognitive_signal.temporal.analyzer import TemporalAnalyzer

DISSONANCE_FIELDS = [
    "protest_positivity",
    "passive_agency_talk",
    "social_isolation_conflict",
    "forced_calm",
    "overall_dissonance",
]


class SignalDetectionPipeline:
    def __init__(
        self,
        signal_extractor: SignalExtractor | None = None,
        dissonance_detector: DissonanceDetector | None = None,
        temporal_analyzer: TemporalAnalyzer | None = None,
        semantic_analyzer: SemanticMotionAnalyzer | None = None,
        state_segmenter: StateSegmenter | None = None,
        changepoint_detector: ChangePointDetector | None = None,
        temporal_window: int = 7,
        semantic_window: int = 7,
        semantic_n_components: int = 10,
        semantic_epsilon: float = 0.5,
        semantic_tau: float = 0.7,
        semantic_quantile: float = 0.85,
        hmm_max_states: int = 6,
        hmm_iterations: int = 150,
    ):
        self.signal_extractor = signal_extractor or SignalExtractor()
        self.dissonance_detector = dissonance_detector or DissonanceDetector()
        self.temporal_analyzer = temporal_analyzer or TemporalAnalyzer()
        self.semantic_analyzer = semantic_analyzer or SemanticMotionAnalyzer()
        self.state_segmenter = state_segmenter or StateSegmenter()
        self.changepoint_detector = changepoint_detector or ChangePointDetector()

        self.temporal_window = temporal_window
        self.semantic_window = semantic_window
        self.semantic_n_components = semantic_n_components
        self.semantic_epsilon = semantic_epsilon
        self.semantic_tau = semantic_tau
        self.semantic_quantile = semantic_quantile
        self.hmm_max_states = hmm_max_states
        self.hmm_iterations = hmm_iterations

    def process_entry(self, entry: JournalEntry) -> SignalVector:
        """Extract layer-1 orthogonal signals for a single journal entry."""
        return self.signal_extractor.extract(entry)

    def process_journal(self, entries: list[JournalEntry]) -> SignalDetectionOutput:
        """Run the full layered pipeline over chronologically ordered entries."""
        started = perf_counter()
        if not entries:
            return SignalDetectionOutput(
                journal_entries=[],
                signal_vectors=[],
                dissonance_vectors=[],
                temporal_signals=self._empty_temporal_signals(),
                semantic_motion=self._empty_semantic_motion(),
                segmentation=self._empty_segmentation(),
                state_labels={},
                changepoints=self._empty_changepoints(),
                phases=[],
                _raw_observations=np.empty((0, 0), dtype=np.float32),
                _raw_feature_names=list(OBSERVATION_FEATURE_NAMES),
                _raw_processing_seconds=perf_counter() - started,
            )

        signal_vectors = [self.process_entry(entry) for entry in entries]
        dissonance_vectors = self._compute_dissonance(entries, signal_vectors)

        temporal_signals = self.temporal_analyzer.analyze_all(
            signal_vectors, window=self.temporal_window
        )
        temporal_signals.update(
            self._analyze_dissonance_temporal(
                dissonance_vectors=dissonance_vectors,
                window=self.temporal_window,
            )
        )

        embeddings = [vector.embedding for vector in signal_vectors]
        semantic_motion = self.semantic_analyzer.analyze(
            embeddings=embeddings,
            epsilon=self.semantic_epsilon,
            tau=self.semantic_tau,
            window=self.semantic_window,
            n_components=self.semantic_n_components,
        )

        observations = self.state_segmenter.prepare_observations(signal_vectors)
        if len(signal_vectors) == 1:
            n_states = 1
        else:
            n_states = self.state_segmenter.select_n_states(
                observations=observations,
                max_states=min(self.hmm_max_states, len(signal_vectors)),
            )
            n_states = max(1, n_states)

        segmentation = self.state_segmenter.fit_predict(
            observations=observations,
            n_states=n_states,
            n_iterations=self.hmm_iterations,
        )
        state_labels = self.state_segmenter.label_states(
            segmentation.state_means, OBSERVATION_FEATURE_NAMES
        )

        changepoints = self.changepoint_detector.detect_all(
            signal_vectors=signal_vectors,
            embeddings=embeddings,
            semantic_quantile=self.semantic_quantile,
        )
        phases = self.changepoint_detector.annotate_phases(
            changepoints=changepoints.merged_changepoints,
            n_entries=len(entries),
        )
        if not phases:
            phases = [Phase(start=0, end=len(entries) - 1, phase_id=0)]

        return SignalDetectionOutput(
            journal_entries=entries,
            signal_vectors=signal_vectors,
            dissonance_vectors=dissonance_vectors,
            temporal_signals=temporal_signals,
            semantic_motion=semantic_motion,
            segmentation=segmentation,
            state_labels=state_labels,
            changepoints=changepoints,
            phases=phases,
            _raw_observations=observations,
            _raw_feature_names=list(OBSERVATION_FEATURE_NAMES),
            _raw_processing_seconds=perf_counter() - started,
        )

    def _compute_dissonance(
        self,
        entries: list[JournalEntry],
        signal_vectors: list[SignalVector],
    ) -> list[DissonanceVector]:
        dissonance_vectors: list[DissonanceVector] = []
        for index, (entry, signal_vector) in enumerate(zip(entries, signal_vectors)):
            dissonance_vectors.append(
                self.dissonance_detector.detect(
                    signal_vector=signal_vector,
                    recent_signals=signal_vectors[:index],
                    text=entry.text,
                )
            )
        return dissonance_vectors

    def _analyze_dissonance_temporal(
        self,
        dissonance_vectors: list[DissonanceVector],
        window: int,
    ) -> dict[str, list]:
        output: dict[str, list] = {}
        for field_name in DISSONANCE_FIELDS:
            series = [float(getattr(vector, field_name)) for vector in dissonance_vectors]
            output[f"dissonance_{field_name}"] = self.temporal_analyzer.analyze_signal(
                series, window=window
            )
        return output

    @staticmethod
    def _empty_semantic_motion() -> SemanticMotionOutput:
        return SemanticMotionOutput(
            step_distances=[],
            reduced_embeddings=np.empty((0, 0), dtype=np.float32),
            drift_vectors=[],
            identity_shifts=[],
        )

    @staticmethod
    def _empty_segmentation() -> SegmentationOutput:
        return SegmentationOutput(
            state_sequence=[],
            transition_matrix=np.empty((0, 0), dtype=np.float32),
            state_means=np.empty((0, 0), dtype=np.float32),
            state_covariances=np.empty((0, 0, 0), dtype=np.float32),
            log_likelihood=0.0,
        )

    @staticmethod
    def _empty_changepoints() -> ChangePointOutput:
        return ChangePointOutput(
            signal_changepoints={name: [] for name in SCALAR_SIGNALS},
            semantic_changepoints=[],
            merged_changepoints=[],
        )

    @staticmethod
    def _empty_temporal_signals() -> dict[str, list]:
        keys = list(SCALAR_SIGNALS) + [f"dissonance_{name}" for name in DISSONANCE_FIELDS]
        return {key: [] for key in keys}


def run_pipeline(entry: JournalEntry) -> SignalDetectionOutput:
    """Backward-compatible helper for one-entry runs."""
    pipeline = SignalDetectionPipeline()
    return pipeline.process_journal([entry])
