"""
Stegasoo Audio Utilities (v4.3.0)

Audio format detection, transcoding, and metadata extraction for audio steganography.

Dependencies:
- soundfile (sf): Fast WAV/FLAC reading without ffmpeg
- pydub: MP3/OGG/AAC transcoding (wraps ffmpeg)

Both are optional — functions degrade gracefully when unavailable.
"""

from __future__ import annotations

import io
import shutil

from .constants import (
    EMBED_MODE_AUDIO_AUTO,
    MAX_AUDIO_DURATION,
    MAX_AUDIO_FILE_SIZE,
    MAX_AUDIO_SAMPLE_RATE,
    MIN_AUDIO_SAMPLE_RATE,
    VALID_AUDIO_EMBED_MODES,
)
from .debug import get_logger
from .exceptions import AudioTranscodeError, AudioValidationError, UnsupportedAudioFormatError
from .models import AudioInfo, ValidationResult

logger = get_logger(__name__)


# =============================================================================
# FFMPEG AVAILABILITY
# =============================================================================


def has_ffmpeg_support() -> bool:
    """Check if ffmpeg is available on the system.

    Returns:
        True if ffmpeg is found on PATH, False otherwise.
    """
    return shutil.which("ffmpeg") is not None


# =============================================================================
# FORMAT DETECTION
# =============================================================================


def detect_audio_format(audio_data: bytes) -> str:
    """Detect audio format from magic bytes.

    Examines the first bytes of audio data to identify the container format.

    Magic byte signatures:
    - WAV: b"RIFF" at offset 0 + b"WAVE" at offset 8
    - FLAC: b"fLaC" at offset 0
    - MP3: b"\\xff\\xfb", b"\\xff\\xf3", b"\\xff\\xf2" (sync bytes) or b"ID3" (ID3 tag)
    - OGG (Vorbis/Opus): b"OggS" at offset 0
    - AAC: b"\\xff\\xf1" or b"\\xff\\xf9" (ADTS header)
    - M4A/MP4: b"ftyp" at offset 4

    Args:
        audio_data: Raw audio file bytes.

    Returns:
        Format string: "wav", "flac", "mp3", "ogg", "aac", "m4a", or "unknown".
    """
    if len(audio_data) < 12:
        logger.debug("detect_audio_format: data too short (%d bytes)", len(audio_data))
        return "unknown"

    # WAV: RIFF....WAVE
    if audio_data[:4] == b"RIFF" and audio_data[8:12] == b"WAVE":
        logger.debug("Detected WAV format (%d bytes)", len(audio_data))
        return "wav"

    # FLAC
    if audio_data[:4] == b"fLaC":
        return "flac"

    # OGG (Vorbis or Opus)
    if audio_data[:4] == b"OggS":
        return "ogg"

    # MP3 with ID3 tag
    if audio_data[:3] == b"ID3":
        return "mp3"

    # MP3 sync bytes (MPEG audio frame header)
    if len(audio_data) >= 2 and audio_data[:2] in (b"\xff\xfb", b"\xff\xf3", b"\xff\xf2"):
        return "mp3"

    # M4A/MP4 container: "ftyp" at offset 4
    if audio_data[4:8] == b"ftyp":
        return "m4a"

    # AAC ADTS header
    if len(audio_data) >= 2 and audio_data[:2] in (b"\xff\xf1", b"\xff\xf9"):
        return "aac"

    return "unknown"


# =============================================================================
# TRANSCODING
# =============================================================================


