"""Audio-file BPM analysis helpers."""

from __future__ import annotations

from pathlib import Path
from statistics import fmean
import struct
import wave

from .ranges import normalize_bpm


def analyze_audio_file(file_path: str | Path, min_bpm: float = 70, max_bpm: float = 180) -> float:
    """Estimate BPM from a local uncompressed WAV audio file."""

    path = Path(file_path)
    if not path.is_file():
        raise ValueError(f"Audio file not found: {path}")

    samples, sample_rate = _read_wav_mono_samples(path)
    if len(samples) < sample_rate:
        raise ValueError("Audio file is too short for BPM analysis.")

    onset_envelope = _build_onset_envelope(samples, window_size=1024, hop_size=512)
    bpm = _estimate_bpm_from_onsets(
        onset_envelope,
        sample_rate=sample_rate,
        hop_size=512,
        min_bpm=min_bpm,
        max_bpm=max_bpm,
    )
    return round(normalize_bpm(bpm, min_bpm=min_bpm, max_bpm=max_bpm), 2)


def _read_wav_mono_samples(path: Path) -> tuple[list[float], int]:
    with wave.open(str(path), "rb") as wav_file:
        if wav_file.getcomptype() != "NONE":
            raise ValueError("Only uncompressed WAV files are supported.")

        channels = wav_file.getnchannels()
        sample_width = wav_file.getsampwidth()
        sample_rate = wav_file.getframerate()
        frame_count = wav_file.getnframes()
        raw_frames = wav_file.readframes(frame_count)

    if channels <= 0:
        raise ValueError("Audio file must contain at least one channel.")

    interleaved_samples = _decode_pcm_samples(raw_frames, sample_width)
    expected_samples = frame_count * channels
    if len(interleaved_samples) != expected_samples:
        raise ValueError("Audio file data is truncated or malformed.")

    mono_samples: list[float] = []
    for index in range(0, len(interleaved_samples), channels):
        frame = interleaved_samples[index : index + channels]
        mono_samples.append(fmean(frame))

    return mono_samples, sample_rate


def _decode_pcm_samples(raw_frames: bytes, sample_width: int) -> list[float]:
    if sample_width == 1:
        return [((sample - 128) / 128.0) for sample in raw_frames]

    if sample_width == 2:
        values = struct.iter_unpack("<h", raw_frames)
        return [sample[0] / 32768.0 for sample in values]

    if sample_width == 3:
        samples: list[float] = []
        for index in range(0, len(raw_frames), 3):
            chunk = raw_frames[index : index + 3]
            if len(chunk) < 3:
                break
            integer = chunk[0] | (chunk[1] << 8) | (chunk[2] << 16)
            if integer & 0x800000:
                integer -= 0x1000000
            samples.append(integer / 8388608.0)
        return samples

    if sample_width == 4:
        values = struct.iter_unpack("<i", raw_frames)
        return [sample[0] / 2147483648.0 for sample in values]

    raise ValueError("Unsupported WAV sample width. Use 8, 16, 24, or 32-bit PCM WAV.")


def _build_onset_envelope(samples: list[float], window_size: int, hop_size: int) -> list[float]:
    if len(samples) < window_size:
        raise ValueError("Audio file is too short for BPM analysis.")

    energy: list[float] = []
    for start in range(0, len(samples) - window_size + 1, hop_size):
        window = samples[start : start + window_size]
        energy.append(sum(abs(sample) for sample in window) / window_size)

    if len(energy) < 4:
        raise ValueError("Audio file is too short for BPM analysis.")

    onset_envelope: list[float] = []
    previous = energy[0]
    for current in energy[1:]:
        onset_envelope.append(max(0.0, current - previous))
        previous = current

    if max(onset_envelope, default=0.0) <= 0:
        raise ValueError("Could not detect rhythmic onsets from the audio file.")

    return onset_envelope


def _estimate_bpm_from_onsets(
    onset_envelope: list[float],
    sample_rate: int,
    hop_size: int,
    min_bpm: float,
    max_bpm: float,
) -> float:
    if min_bpm <= 0 or max_bpm <= 0 or min_bpm > max_bpm:
        raise ValueError("min_bpm and max_bpm must be positive and ordered.")

    min_lag = max(1, int((60.0 * sample_rate) / (hop_size * max_bpm)))
    max_lag = max(min_lag, int((60.0 * sample_rate) / (hop_size * min_bpm)))

    best_lag = min_lag
    best_score = float("-inf")

    for lag in range(min_lag, max_lag + 1):
        score = 0.0
        for index in range(lag, len(onset_envelope)):
            score += onset_envelope[index] * onset_envelope[index - lag]

        if score > best_score:
            best_score = score
            best_lag = lag

    if best_score <= 0:
        raise ValueError("Could not estimate BPM from the audio file.")

    return (60.0 * sample_rate) / (hop_size * best_lag)

