from __future__ import annotations

import json
import os
import subprocess
from dataclasses import dataclass
from pathlib import Path
from urllib.parse import urlparse

from fastapi import HTTPException

from app.config import settings

VIDEO_EXTENSIONS = {".mp4", ".mov", ".m4v", ".webm", ".avi", ".mkv"}


@dataclass(slots=True)
class UploadVideoAsset:
    url: str
    path: Path
    duration_seconds: float


def _project_root() -> Path:
    return Path.cwd()


def _abs_path(value: str | Path) -> Path:
    path = Path(value)
    if not path.is_absolute():
        path = _project_root() / path
    return path.resolve()


def upload_root() -> Path:
    return _abs_path(settings.UPLOAD_LOCAL_PATH)


def shot_segment_root() -> Path:
    return _abs_path(getattr(settings, "SHOT_SEGMENT_LOCAL_PATH", "./storage/uploads/shot_segments"))


def _strip_base_url(url: str) -> str:
    text = str(url or "").strip()
    if not text:
        return text

    base_url = str(getattr(settings, "BASE_URL", "") or "").strip().rstrip("/")
    if base_url and text.startswith(base_url + "/"):
        return text[len(base_url):]

    parsed = urlparse(text)
    if parsed.scheme in ("http", "https"):
        # 只接受本系统 BASE_URL 下的上传资源；外部 URL 不允许 ffmpeg 本地切片。
        raise HTTPException(status_code=400, detail="拆镜源视频必须来自本系统上传接口，不能传外部 http/https URL")

    return text


def _safe_relative_from_upload_url(url: str) -> str:
    value = _strip_base_url(url)
    value = value.split("?", 1)[0].split("#", 1)[0]

    if value.startswith("/uploads/"):
        rel = value.replace("/uploads/", "", 1)
    elif value.startswith("uploads/"):
        rel = value.replace("uploads/", "", 1)
    else:
        raise HTTPException(status_code=400, detail="拆镜源视频链接必须是 /uploads/ 下的上传资源")

    rel = rel.lstrip("/")
    if not rel or ".." in Path(rel).parts:
        raise HTTPException(status_code=400, detail="上传视频路径非法")
    return rel


def resolve_upload_video_path(video_url: str) -> Path:
    rel = _safe_relative_from_upload_url(video_url)
    root = upload_root()
    path = (root / rel).resolve()

    try:
        path.relative_to(root)
    except ValueError as exc:
        raise HTTPException(status_code=400, detail="上传视频路径越界") from exc

    if path.suffix.lower() not in VIDEO_EXTENSIONS:
        raise HTTPException(status_code=400, detail="上传资源不是支持的视频格式")
    if not path.exists() or not path.is_file():
        raise HTTPException(status_code=404, detail=f"上传视频文件不存在: {video_url}")
    return path


def build_upload_url_from_path(path: str | Path) -> str:
    root = upload_root()
    checked_path = _abs_path(path)
    try:
        rel = checked_path.relative_to(root).as_posix()
    except ValueError as exc:
        raise HTTPException(status_code=500, detail="生成上传资源 URL 失败：路径不在 uploads 目录下") from exc
    return f"/uploads/{rel}"


def get_ffmpeg_bin() -> str:
    return str(getattr(settings, "FFMPEG_BIN", "") or "ffmpeg")


def get_ffprobe_bin() -> str:
    configured = str(getattr(settings, "FFPROBE_BIN", "") or "").strip()
    if configured:
        return configured
    ffmpeg_bin = get_ffmpeg_bin()
    if ffmpeg_bin.endswith("ffmpeg.exe"):
        return ffmpeg_bin[:-10] + "ffprobe.exe"
    if ffmpeg_bin.endswith("ffmpeg"):
        return ffmpeg_bin[:-6] + "ffprobe"
    return "ffprobe"


def probe_video_duration_seconds(video_path: str | Path) -> float:
    path = _abs_path(video_path)
    timeout = int(getattr(settings, "SHOT_FFPROBE_TIMEOUT_SECONDS", 20) or 20)
    cmd = [
        get_ffprobe_bin(),
        "-v", "error",
        "-show_entries", "format=duration",
        "-of", "json",
        str(path),
    ]
    try:
        completed = subprocess.run(
            cmd,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=True,
            timeout=timeout,
            check=False,
        )
    except Exception as exc:
        raise HTTPException(status_code=400, detail=f"ffprobe 获取视频时长失败: {exc}") from exc

    if completed.returncode != 0:
        raise HTTPException(status_code=400, detail=f"ffprobe 获取视频时长失败: {completed.stderr.strip()}")

    try:
        data = json.loads(completed.stdout or "{}")
        duration = float((data.get("format") or {}).get("duration") or 0)
    except Exception as exc:
        raise HTTPException(status_code=400, detail="ffprobe 返回的视频时长无法解析") from exc

    if duration <= 0:
        raise HTTPException(status_code=400, detail="视频时长无效")
    return round(duration, 3)


def validate_upload_video_asset(video_url: str, frontend_duration_seconds: float | None = None) -> UploadVideoAsset:
    path = resolve_upload_video_path(video_url)
    real_duration = probe_video_duration_seconds(path)

    if frontend_duration_seconds is not None and frontend_duration_seconds > 0:
        tolerance = float(getattr(settings, "SHOT_DURATION_TOLERANCE_SECONDS", 1.0) or 1.0)
        # 超出误差时以后端 ffprobe 为准，不拒绝，避免前端浮点或浏览器 metadata 偏差导致创建失败。
        if abs(float(frontend_duration_seconds) - real_duration) <= tolerance:
            real_duration = round(float(frontend_duration_seconds), 3)

    return UploadVideoAsset(url=_strip_base_url(video_url), path=path, duration_seconds=real_duration)


def validate_split_range(*, start_second: float, end_second: float, video_duration_seconds: float) -> tuple[float, float, float]:
    start = round(float(start_second), 3)
    end = round(float(end_second), 3)

    if start < 0:
        raise HTTPException(status_code=400, detail="开始秒不能小于0")
    if end <= start:
        raise HTTPException(status_code=400, detail="结束秒必须大于开始秒")

    tolerance = float(getattr(settings, "SHOT_SPLIT_END_TOLERANCE_SECONDS", 0.5) or 0.5)
    if end > float(video_duration_seconds) + tolerance:
        raise HTTPException(status_code=400, detail="结束秒不能超过视频总时长")

    duration = round(end - start, 3)
    min_seconds = float(getattr(settings, "SHOT_SPLIT_MIN_SECONDS", 1) or 1)
    max_seconds = float(getattr(settings, "SHOT_SPLIT_MAX_SECONDS", 120) or 120)
    if duration < min_seconds:
        raise HTTPException(status_code=400, detail=f"拆镜片段不能低于 {min_seconds:g} 秒")
    if duration > max_seconds:
        raise HTTPException(status_code=400, detail=f"拆镜片段不能超过 {max_seconds:g} 秒")
    return start, end, duration


def format_second(value: float) -> int | float:
    checked = float(value)
    if checked.is_integer():
        return int(checked)
    return round(checked, 2)


def build_time_node(start_second: float, end_second: float) -> str:
    return f"{format_second(start_second)}-{format_second(end_second)}秒"


def ensure_shot_segment_dir(date_dir: str) -> Path:
    root = shot_segment_root()
    output_dir = (root / date_dir).resolve()
    try:
        output_dir.relative_to(root)
    except ValueError as exc:
        raise RuntimeError("拆镜输出目录越界") from exc
    os.makedirs(output_dir, exist_ok=True)
    return output_dir
