#!/usr/bin/env python3
"""
视频解构脚本 - 将视频拆解为可复用的内容资产
自动提取：音频、ASR 文本（含逐字时间戳）、场景切分、关键帧图片。
输出结构化资产清单 JSON。

依赖：本地须安装 ffmpeg（brew install ffmpeg）

用法:
  python deconstruct_video.py --url "https://example.com/video.mp4"
  python deconstruct_video.py --local /path/to/video.mp4
  python deconstruct_video.py --url "https://example.com/video.mp4" --skip-asr
  python deconstruct_video.py --url "https://example.com/video.mp4" --json-output

环境变量:
  MM_API_BASE_URL  - 后端 API 地址（默认: https://api-agent.remixmate.ai/api）
  PRIV_TOKEN       - PrivToken 认证令牌
"""

import argparse
import json
import os
import re
import subprocess
import sys
import time
import urllib.error
import urllib.request

API_BASE_URL = os.environ.get("MM_API_BASE_URL", "https://api-agent.remixmate.ai/api")

PRIVATE_TOKEN = ""  # 在 main() 中通过 resolve_token() 初始化
SKILL_NAME = "video-parser"
AGENT_NAME = os.environ.get("AGENT_NAME", "")

# 是否静默模式（--json-output 时抑制进度输出）
QUIET = False


def log(msg: str) -> None:
    """打印进度信息（静默模式下不输出）"""
    if not QUIET:
        print(msg, flush=True)


def log_err(msg: str) -> None:
    """打印错误信息到 stderr（始终输出）"""
    print(msg, file=sys.stderr, flush=True)


def resolve_token(cli_token: str = None) -> str:
    """优先从 CLI 参数读取 token，其次环境变量，最后报错退出"""
    if cli_token:
        return cli_token
    token = os.environ.get("PRIV_TOKEN", "")
    if not token:
        if sys.stdin.isatty():
            token = input("请输入 PrivToken: ").strip()
        else:
            print("❌ PRIV_TOKEN env var not configured; cannot authenticate", file=sys.stderr)
            sys.exit(1)
    return token


def _build_headers() -> dict:
    """构建统一请求头（X-Priv-Token 认证头）"""
    headers = {
        "Content-Type": "application/json",
        "X-Priv-Token": PRIVATE_TOKEN,
        "x-invoke-skill": SKILL_NAME,
    }
    if AGENT_NAME:
        headers["x-invoke-agent"] = AGENT_NAME
    return headers


# ---------------------------------------------------------------------------
# 前置检查
# ---------------------------------------------------------------------------

def check_ffmpeg() -> None:
    """检查 ffmpeg 是否已安装"""
    try:
        subprocess.run(
            ["ffmpeg", "-version"],
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL,
            check=True,
        )
    except (FileNotFoundError, subprocess.CalledProcessError):
        log_err("ffmpeg 未安装。请先安装：brew install ffmpeg")
        sys.exit(1)


# ---------------------------------------------------------------------------
# 步骤 1：获取视频到本地
# ---------------------------------------------------------------------------

def download_video(url: str, output_dir: str) -> str:
    """下载远程视频到本地，返回本地文件路径"""
    # 从 URL 推断文件扩展名
    url_path = url.split("?")[0]
    ext = os.path.splitext(url_path)[1] or ".mp4"
    local_path = os.path.join(output_dir, f"source_video{ext}")

    log(f"[1/6] 正在下载视频: {url[:80]}{'...' if len(url) > 80 else ''}")
    try:
        req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
        with urllib.request.urlopen(req, timeout=300) as resp:
            with open(local_path, "wb") as f:
                while True:
                    chunk = resp.read(1024 * 1024)  # 1MB chunks
                    if not chunk:
                        break
                    f.write(chunk)
        file_size = os.path.getsize(local_path)
        log(f"   下载完成: {local_path} ({file_size / 1024 / 1024:.1f} MB)")
    except Exception as e:
        log_err(f"下载视频失败: {e}")
        sys.exit(1)

    return local_path


