"""Loudness engine — BS.1770 integrated LUFS + peak/true-peak. CPU, NO torch.

The mastering numbers PLN + hexa actually gate on (reference_postprod_master:
-14 LUFS streaming, -9 LUFS club). pyloudnorm does the K-weighted BS.1770
integrated loudness; true-peak is a cheap 4× oversample (catches inter-sample
peaks a raw sample-peak misses). Returns gain-to-target so a caller knows exactly
how much to push for each delivery target.

FULL-TRACK (#7): streams the WHOLE track in bounded windows. Peak / true-peak /
RMS / crest are EXACT (running max / accumulated sum-of-squares). Integrated LUFS
is energy-weighted across per-window meters — pyloudnorm's gating can't be merged
exactly across chunks, so this is a close APPROXIMATION, flagged `approximate:true`
with `lufs_method:"chunked"`. For a steady master the error is well under ~0.5 LU.
"""
import numpy as np

from . import streaming

_EPS = 1e-12
TARGETS = {"streaming": -14.0, "club": -9.0}   # LUFS, reference_postprod_master
_METER_MIN_S = 0.4                              # pyloudnorm needs ≥ one 400 ms gated block


def _db(x):
    return float(20.0 * np.log10(max(abs(float(x)), _EPS)))


def _true_peak_dbfs(mono, sr):
    """4× oversample → max |sample| in dBFS (approximates inter-sample true peak)."""
    try:
        from scipy.signal import resample_poly
        up = resample_poly(mono, 4, 1)
        return _db(np.max(np.abs(up)))
    except Exception:
        return _db(np.max(np.abs(mono)))


def loudness(audio_path, win_s=streaming.DEFAULT_WIN_S) -> dict:
    """Integrated LUFS (chunked-approx), sample/true peak (dBFS, exact), crest,
    + gain-to-target, over the whole track."""
    meta = streaming.probe(audio_path)
    sr, channels = meta["sr"], meta["channels"]
    if meta["frames"] and meta["frames"] < sr // 2:
        raise ValueError("clip too short for integrated loudness (need ≥ 0.5 s)")

    try:
        import pyloudnorm as pyln
        meter = pyln.Meter(sr)
    except Exception:
        meter = None

    sumsq = 0.0
    n = 0
    peak = _EPS
    tp = _EPS
    energy_sum = 0.0        # Σ dur_i · 10^(LUFS_i/10) — linear loudness, duration-weighted
    dur_sum = 0.0
    for block, sr, _ in streaming.windows(audio_path, win_s, mono=False, dtype="float64"):
        if not len(block):
            continue
        mono = block if block.ndim == 1 else block.mean(axis=1)
        sumsq += float(np.sum(mono ** 2))
        n += mono.size
        peak = max(peak, float(np.max(np.abs(mono))))
        tp = max(tp, 10 ** (_true_peak_dbfs(mono, sr) / 20.0))
        if meter is not None and len(block) >= int(_METER_MIN_S * sr):
            try:
                lw = float(meter.integrated_loudness(block))
            except Exception:
                lw = float("-inf")
            if np.isfinite(lw):                        # silence-gated windows → skip
                w = len(block) / sr
                energy_sum += w * (10 ** (lw / 10.0))
                dur_sum += w

    if n < sr // 2:
        raise ValueError("clip too short for integrated loudness (need ≥ 0.5 s)")

    rms_db = _db(np.sqrt(sumsq / max(n, 1) + _EPS))
    peak_db = _db(peak)
    tp_db = _db(tp)
    lufs = 10.0 * np.log10(energy_sum / dur_sum) if dur_sum > 0 else None
    gain = ({k: round(t - lufs, 2) for k, t in TARGETS.items()}
            if lufs is not None and np.isfinite(lufs) else None)
    return {
        "lufs_integrated": round(lufs, 2) if lufs is not None and np.isfinite(lufs) else None,
        "lufs_method": "chunked",
        "approximate": True,          # energy-weighted across windows, not a single gated pass
        "peak_dbfs": round(peak_db, 2),
        "true_peak_dbfs": round(tp_db, 2),
        "rms_dbfs": round(rms_db, 2),
        "crest_db": round(peak_db - rms_db, 2),
        "gain_to_target_db": gain,    # add this many dB to hit each LUFS target
        "channels": int(channels),
        "sr": int(sr),
        "duration_s": round(meta["duration_s"], 3),
        "engine": "light",
        "full_track": True,
    }
