"""Adapter for ffmpeg-related operations.

Provides a small OO wrapper around common ffmpeg workflows used by the
project. Methods delegate to the infra implementation where appropriate
or run the ffmpeg commands directly for small utilities.
"""
import subprocess
import os
import shutil
import tempfile
import logging
from typing import Iterable, List, Optional


logger = logging.getLogger(__name__)


def ensure_ffmpeg_available() -> bool:
    """Simple check to ensure ffmpeg/ffprobe are present in PATH.

    Returns True if both are available, otherwise raises RuntimeError.
    """
    for cmd in ("ffmpeg", "ffprobe"):
        try:
            subprocess.run([cmd, "-version"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True)
        except Exception:
            raise RuntimeError(f"Required binary not found in PATH: {cmd}")
    return True


__all__ = ["FFmpegAudioProcessor", "ensure_ffmpeg_available"]
import os
import shutil
import subprocess
import tempfile
from typing import Iterable, List, Optional


def ensure_ffmpeg_available() -> None:
    if shutil.which("ffmpeg") is None:
        raise RuntimeError("ffmpeg no está disponible en PATH")


def _run(cmd: List[str], hide_output: bool = False) -> None:
    logger.debug("Ejecutando comando: %s", " ".join(cmd))
    if hide_output:
        subprocess.run(cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
    else:
        subprocess.run(cmd, check=True)


def extract_audio(video_path: str, out_wav: str, sr: int = 16000) -> None:
    """Extrae la pista de audio de un vídeo y la convierte a WAV PCM mono a sr hz."""
    ensure_ffmpeg_available()
    cmd = [
        "ffmpeg",
        "-y",
        "-i",
        video_path,
        "-vn",
        "-acodec",
        "pcm_s16le",
        "-ar",
        str(sr),
        "-ac",
        "1",
        out_wav,
    ]
    logger.info("Extraer audio: %s -> %s (sr=%s)", video_path, out_wav, sr)
    _run(cmd)


def replace_audio_in_video(video_path: str, audio_path: str, out_video: str) -> None:
    """Reemplaza la pista de audio del vídeo por audio_path (codifica a AAC)."""
    ensure_ffmpeg_available()
    cmd = [
        "ffmpeg",
        "-y",
        "-i",
        video_path,
        "-i",
        audio_path,
        "-map",
        "0:v:0",
        "-map",
        "1:a:0",
        "-c:v",
        "copy",
        "-c:a",
        "aac",
        "-b:a",
        "192k",
        out_video,
    ]
    logger.info("Replace audio en video: %s + %s -> %s", video_path, audio_path, out_video)
    _run(cmd)


def burn_subtitles(video_path: str, srt_path: str, out_video: str, font: Optional[str] = "Arial", size: int = 24) -> None:
    """Quema subtítulos en el vídeo usando el filtro subtitles de ffmpeg.

    Nota: el path al .srt debe ser accesible y no contener caracteres problemáticos.
    """
    ensure_ffmpeg_available()
    # usar filter_complex cuando el path contiene caracteres especiales puede complicar,
    # pero normalmente subtitles=path funciona si el path es abosluto
    abs_srt = os.path.abspath(srt_path)
    vf = f"subtitles={abs_srt}:force_style='FontName={font},FontSize={size}'"
    cmd = [
        "ffmpeg",
        "-y",
        "-i",
        video_path,
        "-vf",
        vf,
        "-c:a",
        "copy",
        out_video,
    ]
    logger.info("Burn subtitles: %s + %s -> %s", video_path, srt_path, out_video)
    _run(cmd)


def save_bytes_as_wav(raw_bytes: bytes, target_path: str, sr: int = 22050) -> None:
    """Guarda bytes recibidos de un servicio TTS en un WAV válido usando ffmpeg.

    Escribe bytes a un archivo temporal y usa ffmpeg para convertir al formato objetivo.
    """
    ensure_ffmpeg_available()
    with tempfile.NamedTemporaryFile(delete=False, suffix=".bin") as tmp:
        tmp.write(raw_bytes)
        tmp.flush()
        tmp_path = tmp.name

    logger.debug("Convertir bytes a wav -> %s (sr=%s)", target_path, sr)
    try:
        cmd = [
            "ffmpeg",
            "-y",
            "-i",
            tmp_path,
            "-ar",
            str(sr),
            "-ac",
            "1",
            "-sample_fmt",
            "s16",
            target_path,
        ]
        _run(cmd, hide_output=True)
    except subprocess.CalledProcessError:
        # fallback: escribir bytes crudos
        logger.exception("ffmpeg falló al convertir bytes a wav; escribiendo crudo")
        with open(target_path, "wb") as out:
            out.write(raw_bytes)
    finally:
        try:
            os.remove(tmp_path)
        except Exception:
            pass


def create_silence(duration: float, out_path: str, sr: int = 22050) -> None:
    """Crea un WAV silencioso de duración (segundos) usando anullsrc."""
    ensure_ffmpeg_available()
    cmd = [
        "ffmpeg",
        "-y",
        "-f",
        "lavfi",
        "-i",
        f"anullsrc=channel_layout=mono:sample_rate={sr}",
        "-t",
        f"{duration}",
        "-c:a",
        "pcm_s16le",
        out_path,
    ]
    try:
        _run(cmd, hide_output=True)
    except subprocess.CalledProcessError:
        logger.exception("No se pudo crear silencio con ffmpeg; creando archivo de ceros")
        with open(out_path, "wb") as fh:
            fh.write(b"\x00" * 1024)


def pad_or_trim_wav(in_path: str, out_path: str, target_duration: float, sr: int = 22050) -> None:
    """Rellena con silencio o recorta para que el WAV tenga target_duration en segundos."""
    ensure_ffmpeg_available()
    # obtener duración con ffprobe
    try:
        p = subprocess.run(
            [
                "ffprobe",
                "-v",
                "error",
                "-show_entries",
                "format=duration",
                "-of",
                "default=noprint_wrappers=1:nokey=1",
                in_path,
            ],
            capture_output=True,
            text=True,
            check=True,
        )
        cur = float(p.stdout.strip())
    except Exception:
        cur = 0.0

    if cur == 0.0:
        logger.debug("Duración desconocida; copiando %s -> %s", in_path, out_path)
        shutil.copy(in_path, out_path)
        return

    if abs(cur - target_duration) < 0.02:
        logger.debug("Duración ya cercana al target; copiando")
        shutil.copy(in_path, out_path)
        return

    if cur > target_duration:
        cmd = ["ffmpeg", "-y", "-i", in_path, "-t", f"{target_duration}", out_path]
        _run(cmd, hide_output=True)
        return

    # pad: crear silencio y concatenar
    pad = target_duration - cur
    with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as sil:
        sil_path = sil.name
    listname = None
    try:
        create_silence(pad, sil_path, sr=sr)
        with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".txt") as listf:
            listf.write(f"file '{os.path.abspath(in_path)}'\n")
            listf.write(f"file '{os.path.abspath(sil_path)}'\n")
            listname = listf.name
        cmd2 = ["ffmpeg", "-y", "-f", "concat", "-safe", "0", "-i", listname, "-c", "copy", out_path]
        _run(cmd2, hide_output=True)
    finally:
        try:
            os.remove(sil_path)
        except Exception:
            pass
        try:
            if listname:
                os.remove(listname)
        except Exception:
            pass


def concat_wavs(chunks: Iterable[str], out_path: str) -> None:
    """Concatena una lista de WAVs en out_path usando el demuxer concat (sin recodificar)."""
    ensure_ffmpeg_available()
    with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".txt") as listf:
        for c in chunks:
            listf.write(f"file '{os.path.abspath(c)}'\n")
        listname = listf.name

    try:
        cmd = ["ffmpeg", "-y", "-f", "concat", "-safe", "0", "-i", listname, "-c", "copy", out_path]
        _run(cmd)
    except subprocess.CalledProcessError:
        # fallback: reconvertir por entrada concat
        tmp_concat = out_path + ".tmp.wav"
        cmd2 = ["ffmpeg", "-y", "-i", f"concat:{'|'.join(chunks)}", "-c", "copy", tmp_concat]
        _run(cmd2)
        shutil.move(tmp_concat, out_path)
    finally:
        try:
            os.remove(listname)
        except Exception:
            pass


