# LIB · Seedance 2.0 视频生成适配器 + 异步任务队列
# D180 · 2026-07-10
"""火山方舟 ARK → Seedance 2.0 · 提交+轮询+下载+写库 全自动"""
import json, os, subprocess, time, tempfile, base64

from lib.secrets import get_ark_key
ARK_KEY = get_ark_key()
ARK_BASE = "https://ark.cn-beijing.volces.com/api/v3"
SEEDANCE_MODEL = "doubao-seedance-2-0-260128"
TASK_URL = f"{ARK_BASE}/contents/generations/tasks"

# 成本估算（元/秒·720p）· 参考价，实际以火山方舟计费为准
COST_PER_SECOND = {"720p": 0.5, "1080p": 1.0}

def _curl(method, url, payload=None):
    """通用 curl 调用"""
    tf = None
    cmd = ["curl", "-s", url, "-H", f"Authorization: Bearer {ARK_KEY}"]
    if payload:
        tf = tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False, encoding='utf-8')
        json.dump(payload, tf, ensure_ascii=False)
        tf.close()
        cmd += ["-H", "Content-Type: application/json", "-d", f"@{tf.name}"]
    if method == "POST":
        cmd.insert(1, "-X")
        cmd.insert(2, "POST")
    try:
        r = subprocess.run(cmd, capture_output=True, text=True, timeout=15)
        if tf: os.unlink(tf.name)
        return json.loads(r.stdout) if r.stdout else {"error": r.stderr}
    except Exception as e:
        if tf: os.unlink(tf.name)
        return {"error": str(e)}

def submit_task(prompt, references=None, duration=6, resolution="720p"):
    """提交 Seedance 生成任务 → 返回 task_id"""
    content = [{"type": "text", "text": prompt}]
    if references:
        for ref_type, ref_path in references:
            with open(ref_path, "rb") as f:
                fmt = "png" if ref_path.endswith(".png") else "jpeg"
                b64 = base64.b64encode(f.read()).decode()
            content.append({
                "type": "image_url",
                "role": ref_type,
                "image_url": {"url": f"data:image/{fmt};base64,{b64}"}
            })
    payload = {
        "model": SEEDANCE_MODEL,
        "content": content,
        "duration": duration,
        "resolution": resolution
    }
    result = _curl("POST", TASK_URL, payload)
    if "id" in result:
        return result["id"]
    return {"error": result}

def poll_task(task_id, poll_interval=30, max_wait=600):
    """轮询任务状态 → 返回结果"""
    elapsed = 0
    while elapsed < max_wait:
        result = _curl("GET", f"{TASK_URL}/{task_id}")
        if "error" in result:
            return result
        status = result.get("status", "")
        if status == "succeeded":
            return result
        if status == "failed":
            return {"error": f"Task failed: {result}"}
        time.sleep(poll_interval)
        elapsed += poll_interval
    return {"error": "timeout"}

def download_video(video_url, output_path):
    """下载视频"""
    subprocess.run(["curl", "-s", "-o", output_path, video_url], check=False)
    return os.path.exists(output_path) and os.path.getsize(output_path) > 0

def generate_shot(shot_id, prompt, references, duration=6, output_dir="outputs/videos"):
    """一站式生成：提交→轮询→下载→返回结果"""
    print(f"[LIB] 提交 {shot_id}...")
    os.makedirs(output_dir, exist_ok=True)
    task_id = submit_task(prompt, references, duration)
    if isinstance(task_id, dict):
        return {"error": task_id}

    print(f"[LIB] 任务 {task_id[:20]}... 等待完成")
    result = poll_task(task_id)
    if "error" in result:
        return result

    video_url = result.get("content", {}).get("video_url")
    if not video_url:
        return {"error": "no video_url"}

    output_path = os.path.join(output_dir, f"{shot_id}.mp4")
    print(f"[LIB] 下载视频...")
    if download_video(video_url, output_path):
        return {
            "task_id": task_id,
            "output_path": output_path,
            "seed": result.get("seed"),
            "duration": result.get("duration"),
            "resolution": result.get("resolution"),
            "tokens": result.get("usage", {}).get("total_tokens", 0)
        }
    return {"error": "download failed"}

