"""Audio decoder for AI MusiMuse.

This module implements :class:`AudioDecoder`, which decodes supported
audio files into normalized :class:`~decoder.decoded_audio.DecodedAudio`
objects.

The decoder uses ``soundfile`` as the decoding backend for v0.1.
The backend is isolated behind the :class:`AudioDecoder` interface so
that it can be replaced (e.g. with PyAV) without changing application
code.

The Decoder must not calculate DSP features, access the database, or
import SQLAlchemy.
"""

from __future__ import annotations

from pathlib import Path

import soundfile as sf

from core.exceptions import DecodingError
from decoder.decoded_audio import DecodedAudio
from log.logger import get_logger

_SUBTYPE_BIT_DEPTH: dict[str, int] = {
    "PCM_S8": 8,
    "PCM_U8": 8,
    "PCM_16": 16,
    "PCM_24": 24,
    "PCM_32": 32,
    "FLOAT": 32,
    "DOUBLE": 64,
}

_logger = get_logger(__name__)


def _extract_bit_depth(subtype: str) -> int | None:
    """Map a soundfile subtype string to a bit depth.

    Args:
        subtype: The soundfile subtype string (e.g. ``"PCM_16"``).

    Returns:
        The bit depth as an integer, or ``None`` if unknown.
    """
    return _SUBTYPE_BIT_DEPTH.get(subtype)


class AudioDecoder:
    """Decodes supported audio files into normalized DecodedAudio objects.

    The decoder normalizes samples to float32 in the range [-1.0, 1.0].
    Multi-channel audio preserves its original channel count.
    No resampling is performed.
    """

    def decode(self, file_path: Path) -> DecodedAudio:
        """Decode an audio file and return a DecodedAudio object.

        Args:
            file_path: Path to the audio file to decode.

        Returns:
            A :class:`DecodedAudio` instance with normalized samples
            and extracted metadata.

        Raises:
            DecodingError: If the file cannot be decoded.
        """
        _logger.info("decoder.file", path=str(file_path))

        try:
            info = sf.info(str(file_path))
            samples, sample_rate = sf.read(
                str(file_path),
                dtype="float32",
                always_2d=True,
            )
        except Exception as exc:
            _logger.error("decoder.failed", path=str(file_path), error=str(exc))
            raise DecodingError(f"Failed to decode '{file_path}': {exc}") from exc

        n_frames, n_channels = samples.shape
        duration = n_frames / sample_rate
        bit_depth = _extract_bit_depth(info.subtype)

        _logger.info(
            "decoder.file_decoded",
            path=str(file_path),
            frames=n_frames,
            channels=n_channels,
            sample_rate=sample_rate,
            duration=duration,
            bit_depth=bit_depth,
        )

        return DecodedAudio(
            samples=samples,
            sample_rate=sample_rate,
            channels=n_channels,
            duration=duration,
            bit_depth=bit_depth,
        )
