#!/usr/bin/env python3
"""录屏产物收尾客户端 —— 把本地视频推给 ab-render 的 /finalize 端点。

ab-render 侧（apps/ab-render/server/finalize.ts）负责：webm→mp4 转码 + 抽首帧封面
+ 上传阿里云 VOD + 轮询拿到 CDN 播放地址。本模块只做 HTTP 层：POST 原始字节 +
轮询 /finalizeStatus。纯标准库，不依赖 ffmpeg / OSS / 阿里云 SDK。

与 render-video 的 remote_renderer_client 一样直连 ab-render：
  Base URL 优先级：显式传入 > 环境变量 REMOTION_RENDER_API_URL > 默认生产地址。

认证头（复用 X-Priv-Token 规范）：
  X-Priv-Token: <PRIV_TOKEN>
  x-invoke-skill:  web-record
  x-invoke-agent:  <AGENT_NAME>   # optional
  x-conversation-id: <CONVERSATION_ID>  # optional
"""
from __future__ import annotations

import json
import os
import sys
import time
import urllib.error
import urllib.parse
import urllib.request
from pathlib import Path
from typing import Callable, Optional

DEFAULT_API_BASE_URL = "https://api-render.remixmate.ai"
SKILL_NAME = "web-record"

TERMINAL_SUCCESS = {"succeeded", "succeed", "completed"}
TERMINAL_FAILURE = {"failed", "cancelled"}


class FinalizeError(RuntimeError):
    """上传/收尾失败，交由调用方决定是否降级为「仅本地文件」。"""


def _base_url(explicit: Optional[str]) -> str:
    base = (
        explicit
        or os.environ.get("REMOTION_RENDER_API_URL")
        or os.environ.get("RENDER_API_URL")
        or DEFAULT_API_BASE_URL
    ).strip()
    return base.rstrip("/")


def _headers(private_token: str, *, content_type: Optional[str], conversation_id: Optional[str]) -> dict:
    if not private_token:
        raise FinalizeError("PrivToken 未设置（PRIV_TOKEN）")
    h = {"X-Priv-Token": private_token, "x-invoke-skill": SKILL_NAME}
    if content_type:
        h["Content-Type"] = content_type
    agent = os.environ.get("AGENT_NAME", "")
    if agent:
        h["x-invoke-agent"] = agent
    if conversation_id:
        h["x-conversation-id"] = conversation_id
    return h


def _submit(
    local_path: str,
    *,
    private_token: str,
    base_url: str,
    transcode: bool,
    cover: bool,
    cover_at_sec: float,
    title: Optional[str],
    resolve_cdn: bool,
    keep_tail_sec: Optional[float],
    conversation_id: Optional[str],
    request_timeout: float,
) -> str:
    """POST /finalize（原始字节 + query options）→ 返回 taskId。"""
    ext = Path(local_path).suffix.lstrip(".").lower() or "webm"
    query = {
        "transcode": "1" if transcode else "0",
        "cover": "1" if cover else "0",
        "coverAtSec": str(cover_at_sec),
        "resolveCdn": "1" if resolve_cdn else "0",
        "ext": ext,
    }
    if title:
        query["title"] = title
    # 端锚裁剪窗口：让 ab-render 在转码同一 pass 里切掉开头加载段（skill 侧不再裁）。
    if keep_tail_sec and keep_tail_sec > 0:
        query["keepTailSec"] = f"{keep_tail_sec:.3f}"
    url = f"{base_url}/finalize?{urllib.parse.urlencode(query)}"

    with open(local_path, "rb") as f:
        body = f.read()

    req = urllib.request.Request(
        url,
        data=body,
        headers=_headers(private_token, content_type="application/octet-stream", conversation_id=conversation_id),
        method="POST",
    )
    try:
        with urllib.request.urlopen(req, timeout=request_timeout) as resp:
            result = json.loads(resp.read().decode("utf-8"))
    except urllib.error.HTTPError as exc:
        detail = ""
        try:
            detail = exc.read().decode("utf-8")[:200]
        except Exception:
            pass
        raise FinalizeError(f"/finalize HTTP {exc.code}: {detail}") from exc
    except (urllib.error.URLError, OSError) as exc:
        raise FinalizeError(f"/finalize 网络错误: {exc}") from exc

    if result.get("code") != 0:
        raise FinalizeError(f"/finalize code={result.get('code')}: {result.get('msg')}")
    task_id = (result.get("data") or {}).get("taskId")
    if not task_id:
        raise FinalizeError(f"/finalize 未返回 taskId: {result}")
    return str(task_id)


