# -*- coding: utf-8 -*-
"""
FFmpeg 切分、去字幕、合并，与 Java ChunkSplitService / FFmpegService / ChunkMergeService 逻辑对齐。
"""
import logging
import os
import subprocess
import tempfile
import uuid
from pathlib import Path
from typing import List, Optional, Tuple

logger = logging.getLogger(__name__)


def _find_ffmpeg() -> str:
    return os.environ.get("FFMPEG_PATH", "ffmpeg")


def _find_ffprobe() -> str:
    return os.environ.get("FFPROBE_PATH", "ffprobe")


def check_ffmpeg_available() -> Optional[str]:
    """
    检测 ffmpeg / ffprobe 是否可用。可用则返回 None，不可用则返回错误说明（含安装提示）。
    """
    missing = []
    for name, getter in [("ffmpeg", _find_ffmpeg), ("ffprobe", _find_ffprobe)]:
        path = getter()
        try:
            r = subprocess.run(
                [path, "-version"],
                capture_output=True,
                text=True,
                timeout=5,
            )
            if r.returncode != 0:
                missing.append(name)
        except (FileNotFoundError, OSError):
            missing.append(name)
    if not missing:
        return None
    return (
        "未检测到 {}，请先安装 FFmpeg（本程序不会自动安装系统依赖）。\n"
        "  macOS:    brew install ffmpeg\n"
        "  Ubuntu:   sudo apt install ffmpeg\n"
        "  Windows:  从 https://ffmpeg.org/download.html 下载或将 ffmpeg/ffprobe 加入 PATH\n"
        "  或设置环境变量: FFMPEG_PATH=... FFPROBE_PATH=..."
    ).format(" / ".join(missing))


