"""剪辑计划渲染任务 — Phase 8 任务 2.05.

Celery 任务 worker.render_edit_plan:
  1. 加载 EditPlan + EditPlanClips
  2. 下载各片段素材
  3. 按 order 顺序拼接片段
  4. 上传渲染结果到 OSS
  5. 更新 EditPlan / EditPlanClip 状态
  6. 更新 GenerationTask 进度
"""

from __future__ import annotations

import logging
import os
import shutil
import subprocess  # nosec B404
import tempfile
from datetime import datetime, timezone
from pathlib import Path
from typing import Optional
from urllib.parse import urlparse

import oss2
from worker_app.celery_app import celery_app
from worker_app.db import SessionLocal

logger = logging.getLogger(__name__)

FFMPEG_BIN = shutil.which("ffmpeg") or "ffmpeg"
FFPROBE_BIN = shutil.which("ffprobe") or "ffprobe"
OUTPUT_WIDTH = 1280
OUTPUT_HEIGHT = 720
OUTPUT_FPS = 25.0


# ── OSS helpers ───────────────────────────────────────────────────────────────


def _oss_settings() -> tuple[str, str, str, str] | None:
    """获取 OSS 配置"""
    access_key_id = os.getenv("OSS_ACCESS_KEY_ID")
    access_key_secret = os.getenv("OSS_ACCESS_KEY_SECRET")
    endpoint = os.getenv("OSS_ENDPOINT")
    bucket_name = os.getenv("OSS_BUCKET_NAME")
    if not all([access_key_id, access_key_secret, endpoint, bucket_name]):
        return None
    return access_key_id, access_key_secret, endpoint, bucket_name


def _oss_bucket() -> oss2.Bucket | None:
    """获取 OSS Bucket"""
    settings = _oss_settings()
    if settings is None:
        return None
    access_key_id, access_key_secret, endpoint, bucket_name = settings
    return oss2.Bucket(oss2.Auth(access_key_id, access_key_secret), endpoint, bucket_name)


def _normalize_storage_key(storage_key_or_url: str) -> str:
    """标准化存储键"""
    if storage_key_or_url.startswith(("http://", "https://")):
        return urlparse(storage_key_or_url).path.lstrip("/")
    return storage_key_or_url.lstrip("/")


def _download_asset(asset_storage_key: str, local_path: Path) -> bool:
    """下载素材文件到本地"""
    bucket = _oss_bucket()
    if bucket is None:
        return False
    try:
        bucket.get_object_to_file(_normalize_storage_key(asset_storage_key), str(local_path))
        return local_path.exists() and local_path.stat().st_size > 0
    except Exception:
        logger.exception("下载素材失败: %s", asset_storage_key)
        return False


def _upload_to_oss(local_path: Path, storage_key: str) -> str | None:
    """上传文件到 OSS，返回公开 URL"""
    bucket = _oss_bucket()
    if bucket is None:
        return None
    try:
        bucket.put_object_from_file(storage_key, str(local_path))
        settings = _oss_settings()
        if settings:
            _, _, endpoint, bucket_name = settings
            return f"https://{bucket_name}.{endpoint.replace('https://', '').replace('http://', '')}/{storage_key}"
        return None
    except Exception:
        logger.exception("上传 OSS 失败: %s", storage_key)
        return None


# ── FFmpeg helpers ────────────────────────────────────────────────────────────


def _run_ffmpeg(command: list[str]) -> None:
    """执行 FFmpeg 命令"""
    subprocess.run(command, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)  # nosec B603


def _probe_duration(local_path: Path) -> float:
    """获取视频/音频时长"""
    try:
        result = subprocess.run(  # nosec B603
            [
                FFPROBE_BIN,
                "-v",
                "error",
                "-show_entries",
                "format=duration",
                "-of",
                "default=noprint_wrappers=1:nokey=1",
                str(local_path),
            ],
            check=True,
            capture_output=True,
            text=True,
        )
        return float(result.stdout.strip())
    except Exception:
        return 0.0


