# -*- coding: utf-8 -*-
"""
Python Worker：从 queue 目录读取任务并执行切分/去字幕/合成，与 Java BackendWorker 行为一致。
用法:
  python worker.py [--data-dir DIR] [--path-replace OLD=NEW] [--workers N]
  或设置环境变量: VIDEOCONVERTER_DATA_DIR, VIDEOCONVERTER_PATH_REPLACE (OLD=NEW)
"""
import argparse
import logging
import sys
import time
from pathlib import Path

from task_queue import QueueStore
from metadata import load_metadata, get_processed_chunks, get_pending_chunks, update_chunk_processed
from ffmpeg_runner import (
    split_video_to_chunks,
    run_desubtitle,
    merge_chunks,
    get_duration,
    get_video_size,
    check_ffmpeg_available,
)

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S",
)
logger = logging.getLogger("worker")


def process_split_task(store: QueueStore, task: dict) -> None:
    task_id = task["task_id"]
    input_file = task["input_file"]
    output_dir = task["output_dir"]
    config = task.get("config") or {}
    range_start = config.get("startTime", 0) or 0
    range_end = config.get("endTime", 0) or 0

    store.add_log(task_id, "INFO", f"开始切分视频: {Path(input_file).name} (范围: {range_start} - {range_end}秒)")
    metadata, video_id = split_video_to_chunks(input_file, output_dir, 120.0, range_start, range_end)
    store.add_log(task_id, "INFO", f"切分完成: {metadata['totalChunks']} 个小块，元数据: {metadata.get('_metadataPath', '')}")

    store.complete_task(task_id)

    # 为每个 chunk 创建去字幕任务
    chunk_files = []
    for ch in metadata.get("chunks") or []:
        rel = ch.get("originalPath", "")
        if rel:
            chunk_path = Path(output_dir) / rel
            chunk_files.append(str(chunk_path))

    for chunk_file in chunk_files:
        store.create_desubtitle_task(chunk_file, output_dir, config, task_id, video_id)
    store.add_log(task_id, "INFO", f"已创建 {len(chunk_files)} 个去字幕任务")


def process_desubtitle_task(store: QueueStore, task: dict) -> None:
    task_id = task["task_id"]
    input_file = task["input_file"]
    output_dir = task["output_dir"]
    video_id = task.get("video_id") or ""
    config = dict(task.get("config") or {})

    input_path = Path(input_file)
    if not input_path.exists():
        store.fail_task(task_id, f"输入文件不存在: {input_file}")
        return

    store.add_log(task_id, "INFO", f"开始去字幕: {input_path.name}")

    output_dir_for_file = Path(output_dir) / video_id if video_id else Path(output_dir)
    output_dir_for_file.mkdir(parents=True, exist_ok=True)
    output_name = input_path.stem + "_desub.mp4"
    output_file = output_dir_for_file / output_name

    config["inputPath"] = input_file
    config["outputPath"] = str(output_file)
    if video_id:
        config["startTime"] = 0
        config["endTime"] = 0
        config["forceKeyframeAtStart"] = True

    try:
        run_desubtitle(config, input_file, str(output_file))
    except Exception as e:
        store.fail_task(task_id, str(e))
        store.add_log(task_id, "WARN", str(e))
        return

    store.complete_task(task_id)
    store.add_log(task_id, "INFO", f"去字幕完成: {output_name}")

    if video_id:
        metadata_path = Path(output_dir) / video_id / "metadata.json"
        if metadata_path.exists():
            chunk_id = input_path.stem
            try:
                update_chunk_processed(str(metadata_path), chunk_id, str(output_file))
                check_and_create_merge_task(store, video_id, output_dir, config)
            except Exception as e:
                logger.warning("更新 metadata 或检查合成失败: videoId=%s, chunkId=%s, %s", video_id, chunk_id, e)