def _format_time(seconds: float) -> str:
    if seconds < 0:
        return "0"
    if seconds < 3600:
        m = int(seconds // 60)
        s = seconds % 60
        return f"{m}:{s:05.2f}" if s != int(s) else f"{m}:{int(s):02d}"
    h = int(seconds // 3600)
    m = int((seconds % 3600) // 60)
    s = seconds % 60
    return f"{h}:{m:02d}:{s:05.2f}" if s != int(s) else f"{h}:{m:02d}:{int(s):02d}"


def get_duration(video_path: str) -> float:
    cmd = [
        _find_ffprobe(),
        "-v", "error",
        "-show_entries", "format=duration",
        "-of", "default=noprint_wrappers=1:nokey=1",
        video_path,
    ]
    r = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
    if r.returncode != 0:
        raise RuntimeError(f"ffprobe 失败: {r.stderr or r.stdout}")
    line = (r.stdout or "").strip()
    if not line:
        return 0.0
    return float(line)


def get_video_size(video_path: str) -> Tuple[int, int]:
    """返回 (width, height)，用于去字幕滤镜。"""
    cmd = [
        _find_ffprobe(),
        "-v", "error",
        "-select_streams", "v:0",
        "-show_entries", "stream=width,height",
        "-of", "csv=p=0",
        video_path,
    ]
    r = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
    if r.returncode != 0:
        return 1920, 1080
    line = (r.stdout or "").strip()
    if not line:
        return 1920, 1080
    parts = line.split(",")
    if len(parts) >= 2:
        try:
            return int(parts[0]), int(parts[1])
        except ValueError:
            pass
    return 1920, 1080


def split_chunk(video_path: str, start_time: float, duration: float, output_path: str) -> None:
    cmd = [
        _find_ffmpeg(),
        "-ss", _format_time(start_time),
        "-i", video_path,
        "-t", _format_time(duration),
        "-c", "copy",
        "-avoid_negative_ts", "make_zero",
        "-y", output_path,
    ]
    run_ffmpeg(cmd)


def run_ffmpeg(cmd: List[str], timeout: Optional[int] = None) -> None:
    logger.debug("FFmpeg: %s", " ".join(cmd))
    p = subprocess.run(
        cmd,
        capture_output=True,
        text=True,
        timeout=timeout or 86400,
    )
    if p.returncode != 0:
        raise RuntimeError(f"FFmpeg 退出码 {p.returncode}: {p.stderr[:2000] if p.stderr else p.stdout}")


def build_video_filter(config: dict, original_width: int, original_height: int) -> str:
    """与 Java buildFilterFor1080pInput / buildFilterFor4KInput 对齐。"""
    target_width = config.get("targetWidth", 1920)
    target_height = config.get("targetHeight", 1080)
    crop_bottom = config.get("cropBottom", 0)
    if target_width <= 0 or target_height <= 0:
        target_width, target_height = 1920, 1080
    cropped_height = original_height - crop_bottom

    if original_width == 1920 and original_height == 1080:
        filters = []
        if crop_bottom > 0:
            filters.append(f"crop={original_width}:{cropped_height}:0:0")
            current_h = cropped_height
        else:
            current_h = original_height
        current_w = original_width
        aspect = current_w / current_h if current_h else 16 / 9
        if abs(aspect - 16 / 9) > 0.01:
            final_w = int(current_h * 16 / 9)
            crop_x = (current_w - final_w) // 2
            filters.append(f"crop={final_w}:{current_h}:{crop_x}:0")
            current_w = final_w
        if current_w != target_width or current_h != target_height:
            filters.append(f"scale={target_width}:{target_height}")
        return ",".join(filters) if filters else "null"

    # 4K/高分辨率：裁底 -> scale -> 裁左右 -> scale
    scaled_w = target_width
    scaled_h = int(cropped_height * scaled_w / original_width)
    final_crop_w = int(scaled_h * 16 / 9)
    crop_x = (scaled_w - final_crop_w) // 2
    return (
        f"crop={original_width}:{cropped_height}:0:0,"
        f"scale={scaled_w}:{scaled_h},"
        f"crop={final_crop_w}:{scaled_h}:{crop_x}:0,"
        f"scale={target_width}:{target_height}"
    )


def build_desubtitle_command(config: dict, input_path: str, output_path: str) -> List[str]:
    """构建去字幕 FFmpeg 命令（软件编码 libx264，服务器通用）。"""
    start_time = config.get("startTime", 0) or 0
    end_time = config.get("endTime", 0) or 0
    keep_audio = config.get("keepAudio", True)
    audio_bitrate = config.get("audioBitrate", 192)
    video_quality = config.get("videoQuality", 23)
    original_width = config.get("originalWidth", 0) or 1920
    original_height = config.get("originalHeight", 0) or 1080
    force_keyframe = config.get("forceKeyframeAtStart", False)

    cmd = [_find_ffmpeg()]
    if start_time > 0:
        cmd += ["-ss", _format_time(start_time)]
    cmd += ["-i", input_path]
    if end_time > 0 and start_time >= 0:
        duration = end_time - start_time
        if duration > 0:
            cmd += ["-t", _format_time(duration)]
    elif end_time > 0:
        cmd += ["-t", _format_time(end_time)]

    vf = build_video_filter(config, original_width, original_height)
    if vf and vf != "null":
        cmd += ["-vf", vf]
    cmd += ["-c:v", "libx264", "-preset", "medium", "-crf", str(video_quality)]
    if force_keyframe:
        cmd += ["-force_key_frames", "expr:eq(n,0)"]
    if keep_audio:
        cmd += ["-c:a", "aac", "-b:a", f"{audio_bitrate}k", "-ac", "2"]
    else:
        cmd += ["-an"]
    cmd += ["-y", output_path]
    return cmd


def run_desubtitle(config: dict, input_path: str, output_path: str, progress_callback=None) -> bool:
    cmd = build_desubtitle_command(config, input_path, output_path)
    run_ffmpeg(cmd)
    return True


def merge_with_concat(filelist_path: str, output_path: str) -> None:
    cmd = [
        _find_ffmpeg(),
        "-f", "concat", "-safe", "0",
        "-i", filelist_path,
        "-c", "copy",
        "-y", output_path,
    ]
    run_ffmpeg(cmd)


def trim_video(input_path: str, start_offset: float, duration: float, output_path: str) -> None:
    cmd = [
        _find_ffmpeg(),
        "-i", input_path,
        "-ss", _format_time(start_offset),
        "-t", _format_time(duration),
        "-c", "copy",
        "-y", output_path,
    ]
    run_ffmpeg(cmd)


def split_video_to_chunks(
    video_path: str,
    output_dir: str,
    chunk_size_sec: float,
    range_start: float,
    range_end: float,
) -> Tuple[dict, str]:
    """
    切分视频为多个 chunk，写入 output_dir/<video_id>/，返回 (metadata_dict, video_id)。
    """
    path = Path(video_path)
    if not path.exists():
        raise FileNotFoundError(f"视频不存在: {video_path}")
    duration = get_duration(video_path)
    if duration <= 0:
        raise ValueError(f"无法获取视频时长: {video_path}")

    start_sec = max(0, range_start)
    end_sec = min(duration, range_end) if range_end > 0 else duration
    if start_sec >= end_sec:
        raise ValueError("开始时间必须小于结束时间")
    effective = end_sec - start_sec

    video_id = path.stem + "_" + uuid.uuid4().hex[:8]
    chunk_dir = Path(output_dir) / video_id
    original_dir = chunk_dir / "original"
    original_dir.mkdir(parents=True, exist_ok=True)

    total_chunks = int(__import__("math").ceil(effective / chunk_size_sec))
    chunks = []
    for i in range(total_chunks):
        ch_start = start_sec + i * chunk_size_sec
        ch_end = min(start_sec + (i + 1) * chunk_size_sec, end_sec)
        ch_duration = ch_end - ch_start
        chunk_id = f"chunk_{i:03d}"
        out_path = original_dir / f"{chunk_id}.mp4"
        split_chunk(video_path, ch_start, ch_duration, str(out_path))
        rel_path = f"{video_id}/original/{chunk_id}.mp4"
        chunks.append({
            "chunkId": chunk_id,
            "startTime": ch_start,
            "endTime": ch_end,
            "originalPath": rel_path,
            "processedPath": "",
            "status": "pending",
            "processedAt": "",
            "errorMessage": "",
        })
        logger.info("切分完成: %s (%.1f - %.1f秒)", chunk_id, ch_start, ch_end)

    _now = __import__("datetime").datetime.utcnow()
    metadata = {
        "videoId": video_id,
        "originalPath": video_path,
        "chunkSize": chunk_size_sec,
        "totalChunks": total_chunks,
        "chunks": chunks,
        "createdAt": _now.isoformat() + "Z",
        "splitStartedAt": _now.isoformat() + "Z",
    }
    meta_path = chunk_dir / "metadata.json"
    with open(meta_path, "w", encoding="utf-8") as f:
        __import__("json").dump(metadata, f, indent=2, ensure_ascii=False)
    metadata["_metadataPath"] = str(meta_path)
    return metadata, video_id


def merge_chunks(metadata: dict, start_time: float, end_time: float, output_path: str) -> bool:
    """合并已处理的 chunk（按 startTime 排序，concat + 可选 trim）。processedPath 支持相对路径（相对 output_dir/video_id）或绝对路径。"""
    chunks = metadata.get("chunks") or []
    processed = [c for c in chunks if c.get("status") == "processed" and c.get("processedPath")]
    out_path = Path(output_path)
    video_id = metadata.get("videoId") or ""
    chunk_dir = out_path.parent / video_id if video_id else out_path.parent

    def resolve_path(c: dict) -> Path:
        raw = c["processedPath"]
        p = Path(raw)
        if p.is_absolute():
            return p.resolve()
        return (chunk_dir / raw).resolve()

    processed = [c for c in processed if resolve_path(c).exists()]
    if not processed:
        raise ValueError("没有可用的已处理小块")
    processed.sort(key=lambda c: c["startTime"])

    with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as f:
        for c in processed:
            p = resolve_path(c)
            # FFmpeg concat 列表：路径中单引号须转义为 '\''
            path_str = str(p).replace("'", "'\\''")
            f.write(f"file '{path_str}'\n")
        list_path = f.name
    try:
        tmp_concat = out_path.parent / f"chunk_merge_{os.getpid()}.mp4"
        tmp_trim = out_path.parent / f"chunk_trim_{os.getpid()}.mp4"
        try:
            merge_with_concat(list_path, str(tmp_concat))
            first_start = processed[0]["startTime"]
            trim_start = max(0, start_time - first_start)
            exact_duration = end_time - start_time
            trim_video(str(tmp_concat), trim_start, exact_duration, str(tmp_trim))
            tmp_trim.replace(out_path)
        finally:
            tmp_concat.unlink(missing_ok=True)
            tmp_trim.unlink(missing_ok=True)
        return True
    finally:
        os.unlink(list_path)