def get_video_duration_ms(video_path: str) -> int:
    """使用 ffprobe 获取视频时长（毫秒）"""
    try:
        result = subprocess.run(
            [
                "ffprobe", "-v", "error",
                "-show_entries", "format=duration",
                "-of", "default=noprint_wrappers=1:nokey=1",
                video_path,
            ],
            capture_output=True, text=True, check=True,
        )
        duration_sec = float(result.stdout.strip())
        return int(duration_sec * 1000)
    except Exception:
        return 0


# ---------------------------------------------------------------------------
# 步骤 2：提取音频
# ---------------------------------------------------------------------------

def extract_audio(video_path: str, output_dir: str) -> str:
    """从视频中提取音频为 MP3，返回音频文件路径"""
    audio_path = os.path.join(output_dir, "audio.mp3")
    log("[2/6] 正在提取音频...")

    try:
        subprocess.run(
            [
                "ffmpeg", "-y", "-i", video_path,
                "-vn", "-acodec", "libmp3lame", "-q:a", "2",
                audio_path,
            ],
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL,
            check=True,
        )
    except subprocess.CalledProcessError as e:
        log_err(f"音频提取失败: {e}")
        sys.exit(1)

    file_size = os.path.getsize(audio_path)
    log(f"   音频已保存: {audio_path} ({file_size / 1024:.1f} KB)")
    return audio_path


# ---------------------------------------------------------------------------
# 步骤 3：上传音频到 OSS
# ---------------------------------------------------------------------------

def upload_audio(audio_path: str) -> str:
    """上传音频文件到 OSS（POST /file/uploadMediaFile，multipart），返回文件 URL"""
    log("[3/6] 正在上传音频到 OSS...")

    url = f"{API_BASE_URL}/file/uploadMediaFile"
    boundary = f"----PythonBoundary{int(time.time() * 1000)}"
    filename = os.path.basename(audio_path)

    with open(audio_path, "rb") as f:
        file_data = f.read()

    # 构建 multipart/form-data body
    body = b""
    # file 字段
    body += f"--{boundary}\r\n".encode()
    body += f'Content-Disposition: form-data; name="file"; filename="{filename}"\r\n'.encode()
    body += b"Content-Type: audio/mpeg\r\n\r\n"
    body += file_data
    body += b"\r\n"
    # title 字段
    body += f"--{boundary}\r\n".encode()
    body += b'Content-Disposition: form-data; name="title"\r\n\r\n'
    body += filename.encode()
    body += b"\r\n"
    # 结束
    body += f"--{boundary}--\r\n".encode()

    headers = {
        "Content-Type": f"multipart/form-data; boundary={boundary}",
        "X-Priv-Token": PRIVATE_TOKEN,
        "x-invoke-skill": SKILL_NAME,
    }
    if AGENT_NAME:
        headers["x-invoke-agent"] = AGENT_NAME

    req = urllib.request.Request(url, data=body, headers=headers, method="POST")

    try:
        with urllib.request.urlopen(req, timeout=120) as resp:
            result = json.loads(resp.read().decode("utf-8"))
    except urllib.error.HTTPError as e:
        err_body = e.read().decode("utf-8")
        log_err(f"上传失败 (HTTP {e.code}): {err_body}")
        sys.exit(1)
    except urllib.error.URLError as e:
        log_err(f"上传网络错误: {e.reason}")
        sys.exit(1)

    if result.get("code") != 0:
        msg = result.get("msg") or result.get("message") or "未知错误"
        log_err(f"上传 API 返回错误: {msg}")
        sys.exit(1)

    file_url = (result.get("data") or {}).get("fileUrl", "")
    if not file_url:
        log_err(f"上传返回中无 fileUrl: {json.dumps(result, ensure_ascii=False)}")
        sys.exit(1)

    log(f"   上传完成: {file_url[:80]}...")
    return file_url