def transcode_to_wav(audio_data: bytes) -> bytes:
    """Transcode any supported audio format to WAV PCM format.

    Uses soundfile directly for WAV/FLAC (no ffmpeg needed).
    Uses pydub (wraps ffmpeg) for lossy formats (MP3, OGG, AAC, M4A).

    Args:
        audio_data: Raw audio file bytes in any supported format.

    Returns:
        WAV PCM file bytes (16-bit, original sample rate).

    Raises:
        AudioTranscodeError: If transcoding fails.
        UnsupportedAudioFormatError: If the format cannot be detected.
    """
    fmt = detect_audio_format(audio_data)
    logger.info("transcode_to_wav: input format=%s, size=%d bytes", fmt, len(audio_data))

    if fmt == "unknown":
        raise UnsupportedAudioFormatError(
            "Cannot detect audio format. Supported: WAV, FLAC, MP3, OGG, AAC, M4A."
        )

    # WAV files: validate with soundfile but return as-is if already PCM
    if fmt == "wav":
        try:
            import soundfile as sf

            buf = io.BytesIO(audio_data)
            info = sf.info(buf)
            if info.subtype in ("PCM_16", "PCM_24", "PCM_32", "FLOAT", "DOUBLE"):
                # Re-encode to ensure consistent PCM_16 output
                buf.seek(0)
                data, samplerate = sf.read(buf, dtype="int16")
                out = io.BytesIO()
                sf.write(out, data, samplerate, format="WAV", subtype="PCM_16")
                return out.getvalue()
        except ImportError:
            raise AudioTranscodeError("soundfile package is required for WAV processing")
        except Exception as e:
            raise AudioTranscodeError(f"Failed to process WAV: {e}")

    # FLAC: use soundfile (fast, no ffmpeg)
    if fmt == "flac":
        try:
            import soundfile as sf

            buf = io.BytesIO(audio_data)
            data, samplerate = sf.read(buf, dtype="int16")
            out = io.BytesIO()
            sf.write(out, data, samplerate, format="WAV", subtype="PCM_16")
            return out.getvalue()
        except ImportError:
            raise AudioTranscodeError("soundfile package is required for FLAC processing")
        except Exception as e:
            raise AudioTranscodeError(f"Failed to transcode FLAC to WAV: {e}")

    # Lossy formats (MP3, OGG, AAC, M4A): use pydub + ffmpeg
    return _transcode_with_pydub(audio_data, fmt, "wav")


def transcode_to_mp3(audio_data: bytes, bitrate: str = "256k") -> bytes:
    """Transcode audio to MP3 format.

    Uses pydub (wraps ffmpeg) for transcoding.

    Args:
        audio_data: Raw audio file bytes in any supported format.
        bitrate: Target MP3 bitrate (e.g., "128k", "192k", "256k", "320k").

    Returns:
        MP3 file bytes.

    Raises:
        AudioTranscodeError: If transcoding fails or pydub/ffmpeg unavailable.
    """
    fmt = detect_audio_format(audio_data)

    if fmt == "unknown":
        raise UnsupportedAudioFormatError(
            "Cannot detect audio format. Supported: WAV, FLAC, MP3, OGG, AAC, M4A."
        )

    try:
        from pydub import AudioSegment
    except ImportError:
        raise AudioTranscodeError(
            "pydub package is required for MP3 transcoding. Install with: pip install pydub"
        )

    if not has_ffmpeg_support():
        raise AudioTranscodeError(
            "ffmpeg is required for MP3 transcoding. Install ffmpeg on your system."
        )

    try:
        # Map our format names to pydub format names
        pydub_fmt = _pydub_format(fmt)
        buf = io.BytesIO(audio_data)
        audio = AudioSegment.from_file(buf, format=pydub_fmt)

        out = io.BytesIO()
        audio.export(out, format="mp3", bitrate=bitrate)
        return out.getvalue()
    except Exception as e:
        raise AudioTranscodeError(f"Failed to transcode to MP3: {e}")


def _transcode_with_pydub(audio_data: bytes, src_fmt: str, dst_fmt: str) -> bytes:
    """Transcode audio using pydub (requires ffmpeg).

    Args:
        audio_data: Raw audio bytes.
        src_fmt: Source format string (our naming).
        dst_fmt: Destination format string ("wav" or "mp3").

    Returns:
        Transcoded audio bytes.

    Raises:
        AudioTranscodeError: If transcoding fails.
    """
    try:
        from pydub import AudioSegment
    except ImportError:
        raise AudioTranscodeError(
            "pydub package is required for audio transcoding. Install with: pip install pydub"
        )

    if not has_ffmpeg_support():
        raise AudioTranscodeError(
            "ffmpeg is required for audio transcoding. Install ffmpeg on your system."
        )

    try:
        pydub_fmt = _pydub_format(src_fmt)
        buf = io.BytesIO(audio_data)
        audio = AudioSegment.from_file(buf, format=pydub_fmt)

        out = io.BytesIO()
        if dst_fmt == "wav":
            audio.export(out, format="wav")
        else:
            audio.export(out, format=dst_fmt)
        return out.getvalue()
    except Exception as e:
        raise AudioTranscodeError(f"Failed to transcode {src_fmt} to {dst_fmt}: {e}")