class FFmpegAudioProcessor:
    """Adaptador de audio que expone utilidades necesarias por el orquestador.

    Métodos principales:
    - extract_audio
    - replace_audio_in_video
    - burn_subtitles
    - save_bytes_as_wav
    - create_silence
    - pad_or_trim_wav
    - concat_wavs
    """

    def extract_audio(self, video_path: str, out_wav: str, sr: int = 16000) -> None:
        return extract_audio(video_path, out_wav, sr=sr)

    def replace_audio_in_video(self, video_path: str, audio_path: str, out_video: str) -> None:
        return replace_audio_in_video(video_path, audio_path, out_video)

    def burn_subtitles(self, video_path: str, srt_path: str, out_video: str, font: Optional[str] = "Arial", size: int = 24) -> None:
        return burn_subtitles(video_path, srt_path, out_video, font=font, size=size)

    def save_bytes_as_wav(self, raw_bytes: bytes, target_path: str, sr: int = 22050) -> None:
        return save_bytes_as_wav(raw_bytes, target_path, sr=sr)

    def create_silence(self, duration: float, out_path: str, sr: int = 22050) -> None:
        return create_silence(duration, out_path, sr=sr)

    def pad_or_trim_wav(self, in_path: str, out_path: str, target_duration: float, sr: int = 22050) -> None:
        return pad_or_trim_wav(in_path, out_path, target_duration, sr=sr)

    def concat_wavs(self, chunks: Iterable[str], out_path: str) -> None:
        return concat_wavs(chunks, out_path)

