"""Light emotion engine — CPU-native V/A from librosa features, NO torch/CLAP.

The erable host (Debian 9, no GPU, ~2 GB RAM) can't fit the ~4 GB CLAP/torch
image. This is the deployable fallback: a heuristic valence/arousal read from
cheap signal features, mapped onto the SAME emotion-ontology anchors as the CLAP
engine so the API response shape is identical (engine swap is invisible to hexa).

It is a BASELINE, honestly labelled (engine="light", lower confidence): arousal
from energy/tempo/brightness, valence from major/minor mode (Krumhansl) + brightness.
A precise tier (Essentia MusiCNN, or CLAP on a GPU runner) lands later behind the
same /v1 shape — see task #37. CLAP proposes, PLN's ear confirms.
"""
import numpy as np

# V/A anchors — mirror emotion_ontology.EMOTIONS (name → valence, arousal). Duplicated
# here on purpose so the light path imports NO torch (importing emotion_ontology pulls
# sample_semantics → torch). test_ears_light asserts these stay in sync.
ANCHORS = {
    "euphoric": (0.90, 0.85), "triumphant": (0.80, 0.60), "playful": (0.70, 0.50),
    "warm": (0.60, -0.20), "serene": (0.50, -0.80), "dreamy": (0.40, -0.45),
    "hypnotic": (0.00, 0.35), "cold": (-0.20, -0.30), "tense": (-0.30, 0.60),
    "aggressive": (-0.40, 0.90), "melancholic": (-0.65, -0.40), "dark": (-0.75, 0.20),
}

# Krumhansl-Schmuckler key profiles for major/minor mode estimation.
_MAJ = np.array([6.35, 2.23, 3.48, 2.33, 4.38, 4.09, 2.52, 5.19, 2.39, 3.66, 2.29, 2.88])
_MIN = np.array([6.33, 2.68, 3.52, 5.38, 2.60, 3.53, 2.54, 4.75, 3.98, 2.69, 3.34, 3.17])


def _scale(x, lo, hi):
    """Map [lo,hi] → [-1,1], clamped."""
    return float(np.clip(2 * (x - lo) / (hi - lo) - 1, -1, 1))


def _best_corr(chroma, profile):
    """Max correlation of the 12-bin chroma against a key profile over all 12 rotations."""
    c = chroma - chroma.mean()
    p = profile - profile.mean()
    pn = np.linalg.norm(p) + 1e-9
    best = -1.0
    for k in range(12):
        r = np.roll(c, k)
        denom = (np.linalg.norm(r) + 1e-9) * pn
        best = max(best, float((r @ p) / denom))
    return best


def _mode_valence(chroma_mean):
    """+ for major-leaning, − for minor-leaning, in ~[-1,1]."""
    maj = _best_corr(chroma_mean, _MAJ)
    minr = _best_corr(chroma_mean, _MIN)
    return float(np.clip((maj - minr) * 2.0, -1, 1))


def emotion_light(audio_path, max_seconds=60) -> dict:
    """Heuristic V/A read with the same shape as emotion_ontology.score()."""
    import librosa
    y, sr = librosa.load(str(audio_path), sr=22050, mono=True, duration=max_seconds)
    if y.size == 0:
        raise ValueError("empty / undecodable audio")

    rms = float(np.sqrt(np.mean(y ** 2)))
    cent = float(librosa.feature.spectral_centroid(y=y, sr=sr).mean())
    onset_env = librosa.onset.onset_strength(y=y, sr=sr)
    tempo = float(np.atleast_1d(librosa.feature.tempo(onset_envelope=onset_env, sr=sr))[0])

    # arousal: loud + fast + bright = energetic
    arousal = float(np.clip(np.mean([
        _scale(rms, 0.0, 0.30),
        _scale(tempo, 60, 180),
        _scale(cent, 800, 4000),
    ]), -1, 1))

    # valence: mostly mode (major/minor), a touch of brightness
    chroma = librosa.feature.chroma_cqt(y=y, sr=sr).mean(axis=1)
    valence = float(np.clip(0.75 * _mode_valence(chroma) + 0.25 * _scale(cent, 800, 4000), -1, 1))

    # map onto the ontology anchors by V/A distance → a CLAP-shaped distribution
    names = list(ANCHORS)
    d = np.array([(valence - ANCHORS[n][0]) ** 2 + (arousal - ANCHORS[n][1]) ** 2 for n in names])
    z = -d / 0.25
    z -= z.max()
    dist = np.exp(z); dist /= dist.sum()
    order = np.argsort(-dist)
    top = [(names[i], round(float(dist[i]), 3)) for i in order]
    conf = float(dist[order[0]] - dist[order[1]])

    return {
        "valence": round(valence, 3),
        "arousal": round(arousal, 3),
        "top": top,
        "dist": {n: p for n, p in top},
        "confidence": round(conf, 3),
        "engine": "light",
    }