def _poll(
    task_id: str,
    *,
    private_token: str,
    base_url: str,
    timeout: float,
    interval: float,
    request_timeout: float,
    on_progress: Optional[Callable[[dict], None]],
) -> dict:
    """POST /finalizeStatus 轮询直到 succeeded/failed/超时。成功返回 data dict。"""
    start = time.monotonic()
    consecutive_errors = 0
    while True:
        if time.monotonic() - start > timeout:
            raise FinalizeError(
                f"/finalizeStatus 轮询超时（{timeout:.0f}s, taskId={task_id}）；任务可能仍在后端运行"
            )
        try:
            req = urllib.request.Request(
                f"{base_url}/finalizeStatus",
                data=json.dumps({"taskId": task_id}).encode("utf-8"),
                headers=_headers(private_token, content_type="application/json", conversation_id=None),
                method="POST",
            )
            with urllib.request.urlopen(req, timeout=request_timeout) as resp:
                result = json.loads(resp.read().decode("utf-8"))
            consecutive_errors = 0
        except (urllib.error.URLError, OSError, ConnectionError) as exc:
            consecutive_errors += 1
            if consecutive_errors >= 5:
                raise FinalizeError(f"/finalizeStatus 连续网络错误，放弃（taskId={task_id}）: {exc}") from exc
            time.sleep(interval)
            continue

        if result.get("code") != 0:
            raise FinalizeError(f"/finalizeStatus code={result.get('code')}: {result.get('msg')}")
        data = result.get("data") or {}
        if on_progress is not None:
            try:
                on_progress(data)
            except Exception:
                pass

        status = str(data.get("status") or "").lower()
        if status in TERMINAL_SUCCESS:
            return data
        if status in TERMINAL_FAILURE:
            raise FinalizeError(f"收尾失败（{status}）: {data.get('error') or 'unknown'}")
        time.sleep(interval)


def finalize_recording(
    local_path: str,
    *,
    private_token: str,
    transcode: bool = True,
    cover: bool = True,
    cover_at_sec: float = 0.5,
    title: Optional[str] = None,
    resolve_cdn: bool = True,
    keep_tail_sec: Optional[float] = None,
    base_url: Optional[str] = None,
    conversation_id: Optional[str] = None,
    on_progress: Optional[Callable[[dict], None]] = None,
    poll_timeout: float = 300.0,
    poll_interval: float = 3.0,
    request_timeout: float = 300.0,
) -> dict:
    """把本地视频推给 ab-render 收尾并轮询到终态。

    keep_tail_sec：端锚裁剪窗口（保留视频尾部这么多秒，切掉开头加载段）；由 skill 测量的
    内容窗口得出，None 表示不裁。裁剪与转码在 ab-render 合并成一次 pass。

    返回 finalizeStatus 的 data dict，关键字段：
      fileUrl（CDN 或 vod://）、vodRef、fileId、coverUrl、durationSec。
    失败抛 FinalizeError，由调用方决定降级。
    """
    base = _base_url(base_url)
    # 诊断：明确打出实际请求的 ab-render 地址 + 裁剪窗口，便于排查「发去哪/有没有裁」。
    print(
        f"[record] finalize target: {base}  keepTailSec={keep_tail_sec}",
        file=sys.stderr,
        flush=True,
    )
    task_id = _submit(
        local_path,
        private_token=private_token,
        base_url=base,
        transcode=transcode,
        cover=cover,
        cover_at_sec=cover_at_sec,
        title=title,
        resolve_cdn=resolve_cdn,
        keep_tail_sec=keep_tail_sec,
        conversation_id=conversation_id,
        request_timeout=request_timeout,
    )
    return _poll(
        task_id,
        private_token=private_token,
        base_url=base,
        timeout=poll_timeout,
        interval=poll_interval,
        request_timeout=60.0,
        on_progress=on_progress,
    )


__all__ = ["finalize_recording", "FinalizeError"]
