"""Harmony analyzer for AI MusiMuse.

This module defines :class:`HarmonyAnalyzer`, which extracts
deterministic harmonic descriptors from decoded audio using shared
DSP utilities.

No chord recognition, chord progression analysis, melody extraction,
or machine learning is performed.
The analyzer must not perform logging — the pipeline owns all logging.
The analyzer must not access the database — persistence is the
pipeline's responsibility.
"""

from __future__ import annotations

import time

import numpy as np

from analyzer.analyzer import Analyzer
from analyzer.context import AnalysisContext
from analyzer.feature import Feature, FeatureSet
from analyzer.result import AnalysisResult
from core.exceptions import AnalysisError
from dsp.config import CHROMA_BINS, FFT_SIZE, HOP_LENGTH
from dsp.harmony import (
    compute_chroma,
    compute_tonal_centroid,
    compute_tonal_stability,
    estimate_key,
    estimate_mode,
)
from dsp.spectrum import hann_window

CHROMA_NAMES = [
    "harmony.chroma_c",
    "harmony.chroma_csharp",
    "harmony.chroma_d",
    "harmony.chroma_dsharp",
    "harmony.chroma_e",
    "harmony.chroma_f",
    "harmony.chroma_fsharp",
    "harmony.chroma_g",
    "harmony.chroma_gsharp",
    "harmony.chroma_a",
    "harmony.chroma_asharp",
    "harmony.chroma_b",
]

TONNETZ_NAMES = [
    "harmony.tonnetz_x",
    "harmony.tonnetz_y",
    "harmony.tonnetz_z",
    "harmony.tonnetz_u",
    "harmony.tonnetz_v",
    "harmony.tonnetz_w",
]


class HarmonyAnalyzer(Analyzer):
    """Extracts harmonic features from decoded audio.

    Produces 22 features: 12 chroma bins, estimated key, estimated
    mode, 6 tonal centroid dimensions, tonal stability, and pitch
    class entropy.

    All calculations use shared DSP utilities from :mod:`dsp.harmony`
    and :mod:`dsp.spectrum`, and configuration from :mod:`dsp.config`.
    """

    name = "harmony"
    version = "1.0.0"

    def analyze(self, context: AnalysisContext) -> AnalysisResult:
        """Analyze decoded audio and return harmonic features.

        Args:
            context: The analysis context containing decoded audio.

        Returns:
            An :class:`AnalysisResult` with harmonic feature data.

        Raises:
            AnalysisError: If the audio is empty, contains NaN, or
                contains infinite values.
        """
        start = time.perf_counter()

        audio = context.decoded_audio
        samples = audio.samples

        if samples.size == 0:
            raise AnalysisError("Audio contains no samples")

        if np.any(np.isnan(samples)):
            raise AnalysisError("Audio contains NaN values")

        if np.any(np.isinf(samples)):
            raise AnalysisError("Audio contains infinite values")

        # Convert to mono
        mono = np.mean(samples[:, :2], axis=1) if audio.channels > 1 else samples[:, 0]

        sample_rate = audio.sample_rate

        # Chroma vector
        window = hann_window(FFT_SIZE)
        chroma = compute_chroma(
            mono, FFT_SIZE, HOP_LENGTH, window, sample_rate, CHROMA_BINS
        )

        # Key and mode estimation
        key = estimate_key(chroma)
        mode = estimate_mode(chroma)

        # Tonal centroid
        tonnetz = compute_tonal_centroid(chroma)

        # Tonal stability
        tonal_stability = compute_tonal_stability(chroma)

        # Pitch class entropy
        total = float(np.sum(chroma))
        if total > 0.0:
            probs = chroma / total
            probs = probs[probs > 0.0]
            pitch_class_entropy = float(-np.sum(probs * np.log2(probs)))
        else:
            pitch_class_entropy = 0.0

        features: list[Feature] = []

        # 12 chroma features
        for i, name in enumerate(CHROMA_NAMES):
            features.append(
                Feature(
                    name=name,
                    value=float(chroma[i]),
                    analyzer=self.name,
                    version=self.version,
                )
            )

        # Key and mode
        features.append(
            Feature(
                name="harmony.key",
                value=key,
                analyzer=self.name,
                version=self.version,
            )
        )
        features.append(
            Feature(
                name="harmony.mode",
                value=mode,
                analyzer=self.name,
                version=self.version,
            )
        )

        # 6 tonnetz features
        for i, name in enumerate(TONNETZ_NAMES):
            features.append(
                Feature(
                    name=name,
                    value=float(tonnetz[i]),
                    analyzer=self.name,
                    version=self.version,
                )
            )

        # Tonal stability and entropy
        features.append(
            Feature(
                name="harmony.tonal_stability",
                value=tonal_stability,
                analyzer=self.name,
                version=self.version,
            )
        )
        features.append(
            Feature(
                name="harmony.pitch_class_entropy",
                value=pitch_class_entropy,
                analyzer=self.name,
                version=self.version,
            )
        )

        feature_set = FeatureSet(features)
        elapsed_ms = (time.perf_counter() - start) * 1000.0

        return AnalysisResult(
            analyzer_name=self.name,
            analyzer_version=self.version,
            execution_time_ms=elapsed_ms,
            success=True,
            warnings=(),
            feature_set=feature_set,
        )
