"""Light signal building blocks — onsets + waveform peaks. CPU, NO torch.

The cheapest analysis tier: thin librosa wrappers that need no model at all.
Especially useful for hexa (a visuals platform): /waveform gives a render-ready
peak/energy envelope, /onsets gives rhythmic hit times for visual sync + slicing.
Self-contained like ears_light/feats (the container has only armada/api/).
"""
import numpy as np

SR = 22050
MAX_S = 60.0


def _load(audio_path, max_seconds):
    import librosa
    y, _ = librosa.load(str(audio_path), sr=SR, mono=True, duration=max_seconds)
    if y.size < SR // 20:
        raise ValueError("clip too short / undecodable (need ≥ 50 ms of audio)")
    return y


def onsets(audio_path, max_seconds=MAX_S) -> dict:
    """Onset hit times (s) + tempo + onset rate. Drift-tolerant tempo via the
    onset envelope (librosa 0.11 feature.tempo, not the removed rhythm.tempo)."""
    import librosa
    y = _load(audio_path, max_seconds)
    onset_env = librosa.onset.onset_strength(y=y, sr=SR)
    times = librosa.onset.onset_detect(onset_envelope=onset_env, sr=SR, units="time")
    tempo = float(np.atleast_1d(librosa.feature.tempo(onset_envelope=onset_env, sr=SR))[0])
    dur = len(y) / SR
    return {
        "tempo_bpm": round(tempo, 2),
        "onsets": [round(float(t), 4) for t in times],
        "n_onsets": int(len(times)),
        "onset_rate": round(len(times) / dur, 3) if dur else 0.0,
        "duration_s": round(dur, 3),
        "sr": SR,
        "engine": "light",
    }


def waveform(audio_path, bins=800, max_seconds=MAX_S) -> dict:
    """Downsampled waveform for rendering: per-bin [min, max] in [-1,1] (draws the
    classic waveform) + a 0..1 RMS energy envelope (audio-reactive intensity)."""
    y = _load(audio_path, max_seconds)
    bins = max(1, min(int(bins), 4000))
    edges = np.linspace(0, len(y), bins + 1).astype(int)
    peaks, rms = [], []
    for i in range(bins):
        seg = y[edges[i]:edges[i + 1]]
        if seg.size == 0:
            peaks.append([0.0, 0.0]); rms.append(0.0); continue
        peaks.append([round(float(seg.min()), 4), round(float(seg.max()), 4)])
        rms.append(float(np.sqrt(np.mean(seg ** 2))))
    norm = max(rms) or 1.0
    return {
        "bins": bins,
        "peaks": peaks,
        "rms": [round(r / norm, 4) for r in rms],   # 0..1, peak-normalized
        "duration_s": round(len(y) / SR, 3),
        "sr": SR,
        "engine": "light",
    }


def spectrum(audio_path, bands=64, frames=200, mel=True, max_seconds=MAX_S) -> dict:
    """FFT-as-a-service: a downsampled, render-ready spectrogram — `bands` ×
    `frames`, 0..1 normalized (from dB, ref=peak). mel=True uses perceptual mel
    bands (good for visuals); mel=False uses log-spaced linear bins. `frames=1`
    collapses to a single time-averaged FFT magnitude spectrum. Bounded payload,
    so it caches cheaply. Torch-free."""
    import librosa
    y = _load(audio_path, max_seconds)
    bands = max(1, min(int(bands), 256))
    frames = max(1, min(int(frames), 1000))
    if mel:
        S = librosa.feature.melspectrogram(y=y, sr=SR, n_mels=bands)
        band_hz = [round(float(f), 1) for f in librosa.mel_frequencies(n_mels=bands, fmax=SR / 2)]
    else:
        mag = np.abs(librosa.stft(y, n_fft=2048, hop_length=512)) ** 2
        freqs = librosa.fft_frequencies(sr=SR, n_fft=2048)
        edges = np.logspace(np.log10(20), np.log10(SR / 2), bands + 1)
        S = np.stack([mag[(freqs >= edges[i]) & (freqs < edges[i + 1])].sum(axis=0)
                      for i in range(bands)])
        band_hz = [round(float(np.sqrt(edges[i] * edges[i + 1])), 1) for i in range(bands)]
    Sdb = librosa.power_to_db(S + 1e-12, ref=np.max)          # [-80..0] dB
    # downsample time → `frames` columns by averaging within each block
    cols = Sdb.shape[1]
    edges = np.linspace(0, cols, frames + 1).astype(int)
    blocks = [Sdb[:, edges[i]:edges[i + 1]].mean(axis=1) if edges[i + 1] > edges[i]
              else Sdb[:, min(edges[i], cols - 1)] for i in range(frames)]
    spec = (np.clip(np.stack(blocks, axis=1), -80, 0) + 80) / 80.0   # bands×frames, 0..1
    return {
        "bands": bands,
        "frames": frames,
        "mel": bool(mel),
        "band_hz": band_hz,
        "spec": [[round(float(v), 4) for v in row] for row in spec],   # [band][frame]
        "duration_s": round(len(y) / SR, 3),
        "sr": SR,
        "engine": "light",
    }