# ---------------------------------------------------------------------------
# 步骤 4：ASR 语音识别
# ---------------------------------------------------------------------------

def run_asr(audio_url: str) -> dict:
    """调用 ASR 接口（POST /tool/asr），返回识别结果"""
    log("[4/6] 正在进行语音识别 (ASR)...")

    url = f"{API_BASE_URL}/tool/asr"
    payload = {"audioUrl": audio_url}

    data = json.dumps(payload).encode("utf-8")
    req = urllib.request.Request(
        url, data=data, headers=_build_headers(), method="POST",
    )

    try:
        with urllib.request.urlopen(req, timeout=300) as resp:
            result = json.loads(resp.read().decode("utf-8"))
    except urllib.error.HTTPError as e:
        err_body = e.read().decode("utf-8")
        log_err(f"ASR 请求失败 (HTTP {e.code}): {err_body}")
        sys.exit(1)
    except urllib.error.URLError as e:
        log_err(f"ASR 网络错误: {e.reason}")
        sys.exit(1)

    if result.get("code") != 0:
        msg = result.get("msg") or result.get("message") or "未知错误"
        log_err(f"ASR API 返回错误: {msg}")
        sys.exit(1)

    asr_data = result.get("data") or {}
    text = asr_data.get("text", "")
    duration = asr_data.get("duration", 0)
    utterances = asr_data.get("utterances", [])

    log(f"   识别完成: {len(text)} 字, {len(utterances)} 句, 时长 {duration}ms")
    return {
        "text": text,
        "duration": duration,
        "utterances": utterances,
    }


# ---------------------------------------------------------------------------
# 步骤 5：场景切变检测 & 关键帧提取
# ---------------------------------------------------------------------------

def detect_scenes(video_path: str, threshold: float) -> list:
    """
    使用 ffmpeg select 滤镜检测场景切变，返回切点时间戳列表（秒）。
    始终包含 0.0 作为第一个切点。
    """
    log(f"[5/6] 正在检测场景切变 (阈值={threshold})...")

    try:
        result = subprocess.run(
            [
                "ffmpeg", "-i", video_path,
                "-vf", f"select='gt(scene,{threshold})',showinfo",
                "-vsync", "vfr",
                "-f", "null", "-",
            ],
            capture_output=True, text=True,
        )
        # 从 stderr 解析 pts_time
        output = result.stderr
    except Exception as e:
        log_err(f"场景检测失败: {e}")
        return [0.0]

    timestamps = []
    for match in re.finditer(r"pts_time:\s*([\d.]+)", output):
        ts = float(match.group(1))
        timestamps.append(ts)

    # 去重并排序，始终包含 0.0
    timestamps = sorted(set(timestamps))
    if not timestamps or timestamps[0] != 0.0:
        timestamps.insert(0, 0.0)

    log(f"   检测到 {len(timestamps)} 个场景切点")
    return timestamps


def extract_keyframes(video_path: str, timestamps: list, output_dir: str) -> list:
    """按时间戳列表从视频截取关键帧，返回关键帧信息列表"""
    keyframes_dir = os.path.join(output_dir, "keyframes")
    os.makedirs(keyframes_dir, exist_ok=True)

    log(f"[6/6] 正在提取 {len(timestamps)} 张关键帧...")

    keyframes = []
    for i, ts in enumerate(timestamps):
        frame_name = f"frame_{i + 1:03d}.jpg"
        frame_path = os.path.join(keyframes_dir, frame_name)

        try:
            subprocess.run(
                [
                    "ffmpeg", "-y",
                    "-ss", str(ts),
                    "-i", video_path,
                    "-frames:v", "1",
                    "-q:v", "2",
                    frame_path,
                ],
                stdout=subprocess.DEVNULL,
                stderr=subprocess.DEVNULL,
                check=True,
            )
            keyframes.append({
                "index": i,
                "timestampSec": round(ts, 3),
                "localPath": f"keyframes/{frame_name}",
            })
        except subprocess.CalledProcessError:
            log(f"   警告: 无法提取帧 @ {ts:.3f}s，跳过")

    log(f"   已提取 {len(keyframes)} 张关键帧")
    return keyframes