def _concatenate_clips(
    clip_paths: list[Path],
    output_path: Path,
    transition_effects: list[str] | None = None,
) -> bool:
    """将多个片段拼接为最终视频

    使用 FFmpeg concat demuxer 实现。
    """
    if not clip_paths:
        return False

    if len(clip_paths) == 1:
        # 单片段直接复制
        try:
            shutil.copy2(str(clip_paths[0]), str(output_path))
            return True
        except Exception:
            return False

    # 多片段：使用 concat demuxer
    concat_file = output_path.parent / "concat_list.txt"
    try:
        with open(concat_file, "w") as f:
            for p in clip_paths:
                f.write(f"file '{p}'\n")

        command = [
            FFMPEG_BIN,
            "-y",
            "-f",
            "concat",
            "-safe",
            "0",
            "-i",
            str(concat_file),
            "-c",
            "copy",
            str(output_path),
        ]
        _run_ffmpeg(command)
        return output_path.exists() and output_path.stat().st_size > 0
    except Exception:
        logger.exception("拼接片段失败")
        return False
    finally:
        if concat_file.exists():
            concat_file.unlink()


# ── Repository imports (延迟导入避免循环依赖) ─────────────────────────────────


def _get_repos():
    """获取数据库仓储实例"""
    from packages.adapters.sqlalchemy_impl import SQLAlchemyEditPlanRepository
    from packages.adapters.sqlalchemy_impl.edit_plan_clip_repository import (
        SQLAlchemyEditPlanClipRepository,
    )
    from packages.adapters.sqlalchemy_impl.generation_task_repository import (
        SQLAlchemyGenerationTaskRepository,
    )

    db = SessionLocal()
    try:
        plan_repo = SQLAlchemyEditPlanRepository(db)
        clip_repo = SQLAlchemyEditPlanClipRepository(db)
        gen_task_repo = SQLAlchemyGenerationTaskRepository(db)
        yield plan_repo, clip_repo, gen_task_repo, db
    finally:
        db.close()


# ── Celery Task ───────────────────────────────────────────────────────────────


