"""FFmpeg wrapper for merging video with synced audio."""

import platform
import re
import subprocess
import sys
from pathlib import Path

from loguru import logger


def get_duration(file_path: Path) -> float:
    """Get duration of audio/video file in seconds."""
    cmd = [
        "ffprobe", "-v", "error",
        "-show_entries", "format=duration",
        "-of", "default=noprint_wrappers=1:nokey=1",
        str(file_path)
    ]
    result = subprocess.run(cmd, capture_output=True, text=True)
    if result.returncode != 0:
        raise RuntimeError(f"ffprobe failed: {result.stderr}")
    return float(result.stdout.strip())


def get_frame_rate(file_path: Path) -> float:
    """Get video frame rate. Returns 30 as fallback."""
    cmd = [
        "ffprobe", "-v", "error",
        "-select_streams", "v:0",
        "-show_entries", "stream=r_frame_rate",
        "-of", "default=noprint_wrappers=1:nokey=1",
        str(file_path)
    ]
    result = subprocess.run(cmd, capture_output=True, text=True)
    if result.returncode != 0:
        return 30.0
    try:
        # Frame rate is returned as "num/den" (e.g., "30/1")
        num, den = result.stdout.strip().split("/")
        return float(num) / float(den)
    except (ValueError, ZeroDivisionError):
        return 30.0


def get_video_encoder() -> tuple[str, list[str], list[str]]:
    """
    Get the best available video encoder and its options.
    Uses hardware acceleration on macOS (VideoToolbox) with software fallback.
    
    Returns:
        Tuple of (encoder_name, encoder_options, input_options)
    """
    if platform.system() == "Darwin":
        # macOS - try VideoToolbox hardware encoder
        result = subprocess.run(
            ["ffmpeg", "-hide_banner", "-encoders"],
            capture_output=True, text=True
        )
        if "h264_videotoolbox" in result.stdout:
            logger.info("Using hardware acceleration (VideoToolbox)")
            # Use hardware decoding + encoding
            input_opts = ["-hwaccel", "videotoolbox"]
            encoder_opts = ["-q:v", "65"]  # quality 0-100, 65 ≈ CRF 18
            return "h264_videotoolbox", encoder_opts, input_opts
    
    # Fallback to software encoder
    logger.info("Using software encoder (libx264)")
    return "libx264", ["-preset", "fast", "-crf", "18"], []


def _parse_time(time_str: str) -> float:
    """Parse ffmpeg time string (HH:MM:SS.xx) to seconds."""
    parts = time_str.split(":")
    return float(parts[0]) * 3600 + float(parts[1]) * 60 + float(parts[2])


def _run_ffmpeg_with_progress(cmd: list[str], duration: float) -> None:
    """Run ffmpeg command with live progress bar."""
    proc = subprocess.Popen(cmd, stderr=subprocess.PIPE, text=True)
    time_re = re.compile(r"time=(\d+:\d+:\d+\.\d+)")
    is_tty = sys.stderr.isatty()

    for line in proc.stderr:
        match = time_re.search(line)
        if match and duration > 0:
            current = _parse_time(match.group(1))
            pct = min(current / duration * 100, 100)
            if is_tty:
                bar_len = 30
                filled = int(bar_len * pct / 100)
                bar = "█" * filled + "░" * (bar_len - filled)
                sys.stderr.write(f"\r  Encoding {bar} {pct:.0f}%")
                sys.stderr.flush()

    if is_tty:
        sys.stderr.write("\r" + " " * 50 + "\r")
        sys.stderr.flush()

    proc.wait()
    if proc.returncode != 0:
        raise RuntimeError("FFmpeg encoding failed")


def merge(video_path: Path, audio_path: Path, output_path: Path, offset: float) -> None:
    """
    Merge video with new audio at the specified offset.
    
    Args:
        video_path: Source video file
        audio_path: Replacement audio file  
        output_path: Output video file
        offset: Time offset in seconds
            - offset > 0: delay the audio (audio starts later)
            - offset < 0: trim audio from start
    """
    # Get the duration of the new audio - output video will match this length
    audio_duration = get_duration(audio_path)
    logger.info(f"New audio duration: {audio_duration:.3f}s")
    logger.info(f"Merging with offset: {offset:.3f}s")
    
    # Get video frame rate and encoder
    fps = get_frame_rate(video_path)
    encoder, encoder_opts, input_opts = get_video_encoder()
    
    if offset >= 0:
        target_duration = audio_duration
        logger.info(f"Syncing: video from {offset:.3f}s, {fps:.0f}fps CFR")
        
        cmd = [
            "ffmpeg", "-y",
            *input_opts,                  # hardware decoding if available
            "-ss", str(offset),           # fast seek to offset (before -i)
            "-i", str(video_path),
            "-i", str(audio_path),        # audio from start
            "-map", "0:v:0",
            "-map", "1:a:0",
            # Convert VFR to CFR to prevent sync drift
            "-fps_mode", "cfr",
            "-r", str(int(round(fps))),
            "-c:v", encoder,
            *encoder_opts,
            "-c:a", "aac",
            "-b:a", "192k",
            "-t", str(target_duration),
            "-movflags", "+faststart",
            str(output_path)
        ]
        
    else:
        trim_audio = abs(offset)
        target_duration = audio_duration - trim_audio
        logger.info(f"Trimming audio: skipping first {trim_audio:.3f}s, {fps:.0f}fps CFR")
        
        cmd = [
            "ffmpeg", "-y",
            *input_opts,                  # hardware decoding if available
            "-i", str(video_path),
            "-ss", str(trim_audio),
            "-i", str(audio_path),
            "-map", "0:v:0",
            "-map", "1:a:0",
            "-fps_mode", "cfr",
            "-r", str(int(round(fps))),
            "-c:v", encoder,
            *encoder_opts,
            "-c:a", "aac",
            "-b:a", "192k",
            "-t", str(target_duration),
            "-movflags", "+faststart",
            str(output_path)
        ]
    
    _run_ffmpeg_with_progress(cmd, target_duration)
    
    logger.success(f"Created: {output_path}")
    logger.info(f"Size: {output_path.stat().st_size / 1024 / 1024:.1f} MB")


def check_ffmpeg() -> bool:
    """Check if FFmpeg is installed."""
    try:
        subprocess.run(["ffmpeg", "-version"], capture_output=True, check=True)
        return True
    except (subprocess.CalledProcessError, FileNotFoundError):
        return False