# ---------------------------------------------------------------------------
# 步骤 6：汇总输出
# ---------------------------------------------------------------------------

def build_scenes(timestamps: list, video_duration_sec: float, keyframes: list) -> list:
    """根据切点时间戳构建场景分段列表"""
    scenes = []
    keyframe_map = {kf["index"]: kf["localPath"] for kf in keyframes}

    for i, start in enumerate(timestamps):
        end = timestamps[i + 1] if i + 1 < len(timestamps) else video_duration_sec
        scenes.append({
            "index": i,
            "startTimeSec": round(start, 3),
            "endTimeSec": round(end, 3),
            "keyframe": keyframe_map.get(i, None),
        })

    return scenes


def build_result(
    source_url: str,
    local_video_path: str,
    video_duration_ms: int,
    audio_local_path: str,
    audio_oss_url: str,
    asr_result: dict,
    scenes: list,
    keyframes: list,
) -> dict:
    """汇总所有数据为最终资产清单"""
    result = {
        "source": {
            "url": source_url or None,
            "localPath": local_video_path,
            "durationMs": video_duration_ms,
        },
        "audio": {
            "localPath": audio_local_path,
            "url": audio_oss_url,
        },
        "asr": asr_result,
        "scenes": scenes,
        "keyframes": keyframes,
    }
    return result


# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------