def _merge_creation_lock(store: QueueStore, video_id: str):
    """对同一 video_id 串行化「检查并创建 MERGE」，避免多进程同时建两个 MERGE。"""
    from contextlib import contextmanager
    safe_id = (video_id or "").replace("/", "_").replace("\\", "_")[:64]
    lock_path = store.queue_dir / f".merge_{safe_id}.lock"
    lock_path.parent.mkdir(parents=True, exist_ok=True)

    @contextmanager
    def _held():
        try:
            import fcntl
            f = open(lock_path, "w")
            try:
                fcntl.flock(f.fileno(), fcntl.LOCK_EX)
                yield
            finally:
                fcntl.flock(f.fileno(), fcntl.LOCK_UN)
                f.close()
        except ImportError:
            yield  # Windows 无 fcntl，接受可能的重复 MERGE

    return _held()


def check_and_create_merge_task(store: QueueStore, video_id: str, output_dir: str, config: dict) -> None:
    metadata_path = Path(output_dir) / video_id / "metadata.json"
    if not metadata_path.exists():
        return
    data = load_metadata(str(metadata_path))
    chunks = data.get("chunks") or []
    total = len(chunks)
    processed = get_processed_chunks(data)
    pending = get_pending_chunks(data)
    if total == 0 or len(processed) != total or pending:
        return
    with _merge_creation_lock(store, video_id):
        if store.has_merge_task_for_video_id(video_id):
            return
        store.create_merge_task(video_id, output_dir, config)
        logger.info("自动创建合成任务: videoId=%s, 已处理 %d/%d 块", video_id, len(processed), total)