def _pydub_format(fmt: str) -> str:
    """Map our format names to pydub/ffmpeg format names.

    Args:
        fmt: Our internal format name.

    Returns:
        pydub-compatible format string.
    """
    mapping = {
        "wav": "wav",
        "flac": "flac",
        "mp3": "mp3",
        "ogg": "ogg",
        "aac": "aac",
        "m4a": "m4a",
    }
    return mapping.get(fmt, fmt)


# =============================================================================
# METADATA EXTRACTION
# =============================================================================


def get_audio_info(audio_data: bytes) -> AudioInfo:
    """Extract audio metadata from raw audio bytes.

    Uses soundfile for WAV/FLAC (fast, no ffmpeg dependency).
    Falls back to pydub for other formats (requires ffmpeg).

    Args:
        audio_data: Raw audio file bytes.

    Returns:
        AudioInfo dataclass with sample rate, channels, duration, etc.

    Raises:
        UnsupportedAudioFormatError: If the format cannot be detected.
        AudioTranscodeError: If metadata extraction fails.
    """
    fmt = detect_audio_format(audio_data)

    if fmt == "unknown":
        raise UnsupportedAudioFormatError(
            "Cannot detect audio format. Supported: WAV, FLAC, MP3, OGG, AAC, M4A."
        )

    # WAV and FLAC: use soundfile (fast)
    if fmt in ("wav", "flac"):
        return _get_info_soundfile(audio_data, fmt)

    # Lossy formats: use pydub
    return _get_info_pydub(audio_data, fmt)


def _get_info_soundfile(audio_data: bytes, fmt: str) -> AudioInfo:
    """Extract audio info using soundfile (WAV/FLAC).

    Args:
        audio_data: Raw audio bytes.
        fmt: Format string ("wav" or "flac").

    Returns:
        AudioInfo with metadata.
    """
    try:
        import soundfile as sf
    except ImportError:
        raise AudioTranscodeError(
            "soundfile package is required. Install with: pip install soundfile"
        )

    try:
        buf = io.BytesIO(audio_data)
        info = sf.info(buf)

        # Determine bit depth from subtype
        bit_depth = _bit_depth_from_subtype(info.subtype)

        return AudioInfo(
            sample_rate=info.samplerate,
            channels=info.channels,
            duration_seconds=info.duration,
            num_samples=info.frames,
            format=fmt,
            bitrate=None,
            bit_depth=bit_depth,
        )
    except Exception as e:
        raise AudioTranscodeError(f"Failed to read {fmt.upper()} metadata: {e}")


def _bit_depth_from_subtype(subtype: str) -> int | None:
    """Determine bit depth from soundfile subtype string.

    Args:
        subtype: Soundfile subtype (e.g., "PCM_16", "PCM_24", "FLOAT").

    Returns:
        Bit depth as integer, or None if unknown.
    """
    subtype_map = {
        "PCM_S8": 8,
        "PCM_U8": 8,
        "PCM_16": 16,
        "PCM_24": 24,
        "PCM_32": 32,
        "FLOAT": 32,
        "DOUBLE": 64,
    }
    return subtype_map.get(subtype)


def _get_info_pydub(audio_data: bytes, fmt: str) -> AudioInfo:
    """Extract audio info using pydub (lossy formats).

    Args:
        audio_data: Raw audio bytes.
        fmt: Format string ("mp3", "ogg", "aac", "m4a").

    Returns:
        AudioInfo with metadata.
    """
    try:
        from pydub import AudioSegment
    except ImportError:
        raise AudioTranscodeError(
            "pydub package is required for audio metadata. Install with: pip install pydub"
        )

    if not has_ffmpeg_support():
        raise AudioTranscodeError(
            "ffmpeg is required for audio metadata extraction. Install ffmpeg on your system."
        )

    try:
        pydub_fmt = _pydub_format(fmt)
        buf = io.BytesIO(audio_data)
        audio = AudioSegment.from_file(buf, format=pydub_fmt)

        num_samples = int(audio.frame_count())
        duration = audio.duration_seconds
        sample_rate = audio.frame_rate
        channels = audio.channels

        # Estimate bitrate from file size and duration
        bitrate = None
        if duration > 0:
            bitrate = int((len(audio_data) * 8) / duration)

        return AudioInfo(
            sample_rate=sample_rate,
            channels=channels,
            duration_seconds=duration,
            num_samples=num_samples,
            format=fmt,
            bitrate=bitrate,
            bit_depth=audio.sample_width * 8 if audio.sample_width else None,
        )
    except Exception as e:
        raise AudioTranscodeError(f"Failed to read {fmt.upper()} metadata: {e}")