def generate_shot_async(shot_id, prompt, references, duration=6, output_dir="outputs/videos"):
    """异步提交：只提交不等待，返回 task_id 用于后续轮询"""
    task_id = submit_task(prompt, references, duration)
    if isinstance(task_id, dict):
        return task_id
    # 写入任务记录（含成本估算）
    from lib.models import get_db
    db = get_db()
    estimated_cost = round(duration * COST_PER_SECOND.get("720p", 0.5), 2)
    db.execute("""
        INSERT INTO generation_tasks (id, shot_id, task_type, seedance_task_id, status, prompt_used, references_used, cost_estimated, started_at)
        VALUES (?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))
    """, (task_id[:20], shot_id, "video", task_id, "running", prompt, json.dumps([r[0] for r in references] if references else []), estimated_cost))
    db.execute("UPDATE shots SET seedance_task_id = ?, status = 'generating' WHERE id = ?", (task_id, shot_id))
    db.commit()
    return {"task_id": task_id, "status": "submitted"}

def check_and_collect(shot_id):
    """检查任务状态，完成后自动下载"""
    from lib.models import get_db
    db = get_db()
    task = db.execute("""
        SELECT * FROM generation_tasks WHERE shot_id = ? ORDER BY created_at DESC LIMIT 1
    """, (shot_id,)).fetchone()
    if not task:
        return {"error": "no task found"}
    result = poll_task(task["seedance_task_id"], poll_interval=0, max_wait=1)
    if "error" in result:
        return result
    status = result.get("status", "")
    if status in ("running", "queued", "processing"):
        return {"status": "running", "task_id": task["seedance_task_id"]}
    if status != "succeeded":
        return {"error": f"Task {status}: {str(result)[:200]}"}

    # Succeeded - download
    video_url = result.get("content", {}).get("video_url")
    output_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "outputs", "videos")
    os.makedirs(output_dir, exist_ok=True)
    output_path = os.path.join(output_dir, f"{shot_id}.mp4")
    if download_video(video_url, output_path):
        db.execute("""
            UPDATE generation_tasks SET status = 'succeeded', cost_actual = ?, completed_at = datetime('now') WHERE id = ?
        """, (result.get("usage", {}).get("total_tokens", 0), task["id"],))
        # 保存seed
        seed = result.get("seed")
        if seed:
            db.execute("UPDATE shots SET status = 'done', output_path = ?, seed_value = ? WHERE id = ?",
                       (output_path, seed, shot_id))
        else:
            db.execute("UPDATE shots SET status = 'done', output_path = ? WHERE id = ?",
                       (output_path, shot_id))
        db.commit()
        return {"status": "done", "output_path": output_path, "seed": result.get("seed")}
    return {"error": "download failed"}

# ====== 批量并行生成 + 自动重试 ======

