"""
ReplayGain analysis and tag writing for SoulSync.

Analysis is performed via FFmpeg's ebur128 filter (ReplayGain 2.0, -18 LUFS reference).
Tag writing uses mutagen directly to stay consistent with the rest of the codebase.

Supported formats: MP3, FLAC, OGG Vorbis, Opus, M4A/MP4
"""

import logging
import re
import subprocess
from typing import Optional, Tuple, Dict

logger = logging.getLogger(__name__)

# ReplayGain 2.0 reference level (EBU R128)
RG_REFERENCE_LUFS = -18.0

# Tag names used across all formats
_TAG_TRACK_GAIN = "REPLAYGAIN_TRACK_GAIN"
_TAG_TRACK_PEAK = "REPLAYGAIN_TRACK_PEAK"
_TAG_ALBUM_GAIN = "REPLAYGAIN_ALBUM_GAIN"
_TAG_ALBUM_PEAK = "REPLAYGAIN_ALBUM_PEAK"

_AUDIO_EXTENSIONS = {'.mp3', '.flac', '.ogg', '.oga', '.opus', '.m4a', '.mp4'}


# ---------------------------------------------------------------------------
# FFmpeg availability
# ---------------------------------------------------------------------------

def is_ffmpeg_available() -> bool:
    """Return True if ffmpeg is on PATH."""
    try:
        subprocess.run(
            ['ffmpeg', '-version'],
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL,
            timeout=5
        )
        return True
    except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
        return False


# ---------------------------------------------------------------------------
# Analysis
# ---------------------------------------------------------------------------

def analyze_track(file_path: str) -> Tuple[float, float]:
    """
    Analyze a single audio file and return (integrated_lufs, true_peak_dbfs).

    Uses FFmpeg's ebur128 filter with true peak measurement.
    Raises:
        FileNotFoundError: if ffmpeg is not on PATH
        RuntimeError: if ffmpeg fails or output cannot be parsed
    """
    cmd = [
        'ffmpeg', '-nostdin', '-v', 'info',
        '-i', file_path,
        '-filter:a', 'ebur128=peak=true',
        '-f', 'null', '-'
    ]
    try:
        result = subprocess.run(
            cmd,
            stdout=subprocess.DEVNULL,
            stderr=subprocess.PIPE,
            text=True,
            timeout=120
        )
    except FileNotFoundError as exc:
        raise FileNotFoundError("ffmpeg not found on PATH") from exc
    except subprocess.TimeoutExpired as exc:
        raise RuntimeError("ffmpeg timed out analyzing track") from exc

    stderr = result.stderr

    # ebur128 emits two kinds of output:
    #   (a) Per-window progress lines like
    #       "[Parsed_ebur128_0 @ ...] t: 0.5 ... I: -70.0 LUFS ..."
    #       — the "I:" here is the PARTIAL integrated loudness up to that
    #       window. The very first window of nearly any track is silent
    #       (intro fade-in / encoder padding) and reads ~-70 LUFS.
    #   (b) A final "Summary:" block like:
    #         Summary:
    #           Integrated loudness:
    #             I:         -14.3 LUFS
    #           ...
    #           True peak:
    #             Peak:        -0.4 dBFS
    #
    # The OLD code used ``re.search('I:\s+...')`` which returned the FIRST
    # match — i.e. the first per-window partial reading. For nearly every
    # track that came back as ~-70 LUFS, producing gain = -18 - (-70) =
    # +52.00 dB on EVERY track. (User report: "all tracks seem to get
    # the same track_gain ... +52.00 dB".)
    #
    # Fix: anchor parsing to the Summary block so we always read the
    # final integrated value, never a per-window partial.
    summary_idx = stderr.rfind('Summary:')
    if summary_idx >= 0:
        summary_block = stderr[summary_idx:]
        lufs_values = re.findall(r'I:\s+([-\d.]+)\s+LUFS', summary_block)
        peak_matches = re.findall(r'Peak:\s+([-\d.]+)\s+dBFS', summary_block)
    else:
        # No Summary block in the output (truncated / unexpected ffmpeg
        # version). Defensive fallback to the LAST per-window reading,
        # which is at least closer to the final integrated value than
        # the first window (which is always silence).
        lufs_values = re.findall(r'I:\s+([-\d.]+)\s+LUFS', stderr)
        peak_matches = re.findall(r'Peak:\s+([-\d.]+)\s+dBFS', stderr)

    if not lufs_values:
        raise RuntimeError(
            f"Could not parse ebur128 output for '{file_path}'. "
            f"FFmpeg exit code: {result.returncode}"
        )

    integrated_lufs = float(lufs_values[-1])

    if peak_matches:
        true_peak_dbfs = max(float(v) for v in peak_matches)
    else:
        # Fall back to 0 dBFS peak if not available
        true_peak_dbfs = 0.0

    return integrated_lufs, true_peak_dbfs


