#!/usr/bin/env python3
from __future__ import annotations

import math
import os
import re
import shlex
import shutil
import subprocess
import sys
from pathlib import Path
from typing import NoReturn
from urllib.parse import parse_qs, urlparse

DEFAULT_CLIP_FORMAT = (
    "bestvideo*[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/bestvideo+bestaudio/best"
)
DEFAULT_FRAME_FORMAT = "bestvideo*[ext=mp4]/bestvideo/best[ext=mp4]/best"
DEFAULT_YOUTUBE_EXTRACTOR_ARGS = "youtube:player_client=ios,android"

_TIMESTAMP_PART_RE = re.compile(r"^\d+(?:\.\d+)?$")


def die(message: str, exit_code: int = 1) -> NoReturn:
    print(f"Error: {message}", file=sys.stderr)
    raise SystemExit(exit_code)


def require_command(name: str) -> None:
    if shutil.which(name) is None:
        die(f"missing required command: {name}")


def timestamp_to_seconds(timestamp: str) -> float:
    parts = timestamp.split(":")
    if not 1 <= len(parts) <= 3:
        raise ValueError(f"invalid timestamp: {timestamp}")

    total = 0.0
    for part in parts:
        if _TIMESTAMP_PART_RE.fullmatch(part) is None:
            raise ValueError(f"invalid timestamp: {timestamp}")
        total = (total * 60) + float(part)
    return total


def parse_non_negative_float(value: str, label: str) -> float:
    try:
        parsed = float(value)
    except ValueError as exc:
        raise ValueError(f"{label} must be a non-negative number") from exc

    if not math.isfinite(parsed) or parsed < 0:
        raise ValueError(f"{label} must be a non-negative number")
    return parsed


def parse_non_negative_int(value: str, label: str) -> int:
    if not value.isdigit():
        raise ValueError(f"{label} must be an integer")
    return int(value)


def format_seconds(seconds: float) -> str:
    return f"{seconds:.3f}"


def run_passthrough(args: list[str]) -> None:
    completed = subprocess.run(args, check=False)
    if completed.returncode != 0:
        raise SystemExit(completed.returncode)


def run_capture(args: list[str]) -> str:
    completed = subprocess.run(args, capture_output=True, text=True, check=False)
    if completed.returncode != 0:
        if completed.stdout:
            sys.stdout.write(completed.stdout)
        if completed.stderr:
            sys.stderr.write(completed.stderr)
        raise SystemExit(completed.returncode)
    return completed.stdout


def build_ytdlp_command(url: str, format_selector: str) -> list[str]:
    args = ["yt-dlp"]

    cookies_from_browser = os.getenv("YTDLP_COOKIES_FROM_BROWSER")
    if cookies_from_browser:
        args.extend(["--cookies-from-browser", cookies_from_browser])

    cookies_file = os.getenv("YTDLP_COOKIES")
    if cookies_file:
        args.extend(["--cookies", cookies_file])

    js_runtimes = os.getenv("YTDLP_JS_RUNTIMES")
    if js_runtimes is None and is_youtube_url(url) and shutil.which("node") is not None:
        js_runtimes = "node"
    if js_runtimes:
        args.extend(["--js-runtimes", js_runtimes])

    extractor_args = os.getenv("YTDLP_EXTRACTOR_ARGS")
    if extractor_args is None and is_youtube_url(url):
        extractor_args = DEFAULT_YOUTUBE_EXTRACTOR_ARGS
    if extractor_args:
        args.extend(["--extractor-args", extractor_args])

    extra_args = os.getenv("YTDLP_EXTRA_ARGS")
    if extra_args:
        args.extend(shlex.split(extra_args))

    args.extend(["-f", format_selector, "-g", url])
    return args


def resolve_ytdlp_urls(url: str, format_selector: str) -> list[str]:
    output = run_capture(build_ytdlp_command(url, format_selector))
    return [line.strip() for line in output.splitlines() if line.strip()]


def is_youtube_url(value: str) -> bool:
    lower_value = value.lower()
    if "youtube.com/" in lower_value or "youtu.be/" in lower_value:
        return True

    parsed = urlparse(value)
    host = parsed.netloc.lower()
    return host == "youtu.be" or host.endswith(".youtu.be") or host.endswith(
        "youtube.com"
    )


def extract_video_id(url: str) -> str | None:
    parsed = urlparse(url)
    query_values = parse_qs(parsed.query)
    video_ids = query_values.get("v")
    if video_ids and video_ids[0]:
        return video_ids[0]

    host = parsed.netloc.lower()
    if host == "youtu.be" or host.endswith(".youtu.be"):
        short_id = parsed.path.strip("/")
        if short_id:
            return short_id

    match = re.search(r"[?&]v=([^&\s]+)", url)
    if match:
        return match.group(1)

    return None


def ensure_parent_dir(path_str: str) -> None:
    Path(path_str).parent.mkdir(parents=True, exist_ok=True)