def batch_submit(episode_id, max_parallel=3, retry_failed=False):
    """批量提交：自动读库中所有 pending 镜头 → 并行提交 → 并行轮询
    
    支持 Ctrl+C 中断恢复：已提交的任务 ID 都在数据库中，
    中断后用 batch_resume() 继续收集。
    """
    from lib.models import get_db
    db = get_db()

    where = "episode_id = ? AND status IN ('pending', 'failed')" if retry_failed else "episode_id = ? AND status = 'pending'"
    shots = db.execute(f"SELECT * FROM shots WHERE {where} ORDER BY shot_number", (episode_id,)).fetchall()

    if not shots:
        print("⚠️ 没有待生成的镜头")
        return

    print(f"🚀 批量提交 {len(shots)} 个镜头（最多并行 {max_parallel}）")
    print(f"💡 可随时 Ctrl+C 中断，用 batch-resume 恢复\n")

    results = {}
    queue = list(shots)

    while queue or any(r.get("status") == "running" for r in results.values()):
        while len([r for r in results.values() if r.get("status") in ("submitted", "running")]) < max_parallel and queue:
            shot = queue.pop(0)
            shot_id = shot["id"]
            print(f"📤 提交 {shot_id}...")
            refs = db.execute("SELECT sa.role, a.file_path FROM shot_assets sa JOIN assets a ON sa.asset_id = a.id WHERE sa.shot_id = ?", (shot_id,)).fetchall()
            ref_paths = [(r["role"], r["file_path"]) for r in refs] if refs else []
            prompt = shot["prompt"] or shot["description"]
            r = generate_shot_async(shot_id, prompt, ref_paths, int(shot["duration_sec"] or 6))
            if "error" in r:
                results[shot_id] = {"status": "failed", "error": r["error"], "retries": 0}
                print(f"  ❌ {r['error']}")
            else:
                results[shot_id] = {"status": "running", "task_id": r["task_id"], "retries": 0}
                print(f"  ✅ {r['task_id'][:20]}...")

        for shot_id, info in list(results.items()):
            if info["status"] != "running": continue
            result = check_and_collect(shot_id)
            if result.get("status") == "done":
                results[shot_id] = {"status": "done", "output_path": result["output_path"]}
                print(f"✅ {shot_id} 完成")
            elif result.get("error") and "timeout" not in str(result.get("error", "")):
                info["retries"] += 1
                if info["retries"] <= 2:
                    print(f"🔄 {shot_id} 重试 ({info['retries']}/2)")
                    db.execute("UPDATE shots SET status = 'pending' WHERE id = ?", (shot_id,)); db.commit()
                    refs = db.execute("SELECT sa.role, a.file_path FROM shot_assets sa JOIN assets a ON sa.asset_id = a.id WHERE sa.shot_id = ?", (shot_id,)).fetchall()
                    ref_paths = [(r["role"], r["file_path"]) for r in refs]
                    shot = db.execute("SELECT * FROM shots WHERE id = ?", (shot_id,)).fetchone()
                    prompt = shot["prompt"] or shot["description"]
                    new_r = generate_shot_async(shot_id, prompt, ref_paths, int(shot["duration_sec"] or 6))
                    if "error" not in new_r:
                        results[shot_id] = {"status": "running", "task_id": new_r["task_id"], "retries": info["retries"]}
                    else:
                        results[shot_id] = {"status": "failed", "error": new_r["error"], "retries": info["retries"]}
                else:
                    results[shot_id] = {"status": "failed", "retries": info["retries"]}
                    db.execute("UPDATE shots SET status = 'failed', error_log = ? WHERE id = ?", (str(result.get("error", ""))[:200], shot_id)); db.commit()
                    print(f"❌ {shot_id} 重试{info['retries']}次后失败")
        time.sleep(30)

    done = sum(1 for r in results.values() if r["status"] == "done")
    failed = sum(1 for r in results.values() if r["status"] == "failed")
    print(f"\n📊 批量完成: {done}✅  {failed}❌  (共{len(results)})")
    return results


def batch_resume(episode_id):
    """从中断恢复：读数据库中所有 generating 状态的镜头 → 继续轮询收集
    
    适用于 batch_submit 中途 Ctrl+C 后恢复。
    已提交到 Seedance 的任务 ID 保存在 generation_tasks 表中，不会丢失。
    """
    from lib.models import get_db
    db = get_db()

    tasks = db.execute("""
        SELECT gt.shot_id, gt.seedance_task_id, s.shot_number
        FROM generation_tasks gt
        JOIN shots s ON gt.shot_id = s.id
        WHERE s.episode_id = ? AND s.status = 'generating'
        ORDER BY s.shot_number
    """, (episode_id,)).fetchall()

    if not tasks:
        print("⚠️ 没有正在生成的镜头，无需恢复")
        return

    print(f"🔄 恢复 {len(tasks)} 个正在生成的镜头...\n")

    results = {}
    for task in tasks:
        shot_id = task["shot_id"]
        print(f"📥 检查 {shot_id} (task: {task['seedance_task_id'][:20]}...)")
        result = check_and_collect(shot_id)
        if result.get("status") == "done":
            results[shot_id] = "done"
            print(f"  ✅ 已完成")
        elif result.get("status") == "running":
            results[shot_id] = "running"
            print(f"  🔄 仍在生成中")
        else:
            results[shot_id] = "unknown"
            print(f"  ❓ {result}")

    done = sum(1 for v in results.values() if v == "done")
    running = sum(1 for v in results.values() if v == "running")
    print(f"\n📊 恢复结果: {done}✅  {running}🔄  (共{len(results)})")
    if running > 0:
        print(f"💡 还有 {running} 个镜头在生成中，稍后再次运行 batch-resume")
    return results