# ---------------------------------------------------------------------------
# Gain / peak formatting helpers
# ---------------------------------------------------------------------------

def format_gain(lufs: float, reference: float = RG_REFERENCE_LUFS) -> str:
    """Return a formatted gain string like '-2.50 dB'."""
    gain = reference - lufs
    return f"{gain:+.2f} dB"


def format_peak(true_peak_dbfs: float) -> str:
    """Convert a true peak in dBFS to a linear peak string like '0.987654'."""
    linear = 10 ** (true_peak_dbfs / 20.0)
    # Clamp to [0, 1] — values above 0 dBFS (>1.0) are kept as-is (clipping)
    return f"{linear:.6f}"


# ---------------------------------------------------------------------------
# Tag reading
# ---------------------------------------------------------------------------

def read_replaygain_tags(file_path: str) -> Dict[str, Optional[str]]:
    """
    Read existing ReplayGain tags from an audio file.

    Returns a dict with keys:
        track_gain, track_peak, album_gain, album_peak
    All values are strings (e.g. "-2.50 dB") or None if not present.
    """
    result = {
        'track_gain': None,
        'track_peak': None,
        'album_gain': None,
        'album_peak': None,
    }

    import os
    ext = os.path.splitext(file_path)[1].lower()
    if ext not in _AUDIO_EXTENSIONS:
        return result

    try:
        from mutagen import File as MutagenFile
        audio = MutagenFile(file_path, easy=False)
        if audio is None:
            return result

        if ext == '.mp3':
            result['track_gain'] = _read_id3_txxx(audio, _TAG_TRACK_GAIN)
            result['track_peak'] = _read_id3_txxx(audio, _TAG_TRACK_PEAK)
            result['album_gain'] = _read_id3_txxx(audio, _TAG_ALBUM_GAIN)
            result['album_peak'] = _read_id3_txxx(audio, _TAG_ALBUM_PEAK)
        elif ext in ('.flac', '.ogg', '.oga', '.opus'):
            result['track_gain'] = _vorbis_first(audio, _TAG_TRACK_GAIN.lower())
            result['track_peak'] = _vorbis_first(audio, _TAG_TRACK_PEAK.lower())
            result['album_gain'] = _vorbis_first(audio, _TAG_ALBUM_GAIN.lower())
            result['album_peak'] = _vorbis_first(audio, _TAG_ALBUM_PEAK.lower())
        elif ext in ('.m4a', '.mp4'):
            result['track_gain'] = _mp4_rg(audio, _TAG_TRACK_GAIN)
            result['track_peak'] = _mp4_rg(audio, _TAG_TRACK_PEAK)
            result['album_gain'] = _mp4_rg(audio, _TAG_ALBUM_GAIN)
            result['album_peak'] = _mp4_rg(audio, _TAG_ALBUM_PEAK)
    except Exception as e:
        logger.debug("read replaygain tags failed: %s", e)

    return result


def _read_id3_txxx(audio, description: str) -> Optional[str]:
    """Read a TXXX frame value by description (case-insensitive)."""
    try:
        key = f"TXXX:{description}"
        if key in audio.tags:
            frame = audio.tags[key]
            return str(frame.text[0]) if frame.text else None
        # Also try uppercase/lowercase variants
        for frame_key in audio.tags.keys():
            if frame_key.upper() == key.upper():
                frame = audio.tags[frame_key]
                return str(frame.text[0]) if frame.text else None
    except Exception as e:
        logger.debug("read id3 txxx frame failed: %s", e)
    return None


def _vorbis_first(audio, key: str) -> Optional[str]:
    """Return the first value of a Vorbis comment key, or None."""
    try:
        vals = audio.get(key) or audio.get(key.upper())
        if vals:
            return str(vals[0])
    except Exception as e:
        logger.debug("read vorbis comment failed: %s", e)
    return None


def _mp4_rg(audio, tag_name: str) -> Optional[str]:
    """Read a ReplayGain freeform atom from MP4."""
    try:
        key = f"----:com.apple.iTunes:{tag_name}"
        if key in audio:
            raw = audio[key]
            if raw:
                val = raw[0]
                if hasattr(val, 'decode'):
                    return val.decode('utf-8')
                return str(val)
    except Exception as e:
        logger.debug("read mp4 replaygain atom failed: %s", e)
    return None