def _format_duration(sec: float) -> str:
    """不足1分钟用秒，超过60秒用分钟，超过60分钟用小时。"""
    if sec < 60:
        return f"{sec:.1f}秒" if sec != int(sec) else f"{int(sec)}秒"
    if sec < 3600:
        m = int(sec // 60)
        s = int(round(sec % 60))
        return f"{m}分{s}秒" if s else f"{m}分"
    h = int(sec // 3600)
    m = int((sec % 3600) // 60)
    s = int(round(sec % 60))
    if m and s:
        return f"{h}小时{m}分{s}秒"
    if m:
        return f"{h}小时{m}分"
    if s:
        return f"{h}小时{s}秒"
    return f"{h}小时"


def _log_split_to_merge_duration(data: dict, task_id: str, store: QueueStore) -> None:
    """若 metadata 含 splitStartedAt，则计算并输出从切分到合成结束的总时长。"""
    s = data.get("splitStartedAt") or ""
    if not s:
        return
    try:
        from datetime import datetime, timezone
        ts = datetime.fromisoformat(s.replace("Z", "+00:00"))
        if ts.tzinfo is None:
            ts = ts.replace(tzinfo=timezone.utc)
        duration_sec = (datetime.now(timezone.utc) - ts).total_seconds()
        if duration_sec >= 0:
            msg = f"从切分到合成结束总时长: {_format_duration(duration_sec)}"
            logger.info("videoId=%s %s", data.get("videoId", ""), msg)
            store.add_log(task_id, "INFO", msg)
    except Exception:
        pass


def process_merge_task(store: QueueStore, task: dict) -> None:
    task_id = task["task_id"]
    video_id = task.get("video_id")
    output_dir = task["output_dir"]

    store.add_log(task_id, "INFO", f"开始合成视频: videoId={video_id}")

    metadata_path = Path(output_dir) / video_id / "metadata.json"
    if not metadata_path.exists():
        store.fail_task(task_id, f"元数据文件不存在: {metadata_path}")
        return

    data = load_metadata(str(metadata_path))
    processed = get_processed_chunks(data)
    if not processed:
        store.fail_task(task_id, "没有已处理的 chunk")
        return

    start_time = processed[0]["startTime"]
    end_time = processed[-1]["endTime"]
    output_file = Path(output_dir) / f"{video_id}_merged.mp4"

    store.add_log(task_id, "INFO", f"合并 {len(processed)} 个小块，时间范围: {start_time} - {end_time}秒")

    try:
        merge_chunks(data, start_time, end_time, str(output_file))
        store.complete_task(task_id)
        store.add_log(task_id, "INFO", f"合成完成: {output_file.name}")
        # 从切分到合成结束总时长（若 metadata 含 splitStartedAt）
        _log_split_to_merge_duration(data, task_id, store)
    except Exception as e:
        store.fail_task(task_id, str(e))
        store.add_log(task_id, "WARN", str(e))


def process_one_click_compose_task(store: QueueStore, task: dict) -> None:
    task_id = task["task_id"]
    input_file = task["input_file"]
    output_dir = task["output_dir"]
    config = task.get("config") or {}
    range_start = config.get("startTime", 0) or 0
    range_end = config.get("endTime", 0) or 0

    store.add_log(task_id, "INFO", f"开始一键合成: {Path(input_file).name} (videoId={task.get('video_id', '')})")

    metadata, folder_video_id = split_video_to_chunks(input_file, output_dir, 120.0, range_start, range_end)
    store.add_log(task_id, "INFO", f"切分完成: {metadata['totalChunks']} 个小块")

    store.complete_task(task_id)

    chunk_files = []
    for ch in metadata.get("chunks") or []:
        rel = ch.get("originalPath", "")
        if rel:
            chunk_files.append(str(Path(output_dir) / rel))

    for cf in chunk_files:
        store.create_desubtitle_task(cf, output_dir, config, task_id, folder_video_id)
    store.add_log(task_id, "INFO", f"已创建 {len(chunk_files)} 个去字幕任务")


def work_loop(store: QueueStore) -> None:
    while True:
        try:
            if store.is_paused():
                time.sleep(1)
                continue

            task = store.acquire_pending_task()
            if task is None:
                time.sleep(1)
                continue

            task_id = task["task_id"]
            task_type = task.get("task_type", "")
            logger.info("开始处理任务: %s (类型: %s)", task_id, task_type)

            try:
                if task_type == "SPLIT":
                    process_split_task(store, task)
                elif task_type == "DESUBTITLE":
                    process_desubtitle_task(store, task)
                elif task_type == "MERGE":
                    process_merge_task(store, task)
                elif task_type == "ONE_CLICK_COMPOSE":
                    process_one_click_compose_task(store, task)
                else:
                    store.fail_task(task_id, f"未知任务类型: {task_type}")
            except Exception as e:
                logger.exception("任务处理失败: %s", task_id)
                store.fail_task(task_id, str(e))
                store.add_log(task_id, "WARN", str(e))

        except KeyboardInterrupt:
            logger.info("收到中断，退出")
            break
        except Exception as e:
            logger.exception("工作循环异常: %s", e)
            time.sleep(5)


def run_simple_compose(
    input_file: str,
    output_dir: str,
    start_sec: float,
    end_sec: float,
    crop_bottom: int = 208,
    crop_left: int = 293,
    crop_right: int = 293,
) -> str:
    """
    简易模式：在当前目录/指定目录内，对单个视频做「切分→去字幕→合成」全流程，不经过队列。
    返回最终合成文件路径；异常时抛出。
    """
    input_path = Path(input_file)
    if not input_path.exists():
        raise FileNotFoundError(f"文件不存在: {input_file}")
    if end_sec <= 0:
        end_sec = get_duration(input_file)
    start_sec = max(0.0, start_sec)
    if start_sec >= end_sec:
        raise ValueError("开始时间必须小于结束时间")

    width, height = get_video_size(input_file)
    config = {
        "targetWidth": 1920,
        "targetHeight": 1080,
        "cropTop": 0,
        "cropBottom": crop_bottom,
        "cropLeft": crop_left,
        "cropRight": crop_right,
        "keepAudio": True,
        "audioBitrate": 192,
        "videoQuality": 23,
        "originalWidth": width,
        "originalHeight": height,
        "startTime": start_sec,
        "endTime": end_sec,
        "useHardwareAccel": False,
    }

    logger.info("简易模式: 切分 %s (%.0f - %.0f秒), 字幕高度(裁底)=%d", input_path.name, start_sec, end_sec, crop_bottom)
    t0 = time.time()
    metadata, video_id = split_video_to_chunks(input_file, output_dir, 120.0, start_sec, end_sec)
    chunk_list = [c for c in (metadata.get("chunks") or []) if c.get("originalPath")]
    logger.info("切分完成: %d 块，开始去字幕", len(chunk_list))

    for ch in chunk_list:
        rel = ch.get("originalPath", "")
        if not rel:
            continue
        chunk_path = Path(output_dir) / rel
        if not chunk_path.exists():
            continue
        chunk_id = ch.get("chunkId", "")
        out_dir_v = Path(output_dir) / video_id
        out_dir_v.mkdir(parents=True, exist_ok=True)
        output_file = out_dir_v / (Path(chunk_path).stem + "_desub.mp4")
        cfg = dict(config)
        cfg["inputPath"] = str(chunk_path)
        cfg["outputPath"] = str(output_file)
        cfg["startTime"] = 0
        cfg["endTime"] = 0
        cfg["forceKeyframeAtStart"] = True
        run_desubtitle(cfg, str(chunk_path), str(output_file))
        meta_path = Path(output_dir) / video_id / "metadata.json"
        if meta_path.exists():
            update_chunk_processed(str(meta_path), chunk_id, str(output_file))

    data = load_metadata(str(Path(output_dir) / video_id / "metadata.json"))
    processed = get_processed_chunks(data)
    if not processed:
        raise RuntimeError("没有已处理的 chunk")
    start_t = processed[0]["startTime"]
    end_t = processed[-1]["endTime"]
    out_file = Path(output_dir) / f"{video_id}_merged.mp4"
    merge_chunks(data, start_t, end_t, str(out_file))
    elapsed = time.time() - t0
    logger.info("简易模式完成: %s，从切分到合成结束总时长: %s", out_file, _format_duration(elapsed))
    return str(out_file)


def main() -> int:
    parser = argparse.ArgumentParser(
        description="VideoConverter Python Worker：从队列执行任务，或简易模式单文件/批量处理。",
        epilog="""
简易模式示例（不经过队列，指定开始/结束时间、字幕高度，便于测试）:
  单文件:  videoconverter --simple --dir . --start 0 --end 120 --crop-bottom 208 --file 视频.mp4
  批量:    videoconverter --simple --dir . --start 0 --end 60 --crop-bottom 208 --batch
  参数:    --dir 工作目录  --start/--end 秒(0=到结尾)  --crop-bottom 字幕高度  --output 输出目录
""",
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    parser.add_argument(
        "--data-dir",
        default=None,
        help="数据目录（默认 $VIDEOCONVERTER_DATA_DIR 或 ~/.videoconverter/data）",
    )
    parser.add_argument(
        "--path-replace",
        default=None,
        help="路径前缀替换，便于本机任务拷到服务器: OLD=NEW，如 /Users/me/videos=/data/videos",
    )
    parser.add_argument(
        "--workers",
        type=int,
        default=1,
        help="并发 worker 数（默认 1，多进程时由外部起多个 worker 进程）",
    )
    parser.add_argument(
        "--show-schema",
        action="store_true",
        help="打印任务/队列输入格式说明（对接方如何喂数据）",
    )
    parser.add_argument(
        "--simple",
        action="store_true",
        help="简易模式：指定目录内输入开始/结束时间、字幕高度，单文件或批量处理（便于测试）",
    )
    parser.add_argument(
        "--dir",
        default=".",
        help="简易模式：工作目录，默认当前目录",
    )
    parser.add_argument(
        "--start",
        type=float,
        default=0.0,
        help="简易模式：开始时间（秒）",
    )
    parser.add_argument(
        "--end",
        type=float,
        default=0,
        help="简易模式：结束时间（秒），0 表示到视频结尾",
    )
    parser.add_argument(
        "--crop-bottom",
        type=int,
        default=208,
        help="简易模式：字幕高度（裁底像素），默认 208",
    )
    parser.add_argument(
        "--crop-left",
        type=int,
        default=293,
        help="简易模式：左侧裁剪像素，默认 293",
    )
    parser.add_argument(
        "--crop-right",
        type=int,
        default=293,
        help="简易模式：右侧裁剪像素，默认 293",
    )
    parser.add_argument(
        "--file",
        default=None,
        help="简易模式：指定一个视频文件（否则需用 --batch）",
    )
    parser.add_argument(
        "--batch",
        action="store_true",
        help="简易模式：批量处理当前目录下所有视频（.mp4/.mkv/.mov）",
    )
    parser.add_argument(
        "--output",
        default=None,
        help="简易模式：输出目录，默认 <dir>/videoconverter_out",
    )
    args = parser.parse_args()

    if args.show_schema:
        from schema import INPUT_SCHEMA
        print(INPUT_SCHEMA)
        return 0

    err = check_ffmpeg_available()
    if err:
        logger.error("%s", err)
        return 1

    if args.simple:
        base = Path(args.dir).resolve()
        if not base.is_dir():
            logger.error("目录不存在: %s", base)
            return 1
        out_dir = Path(args.output).resolve() if args.output else (base / "videoconverter_out")
        out_dir.mkdir(parents=True, exist_ok=True)
        if args.file:
            p = Path(args.file)
            files = [p.resolve() if p.is_absolute() else (base / p).resolve()]
            if not files[0].exists():
                logger.error("文件不存在: %s", files[0])
                return 1
        elif args.batch:
            exts = {".mp4", ".mkv", ".mov", ".avi", ".flv"}
            files = sorted([p for p in base.iterdir() if p.is_file() and p.suffix.lower() in exts])
            if not files:
                logger.error("目录下没有视频文件: %s", base)
                return 1
        else:
            logger.error("简易模式请指定 --file <路径> 或 --batch")
            return 1
        failed = 0
        for f in files:
            try:
                run_simple_compose(
                    str(f),
                    str(out_dir),
                    args.start,
                    args.end,
                    crop_bottom=args.crop_bottom,
                    crop_left=args.crop_left,
                    crop_right=args.crop_right,
                )
            except Exception as e:
                logger.exception("处理失败 %s: %s", f.name, e)
                failed += 1
        return 1 if failed else 0

    data_dir = args.data_dir or __import__("os").environ.get(
        "VIDEOCONVERTER_DATA_DIR",
        str(Path.home() / ".videoconverter" / "data"),
    )
    path_replace = None
    if args.path_replace:
        if "=" in args.path_replace:
            a, b = args.path_replace.split("=", 1)
            path_replace = (a.strip(), b.strip())
    env_replace = __import__("os").environ.get("VIDEOCONVERTER_PATH_REPLACE")
    if env_replace and "=" in env_replace and path_replace is None:
        a, b = env_replace.split("=", 1)
        path_replace = (a.strip(), b.strip())

    store = QueueStore(data_dir, path_replace=path_replace)
    logger.info("数据目录: %s", data_dir)
    logger.info("队列目录: %s", store.queue_dir)
    if path_replace:
        logger.info("路径替换: %s -> %s", path_replace[0], path_replace[1])

    # 断电/强制关机后：将无主 PROCESSING 任务重置为 PENDING，便于继续处理
    n = store.recover_orphaned_processing()
    if n > 0:
        logger.info("已恢复 %d 个孤儿任务（PROCESSING -> PENDING）", n)

    work_loop(store)
    return 0


if __name__ == "__main__":
    sys.exit(main())
