from __future__ import annotations

import shutil
import subprocess
import time
from pathlib import Path

from app.errors import JobCancelled, VaaniError

ALLOWED_SUFFIXES = {
    ".mp3",
    ".wav",
    ".m4a",
    ".aac",
    ".ogg",
    ".flac",
    ".mp4",
    ".webm",
    ".mkv",
    ".mov",
    ".m4v",
}


def ffmpeg_ready() -> bool:
    return shutil.which("ffmpeg") is not None


def ffprobe_ready() -> bool:
    return shutil.which("ffprobe") is not None or ffmpeg_ready()


def _meta(value: str) -> str:
    return " ".join((value or "").split())[:180]


def run_command(args: list[str], cancel, timeout: int) -> None:
    deadline = time.monotonic() + timeout
    with subprocess.Popen(args, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE) as process:
        try:
            while True:
                if cancel():
                    raise JobCancelled()
                remaining = deadline - time.monotonic()
                if remaining <= 0:
                    raise VaaniError("FFmpeg took too long and was stopped.", "audio_failed", 500, "retry")
                try:
                    _, stderr = process.communicate(timeout=min(0.4, remaining))
                    break
                except subprocess.TimeoutExpired:
                    continue
        finally:
            if process.poll() is None:
                process.kill()
                process.communicate()
    if process.returncode != 0:
        detail = (stderr or b"").decode("utf-8", errors="replace")[-400:]
        raise VaaniError(
            "The audio could not be prepared.",
            "audio_failed",
            500,
            "retry",
        ) from RuntimeError(detail)


def probe_duration(path: Path) -> float:
    if not shutil.which("ffprobe"):
        raise VaaniError(
            "FFmpeg is not installed, so the length of this file cannot be read.",
            "audio_tool_missing",
            500,
            "retry",
        )
    result = subprocess.run(
        [
            "ffprobe",
            "-v",
            "error",
            "-show_entries",
            "format=duration",
            "-of",
            "default=noprint_wrappers=1:nokey=1",
            str(path),
        ],
        capture_output=True,
        text=True,
        timeout=60,
        check=False,
    )
    if result.returncode != 0:
        raise VaaniError("This file has no readable audio.", "audio_unreadable", 400)
    try:
        duration = float((result.stdout or "").strip())
    except ValueError as exc:
        raise VaaniError("This file has no readable audio.", "audio_unreadable", 400) from exc
    if duration <= 0:
        raise VaaniError("This file has no readable audio.", "audio_unreadable", 400)
    return duration


def render_audio(source: Path, mp3: Path, wav: Path, title: str, artist: str, cancel) -> None:
    if not ffmpeg_ready():
        raise VaaniError(
            "FFmpeg is not installed, so the MP3 cannot be made.",
            "audio_tool_missing",
            500,
            "retry",
        )
    partial = mp3.with_name("audio.partial.mp3")
    if partial.exists():
        partial.unlink()
    run_command(
        [
            "ffmpeg",
            "-y",
            "-i",
            str(source),
            "-vn",
            "-ac",
            "2",
            "-ar",
            "44100",
            "-b:a",
            "192k",
            "-metadata",
            f"title={_meta(title)}",
            "-metadata",
            f"artist={_meta(artist) or 'Vaani'}",
            str(partial),
        ],
        cancel,
        timeout=1800,
    )
    partial.replace(mp3)
    wav_partial = wav.with_name("audio.partial.wav")
    if wav_partial.exists():
        wav_partial.unlink()
    run_command(
        [
            "ffmpeg",
            "-y",
            "-i",
            str(mp3),
            "-vn",
            "-ac",
            "1",
            "-ar",
            "16000",
            "-c:a",
            "pcm_s16le",
            str(wav_partial),
        ],
        cancel,
        timeout=1800,
    )
    wav_partial.replace(wav)


def speech_filters(channels: int) -> str:
    parts = []
    if channels >= 2:
        parts.append("dialoguenhance=original=0.15:enhance=3:voice=8")
    parts.extend(["highpass=f=120", "lowpass=f=7600", "afftdn=nr=12:nf=-25"])
    return ",".join(parts)


def isolate_speech(source: Path, dest: Path, cancel) -> None:
    """Write a mono 16 kHz wav with the music bed pulled down. The MP3 is left as recorded."""
    if not ffmpeg_ready():
        raise VaaniError(
            "FFmpeg is not installed, so the speech cannot be separated from the music.",
            "audio_tool_missing",
            500,
            "retry",
        )
    partial = dest.with_name("speech.partial.wav")
    if partial.exists():
        partial.unlink()
    filters = speech_filters(probe_channels(source))
    try:
        _render_speech(source, partial, filters, cancel)
    except VaaniError:
        if "dialoguenhance" not in filters:
            raise
        partial.unlink(missing_ok=True)
        _render_speech(source, partial, speech_filters(1), cancel)
    partial.replace(dest)


def probe_channels(path: Path) -> int:
    if not shutil.which("ffprobe"):
        return 2
    result = subprocess.run(
        [
            "ffprobe",
            "-v",
            "error",
            "-select_streams",
            "a:0",
            "-show_entries",
            "stream=channels",
            "-of",
            "default=noprint_wrappers=1:nokey=1",
            str(path),
        ],
        capture_output=True,
        text=True,
        timeout=30,
        check=False,
    )
    try:
        channels = int((result.stdout or "").strip())
    except ValueError:
        return 2
    return channels if channels > 0 else 2


def _render_speech(source: Path, dest: Path, filters: str, cancel) -> None:
    run_command(
        [
            "ffmpeg",
            "-y",
            "-i",
            str(source),
            "-vn",
            "-af",
            filters,
            "-ac",
            "1",
            "-ar",
            "16000",
            "-c:a",
            "pcm_s16le",
            str(dest),
        ],
        cancel,
        timeout=1800,
    )


def wav_from_mp3(mp3: Path, wav: Path, cancel) -> None:
    render_target = wav.with_name("audio.partial.wav")
    if render_target.exists():
        render_target.unlink()
    run_command(
        [
            "ffmpeg",
            "-y",
            "-i",
            str(mp3),
            "-vn",
            "-ac",
            "1",
            "-ar",
            "16000",
            "-c:a",
            "pcm_s16le",
            str(render_target),
        ],
        cancel,
        timeout=1800,
    )
    render_target.replace(wav)


def find_named(directory: Path, prefix: str) -> Path | None:
    matches = []
    for path in directory.glob(f"{prefix}.*"):
        if path.suffix.lower() in {".part", ".tmp", ".ytdl"}:
            continue
        if path.name.endswith(".partial.mp3") or path.name.endswith(".partial.wav"):
            continue
        if path.is_file() and path.stat().st_size > 0:
            matches.append(path)
    matches.sort(key=lambda item: item.stat().st_mtime, reverse=True)
    return matches[0] if matches else None