def main():
    parser = argparse.ArgumentParser(
        description="Video deconstruction tool — split a video into reusable content assets",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  # Deconstruct from URL
  python deconstruct_video.py --url "https://example.com/video.mp4"

  # Deconstruct from a local file
  python deconstruct_video.py --local /path/to/video.mp4

  # Custom output directory and scene threshold
  python deconstruct_video.py --url "https://example.com/video.mp4" \\
    --output-dir ./my_output --scene-threshold 0.4

  # Skip ASR, only extract keyframes
  python deconstruct_video.py --local video.mp4 --skip-asr

  # JSON pipeline mode
  python deconstruct_video.py --url "https://example.com/video.mp4" --json-output
        """,
    )
    parser.add_argument(
        "--url",
        default=None,
        help="Remote video URL (mutually exclusive with --local)",
    )
    parser.add_argument(
        "--local",
        default=None,
        help="Local video path (mutually exclusive with --url)",
    )
    parser.add_argument(
        "-o", "--output-dir",
        default=None,
        help="Output directory (default: ./deconstructed_<timestamp>)",
    )
    parser.add_argument(
        "--scene-threshold",
        type=float,
        default=0.3,
        help="Scene-change threshold 0.0-1.0 (default: 0.3; lower = more sensitive)",
    )
    parser.add_argument(
        "--priv-token",
        metavar="TOKEN",
        default=None,
        help="PrivToken auth credential (priority: this flag > env var > interactive prompt)",
    )
    parser.add_argument(
        "--json-output",
        action="store_true",
        default=False,
        help="Print only JSON to stdout (pipeline mode, suppresses progress)",
    )
    parser.add_argument(
        "--skip-asr",
        action="store_true",
        default=False,
        help="Skip the ASR step",
    )
    parser.add_argument(
        "--skip-keyframes",
        action="store_true",
        default=False,
        help="Skip the keyframe-extraction step",
    )

    args = parser.parse_args()

    # 校验输入
    if not args.url and not args.local:
        log_err("请提供 --url 或 --local 参数指定视频来源")
        parser.print_help()
        sys.exit(1)
    if args.url and args.local:
        log_err("--url 和 --local 不能同时使用，请选择其一")
        sys.exit(1)
    if args.local and not os.path.isfile(args.local):
        log_err(f"本地视频文件不存在: {args.local}")
        sys.exit(1)
    if not (0.0 <= args.scene_threshold <= 1.0):
        log_err("--scene-threshold 范围为 0.0-1.0")
        sys.exit(1)

    # 静默模式
    global QUIET
    QUIET = args.json_output

    # 初始化 token（如果需要 ASR，则必须有 token）
    global PRIVATE_TOKEN
    if not args.skip_asr:
        PRIVATE_TOKEN = resolve_token(args.priv_token)
    else:
        # 即使跳过 ASR，如果提供了 token 也保存（可能用于其他扩展）
        PRIVATE_TOKEN = args.priv_token or os.environ.get("PRIV_TOKEN", "")

    # 前置检查
    check_ffmpeg()

    # 输出目录
    output_dir = args.output_dir or f"./deconstructed_{int(time.time())}"
    os.makedirs(output_dir, exist_ok=True)
    log(f"输出目录: {os.path.abspath(output_dir)}")

    # -----------------------------------------------------------------------
    # [1] 获取视频到本地
    # -----------------------------------------------------------------------
    source_url = args.url or ""
    if args.url:
        video_path = download_video(args.url, output_dir)
    else:
        video_path = os.path.abspath(args.local)
        log(f"[1/6] 使用本地视频: {video_path}")

    # 获取视频时长
    video_duration_ms = get_video_duration_ms(video_path)
    video_duration_sec = video_duration_ms / 1000.0 if video_duration_ms > 0 else 0.0
    log(f"   视频时长: {video_duration_sec:.1f}s ({video_duration_ms}ms)")

    # -----------------------------------------------------------------------
    # [2] 提取音频
    # -----------------------------------------------------------------------
    audio_path = extract_audio(video_path, output_dir)

    # -----------------------------------------------------------------------
    # [3-4] 上传音频 + ASR
    # -----------------------------------------------------------------------
    audio_oss_url = ""
    asr_result = {"text": "", "duration": 0, "utterances": []}

    if not args.skip_asr:
        audio_oss_url = upload_audio(audio_path)
        asr_result = run_asr(audio_oss_url)
    else:
        log("[3/6] 跳过音频上传（--skip-asr）")
        log("[4/6] 跳过 ASR（--skip-asr）")

    # -----------------------------------------------------------------------
    # [5-6] 场景检测 + 关键帧提取
    # -----------------------------------------------------------------------
    scenes = []
    keyframes = []

    if not args.skip_keyframes:
        timestamps = detect_scenes(video_path, args.scene_threshold)
        keyframes = extract_keyframes(video_path, timestamps, output_dir)
        scenes = build_scenes(timestamps, video_duration_sec, keyframes)
    else:
        log("[5/6] 跳过场景检测（--skip-keyframes）")
        log("[6/6] 跳过关键帧提取（--skip-keyframes）")

    # -----------------------------------------------------------------------
    # 汇总输出
    # -----------------------------------------------------------------------
    final = build_result(
        source_url=source_url,
        local_video_path=video_path,
        video_duration_ms=video_duration_ms,
        audio_local_path=audio_path,
        audio_oss_url=audio_oss_url,
        asr_result=asr_result,
        scenes=scenes,
        keyframes=keyframes,
    )

    # 写入 JSON
    json_path = os.path.join(output_dir, "deconstruction.json")
    with open(json_path, "w", encoding="utf-8") as f:
        json.dump(final, f, ensure_ascii=False, indent=2)

    if args.json_output:
        # 管道模式：仅输出 JSON 到 stdout
        print(json.dumps(final, ensure_ascii=False, indent=2))
    else:
        log(f"\n资产清单已保存: {json_path}")
        log(f"音频文件: {audio_path}")
        if keyframes:
            log(f"关键帧: {len(keyframes)} 张 → {output_dir}/keyframes/")
        if asr_result.get("text"):
            text_preview = asr_result["text"][:60]
            log(f"ASR 文本: {text_preview}{'...' if len(asr_result['text']) > 60 else ''}")
        log("\n视频解构完成！")


if __name__ == "__main__":
    main()