# ---------------------------------------------------------------------------
# Tag writing
# ---------------------------------------------------------------------------

def write_replaygain_tags(
    file_path: str,
    track_gain_db: float,
    track_peak_dbfs: float,
    album_gain_db: Optional[float] = None,
    album_peak_dbfs: Optional[float] = None,
) -> bool:
    """
    Write ReplayGain tags to an audio file.

    Args:
        file_path: Path to the audio file.
        track_gain_db: Track gain in dB (gain = ref - lufs, signed).
        track_peak_dbfs: Track true peak in dBFS.
        album_gain_db: Album gain in dB, or None to skip writing album tags.
        album_peak_dbfs: Album true peak in dBFS, or None to skip album tags.

    Returns:
        True on success, False on failure.
    """
    import os
    ext = os.path.splitext(file_path)[1].lower()
    if ext not in _AUDIO_EXTENSIONS:
        return False

    track_gain_str = f"{track_gain_db:+.2f} dB"
    track_peak_str = format_peak(track_peak_dbfs)

    album_gain_str = f"{album_gain_db:+.2f} dB" if album_gain_db is not None else None
    album_peak_str = format_peak(album_peak_dbfs) if album_peak_dbfs is not None else None

    try:
        from mutagen import File as MutagenFile
        audio = MutagenFile(file_path, easy=False)
        if audio is None:
            return False

        if ext == '.mp3':
            _write_id3_rg(audio, track_gain_str, track_peak_str, album_gain_str, album_peak_str)
        elif ext in ('.flac', '.ogg', '.oga', '.opus'):
            _write_vorbis_rg(audio, track_gain_str, track_peak_str, album_gain_str, album_peak_str)
        elif ext in ('.m4a', '.mp4'):
            _write_mp4_rg(audio, track_gain_str, track_peak_str, album_gain_str, album_peak_str)
        else:
            return False

        audio.save()
        return True
    except Exception:
        return False


def _write_id3_rg(audio, track_gain: str, track_peak: str,
                  album_gain: Optional[str], album_peak: Optional[str]) -> None:
    """Write ReplayGain TXXX frames to an MP3 file's ID3 tags."""
    from mutagen.id3 import TXXX

    if audio.tags is None:
        audio.add_tags()

    def _set_txxx(desc: str, value: str) -> None:
        # Remove any existing frame with this description (case-insensitive)
        to_delete = [k for k in audio.tags.keys() if k.upper() == f"TXXX:{desc}".upper()]
        for k in to_delete:
            del audio.tags[k]
        audio.tags.add(TXXX(encoding=3, desc=desc, text=[value]))

    _set_txxx(_TAG_TRACK_GAIN, track_gain)
    _set_txxx(_TAG_TRACK_PEAK, track_peak)
    if album_gain is not None:
        _set_txxx(_TAG_ALBUM_GAIN, album_gain)
    if album_peak is not None:
        _set_txxx(_TAG_ALBUM_PEAK, album_peak)


def _write_vorbis_rg(audio, track_gain: str, track_peak: str,
                     album_gain: Optional[str], album_peak: Optional[str]) -> None:
    """Write ReplayGain Vorbis comments (FLAC, OGG, Opus)."""
    audio[_TAG_TRACK_GAIN.lower()] = [track_gain]
    audio[_TAG_TRACK_PEAK.lower()] = [track_peak]
    if album_gain is not None:
        audio[_TAG_ALBUM_GAIN.lower()] = [album_gain]
    if album_peak is not None:
        audio[_TAG_ALBUM_PEAK.lower()] = [album_peak]


def _write_mp4_rg(audio, track_gain: str, track_peak: str,
                  album_gain: Optional[str], album_peak: Optional[str]) -> None:
    """Write ReplayGain freeform atoms to an MP4/M4A file."""
    from mutagen.mp4 import MP4FreeForm

    def _set_atom(name: str, value: str) -> None:
        key = f"----:com.apple.iTunes:{name}"
        audio[key] = [MP4FreeForm(value.encode('utf-8'))]

    _set_atom(_TAG_TRACK_GAIN, track_gain)
    _set_atom(_TAG_TRACK_PEAK, track_peak)
    if album_gain is not None:
        _set_atom(_TAG_ALBUM_GAIN, album_gain)
    if album_peak is not None:
        _set_atom(_TAG_ALBUM_PEAK, album_peak)