# =============================================================================
# VALIDATION
# =============================================================================


def validate_audio(
    audio_data: bytes,
    name: str = "Audio",
    check_duration: bool = True,
) -> ValidationResult:
    """Validate audio data for steganography.

    Checks:
    - Not empty
    - Not too large (MAX_AUDIO_FILE_SIZE)
    - Valid audio format (detectable via magic bytes)
    - Duration within limits (MAX_AUDIO_DURATION) if check_duration=True
    - Sample rate within limits (MIN_AUDIO_SAMPLE_RATE to MAX_AUDIO_SAMPLE_RATE)

    Args:
        audio_data: Raw audio file bytes.
        name: Descriptive name for error messages (default: "Audio").
        check_duration: Whether to enforce duration limit (default: True).

    Returns:
        ValidationResult with audio info in details (sample_rate, channels,
        duration, num_samples, format) on success.
    """
    if not audio_data:
        return ValidationResult.error(f"{name} is required")

    if len(audio_data) > MAX_AUDIO_FILE_SIZE:
        size_mb = len(audio_data) / (1024 * 1024)
        max_mb = MAX_AUDIO_FILE_SIZE / (1024 * 1024)
        return ValidationResult.error(
            f"{name} too large ({size_mb:.1f} MB). Maximum: {max_mb:.0f} MB"
        )

    # Detect format
    fmt = detect_audio_format(audio_data)
    if fmt == "unknown":
        return ValidationResult.error(
            f"Could not detect {name} format. " "Supported formats: WAV, FLAC, MP3, OGG, AAC, M4A."
        )

    # Extract metadata for further validation
    try:
        info = get_audio_info(audio_data)
    except (AudioTranscodeError, UnsupportedAudioFormatError) as e:
        return ValidationResult.error(f"Could not read {name}: {e}")
    except Exception as e:
        return ValidationResult.error(f"Could not read {name}: {e}")

    # Check duration
    if check_duration and info.duration_seconds > MAX_AUDIO_DURATION:
        return ValidationResult.error(
            f"{name} too long ({info.duration_seconds:.1f}s). "
            f"Maximum: {MAX_AUDIO_DURATION}s ({MAX_AUDIO_DURATION // 60} minutes)"
        )

    # Check sample rate
    if info.sample_rate < MIN_AUDIO_SAMPLE_RATE:
        return ValidationResult.error(
            f"{name} sample rate too low ({info.sample_rate} Hz). "
            f"Minimum: {MIN_AUDIO_SAMPLE_RATE} Hz"
        )

    if info.sample_rate > MAX_AUDIO_SAMPLE_RATE:
        return ValidationResult.error(
            f"{name} sample rate too high ({info.sample_rate} Hz). "
            f"Maximum: {MAX_AUDIO_SAMPLE_RATE} Hz"
        )

    return ValidationResult.ok(
        sample_rate=info.sample_rate,
        channels=info.channels,
        duration=info.duration_seconds,
        num_samples=info.num_samples,
        format=info.format,
        bitrate=info.bitrate,
        bit_depth=info.bit_depth,
    )


def require_valid_audio(audio_data: bytes, name: str = "Audio") -> None:
    """Validate audio, raising AudioValidationError on failure.

    Args:
        audio_data: Raw audio file bytes.
        name: Descriptive name for error messages.

    Raises:
        AudioValidationError: If validation fails.
    """
    result = validate_audio(audio_data, name)
    if not result.is_valid:
        raise AudioValidationError(result.error_message)


def validate_audio_embed_mode(mode: str) -> ValidationResult:
    """Validate audio embedding mode string.

    Args:
        mode: Embedding mode to validate (e.g., "audio_lsb", "audio_mdct", "audio_auto").

    Returns:
        ValidationResult with mode in details on success.
    """
    valid_modes = VALID_AUDIO_EMBED_MODES | {EMBED_MODE_AUDIO_AUTO}
    if mode not in valid_modes:
        return ValidationResult.error(
            f"Invalid audio embed_mode: '{mode}'. "
            f"Valid options: {', '.join(sorted(valid_modes))}"
        )
    return ValidationResult.ok(mode=mode)