@celery_app.task(name="worker.render_edit_plan", bind=True, max_retries=2)
def render_edit_plan(self, plan_id: str) -> dict:
    """渲染剪辑计划

    流程：
    1. 加载 EditPlan + EditPlanClips
    2. 下载各片段素材到临时目录
    3. 按 order 顺序拼接片段
    4. 上传渲染结果到 OSS
    5. 更新 EditPlan → completed, EditPlanClips → rendered
    6. 更新 GenerationTask 进度
    """
    logger.info("开始渲染剪辑计划: plan_id=%s", plan_id)

    for repos in _get_repos():
        plan_repo, clip_repo, gen_task_repo, db = repos

        try:
            # 1. 加载 EditPlan
            plan = plan_repo.get(plan_id)
            if plan is None:
                logger.error("剪辑计划不存在: %s", plan_id)
                return {"status": "error", "message": f"计划不存在: {plan_id}"}

            # 2. 加载片段列表（按 order 排序）
            clips = clip_repo.list_by_plan(plan_id, skip=0, limit=10000)
            if not clips:
                logger.warning("剪辑计划没有片段: %s", plan_id)
                plan.mark_failed()
                plan_repo.update(plan)
                return {"status": "error", "message": "没有可渲染的片段"}

            # 获取 generation_task_id
            generation_task_id = plan.config.get("generation_task_id", "")

            # 更新 GenerationTask 状态为 running
            if generation_task_id:
                gen_task = gen_task_repo.get(generation_task_id)
                if gen_task:
                    gen_task.status = "running"
                    gen_task.started_at = datetime.now(timezone.utc)
                    gen_task_repo.update(gen_task)

            # 3. 下载素材并拼接
            with tempfile.TemporaryDirectory(prefix="edit_plan_") as tmpdir:
                tmpdir_path = Path(tmpdir)
                clip_paths: list[Path] = []
                rendered_clip_ids: list[str] = []
                failed_clip_ids: list[str] = []

                for clip in clips:
                    if not clip.asset_id:
                        # 没有素材的片段跳过，标记为失败
                        clip.mark_failed()
                        clip_repo.update(clip)
                        failed_clip_ids.append(clip.id)
                        continue

                    # 下载素材
                    ext = Path(clip.asset_id).suffix or ".mp4"
                    local_path = tmpdir_path / f"clip_{clip.order:04d}{ext}"
                    if _download_asset(clip.asset_id, local_path):
                        clip_paths.append(local_path)
                        rendered_clip_ids.append(clip.id)
                    else:
                        clip.mark_failed()
                        clip_repo.update(clip)
                        failed_clip_ids.append(clip.id)

                if not clip_paths:
                    logger.error("所有片段素材下载失败: %s", plan_id)
                    plan.mark_failed()
                    plan_repo.update(plan)
                    if generation_task_id:
                        gen_task = gen_task_repo.get(generation_task_id)
                        if gen_task:
                            gen_task.status = "failed"
                            gen_task.error_message = "所有片段素材下载失败"
                            gen_task.completed_at = datetime.now(timezone.utc)
                            gen_task_repo.update(gen_task)
                    return {"status": "error", "message": "所有片段素材下载失败"}

                # 4. 拼接片段
                output_path = tmpdir_path / f"rendered_{plan_id}.mp4"
                transition_effects = [c.transition_effect for c in clips if c.asset_id]
                success = _concatenate_clips(clip_paths, output_path, transition_effects)

                if not success:
                    logger.error("片段拼接失败: %s", plan_id)
                    plan.mark_failed()
                    plan_repo.update(plan)
                    if generation_task_id:
                        gen_task = gen_task_repo.get(generation_task_id)
                        if gen_task:
                            gen_task.status = "failed"
                            gen_task.error_message = "片段拼接失败"
                            gen_task.completed_at = datetime.now(timezone.utc)
                            gen_task_repo.update(gen_task)
                    return {"status": "error", "message": "片段拼接失败"}

                # 5. 上传到 OSS
                storage_key = f"rendered/{plan_id}/output.mp4"
                output_url = _upload_to_oss(output_path, storage_key)

                # 6. 更新片段状态为 rendered
                for clip_id in rendered_clip_ids:
                    clip = clip_repo.get(clip_id)
                    if clip and clip.status.value == "ready":
                        clip.mark_rendered()
                        clip_repo.update(clip)

                # 7. 更新 EditPlan 状态为 completed
                plan.config["rendered_url"] = output_url or ""
                plan.config["rendered_storage_key"] = storage_key
                plan.mark_completed()
                plan_repo.update(plan)

                # 8. 更新 GenerationTask 状态为 completed
                if generation_task_id:
                    gen_task = gen_task_repo.get(generation_task_id)
                    if gen_task:
                        gen_task.status = "completed"
                        gen_task.progress = 100.0
                        gen_task.result_count = len(rendered_clip_ids)
                        gen_task.completed_at = datetime.now(timezone.utc)
                        gen_task_repo.update(gen_task)

                logger.info(
                    "剪辑计划渲染完成: plan_id=%s rendered=%d failed=%d",
                    plan_id,
                    len(rendered_clip_ids),
                    len(failed_clip_ids),
                )

                return {
                    "status": "completed",
                    "plan_id": plan_id,
                    "rendered_count": len(rendered_clip_ids),
                    "failed_count": len(failed_clip_ids),
                    "output_url": output_url,
                }

        except Exception as exc:
            logger.exception("渲染剪辑计划异常: %s", plan_id)
            # 尝试标记计划为失败
            try:
                plan = plan_repo.get(plan_id)
                if plan and plan.status.value == "rendering":
                    plan.mark_failed()
                    plan_repo.update(plan)
            except Exception as e:
                logger.warning(
                    f"Operation failed in apps/worker/worker_app/tasks/edit_plan_generation.py: {e}", exc_info=True
                )
            raise self.retry(exc=exc, countdown=60)

    return {"status": "error", "message": "数据库连接失败"}
