#!/usr/bin/env python3
"""Search YouTube for reusable b-roll, and pull only the seconds the edit needs.

Two things make this different from "downloading a video".

**Licence is a filter, not a footnote.** YouTube marks uploads as either the
standard licence or Creative Commons Attribution. Only the second one grants
anybody the right to reuse and remix the footage. So the search runs against
YouTube's own CC filter, every result carries the licence field the extractor
reported, and anything that is not `Creative Commons Attribution` is refused for
download unless it is explicitly forced — in which case the manifest records
that the rights were asserted by the operator, not verified here. A b-roll
insert whose licence nobody can name is a takedown waiting to happen, and it
costs a channel far more than the shot was worth.

**Nothing downloads on a search.** Same rule as `stock_media.py`: the search
returns a shortlist with thumbnails and numbers, and pulling footage needs an
explicit id. "The first thing the API returned" is not an editorial decision.

**Only the used seconds are fetched.** `--section 41-49` downloads that range,
not the 22-minute source. That is faster, smaller, and it keeps the manifest
honest about what was actually taken.

Every download writes a provenance record — url, id, uploader, channel, licence,
upload date, the exact section, SHA-256, byte count and the `ffprobe` result —
into the same accumulating `manifest.json` the stock scripts use, so the final
report can list every frame's origin without anybody remembering where it came
from.
"""
from __future__ import annotations

import argparse
import hashlib
import json
import subprocess
import sys
from datetime import datetime, timezone
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent))

from media_probe import probe  # noqa: E402
from skill_config import emit, utf8_stdout, which  # noqa: E402

CC_LICENCE = "Creative Commons Attribution"
# The extractor returns YouTube's own wording, which is
# "Creative Commons Attribution license (reuse allowed)" — not the bare name.
# Matching the exact string refuses every genuinely reusable clip, so the test is
# on the licence *family* instead. It is still a whitelist: anything that does
# not name Creative Commons is refused.
CC_MARKER = "creative commons"


def is_reusable(licence: str | None) -> bool:
    return bool(licence) and CC_MARKER in licence.lower()
# YouTube's own search filter for Creative Commons uploads. Filtering after the
# fact instead means paging through hundreds of standard-licence results to find
# the handful that are reusable.
CC_FILTER = "EgIwAUABGAE%3D"


def require_yt_dlp() -> str:
    binary = which("yt-dlp") or which("yt-dlp.exe")
    if binary:
        return binary
    try:
        import yt_dlp  # noqa: F401
        return sys.executable
    except ImportError:
        raise SystemExit(
            "yt-dlp не найден. Установи: pip install -U yt-dlp  (или winget install yt-dlp)")


def run_yt_dlp(binary: str, arguments: list[str]) -> subprocess.CompletedProcess:
    command = ([binary] if not binary.endswith("python.exe") and "python" not in Path(binary).name
               else [binary, "-m", "yt_dlp"]) + arguments
    return subprocess.run(command, capture_output=True, text=True, errors="replace")


def sha256(path: Path) -> str:
    digest = hashlib.sha256()
    with path.open("rb") as handle:
        for block in iter(lambda: handle.read(1 << 20), b""):
            digest.update(block)
    return digest.hexdigest()


def search(binary: str, query: str, count: int, cc_only: bool) -> list[dict]:
    if cc_only:
        target = (f"https://www.youtube.com/results?search_query="
                  f"{query.replace(' ', '+')}&sp={CC_FILTER}")
    else:
        target = f"ytsearch{count}:{query}"
    done = run_yt_dlp(binary, [
        "--flat-playlist", "--dump-single-json", "--playlist-end", str(count),
        "--no-warnings", target,
    ])
    if done.returncode:
        raise SystemExit(f"yt-dlp search failed:\n{(done.stderr or '').strip()[-1200:]}")
    try:
        payload = json.loads(done.stdout or "{}")
    except json.JSONDecodeError:
        raise SystemExit("yt-dlp did not return JSON for the search")
    results = []
    for entry in (payload.get("entries") or [])[:count]:
        if not entry.get("id"):
            continue
        results.append({
            "id": entry["id"],
            "url": f"https://www.youtube.com/watch?v={entry['id']}",
            "title": entry.get("title"),
            "uploader": entry.get("uploader") or entry.get("channel"),
            "duration": entry.get("duration"),
            "view_count": entry.get("view_count"),
            "thumbnail": entry.get("thumbnails", [{}])[-1].get("url")
            if entry.get("thumbnails") else None,
        })
    return results


def describe(binary: str, video_id: str) -> dict:
    done = run_yt_dlp(binary, ["--dump-single-json", "--no-warnings",
                               f"https://www.youtube.com/watch?v={video_id}"])
    if done.returncode:
        raise SystemExit(f"yt-dlp probe failed for {video_id}:\n"
                         f"{(done.stderr or '').strip()[-1000:]}")
    data = json.loads(done.stdout or "{}")
    formats = [f for f in (data.get("formats") or []) if f.get("height")]
    best = max(formats, key=lambda f: (f.get("height") or 0), default={})
    return {
        "id": data.get("id"),
        "url": data.get("webpage_url"),
        "title": data.get("title"),
        "uploader": data.get("uploader"),
        "channel_url": data.get("channel_url"),
        "upload_date": data.get("upload_date"),
        "duration": data.get("duration"),
        "license": data.get("license"),
        "max_height": best.get("height"),
        "fps": best.get("fps"),
        "view_count": data.get("view_count"),
    }


def parse_section(text: str) -> tuple[float, float]:
    if "-" not in text:
        raise SystemExit("--section хочет вид START-END в секундах, например 41-49")
    start, end = text.split("-", 1)
    a, b = float(start), float(end)
    if b <= a:
        raise SystemExit(f"--section {text}: конец не позже начала")
    return a, b


def main() -> None:
    utf8_stdout()
    parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    parser.add_argument("query", help="Search text, or a video id/url with --ids")
    parser.add_argument("--count", type=int, default=12)
    parser.add_argument("--ids", help="Comma-separated video ids to actually download")
    parser.add_argument("--section", help="START-END in seconds; only this range is fetched")
    parser.add_argument("--out", help="Directory for downloads; required with --ids")
    parser.add_argument("--max-height", type=int, default=1080)
    parser.add_argument("--audio-only", action="store_true",
                        help="Pull the audio track alone, as mp3 — for music beds")
    parser.add_argument("--any-license", action="store_true",
                        help="Search without YouTube's Creative Commons filter. "
                             "Results are still reported with their licence.")
    parser.add_argument("--i-hold-the-rights", action="store_true",
                        help="Download material that is not CC-BY, recording in the "
                             "manifest that the rights were asserted, not verified")
    parser.add_argument("--manifest", help="Provenance file (default: <out>/manifest.json)")
    args = parser.parse_args()

    binary = require_yt_dlp()

    if not args.ids:
        results = search(binary, args.query, args.count, not args.any_license)
        emit({
            "status": "ok" if results else "nothing_found",
            "mode": "search",
            "query": args.query,
            "license_filter": "Creative Commons Attribution" if not args.any_license else "любая",
            "found": len(results),
            "shortlist": results,
            "note": ("Поиск ничего не скачивает. Посмотри ролики по ссылкам, выбери план "
                     "и вернись с --ids и --section."),
            "next": (f'python scripts/youtube_source.py "{args.query}" '
                     f'--ids VIDEO_ID --section 41-49 --out project/assets/youtube'),
        })
        return

    if not args.out:
        raise SystemExit("--out обязателен вместе с --ids")
    out_dir = Path(args.out).resolve()
    out_dir.mkdir(parents=True, exist_ok=True)
    manifest_path = Path(args.manifest) if args.manifest else out_dir / "manifest.json"
    manifest = (json.loads(manifest_path.read_text(encoding="utf-8"))
                if manifest_path.is_file() else {"version": "1.0", "items": []})

    section = parse_section(args.section) if args.section else None
    downloaded, refused = [], []

    for raw in args.ids.split(","):
        video_id = raw.strip().split("v=")[-1].split("/")[-1]
        if not video_id:
            continue
        facts = describe(binary, video_id)
        licence = facts.get("license")
        if not is_reusable(licence) and not args.i_hold_the_rights:
            refused.append({
                **facts,
                "refused_because": (
                    f"лицензия {licence!r} — не Creative Commons. Стандартная лицензия YouTube "
                    "не даёт права на повторное использование: вставка такого кадра — это "
                    "заявка на страйк. Если права есть у тебя (свой канал, письменное "
                    "разрешение, материал в общественном достоянии) — повтори с "
                    "--i-hold-the-rights, и это будет записано в манифест как твоё "
                    "утверждение, а не как проверенный факт."),
            })
            continue

        if args.audio_only:
            target = out_dir / f"yt_{video_id}.mp3"
            # 192k rather than "best": a music bed is going to be ducked ~19 LU
            # under a voice and then re-encoded to AAC, so the extra bitrate is
            # spent on data nobody will ever hear.
            arguments = [
                "-f", "bestaudio/best", "-x", "--audio-format", "mp3",
                "--audio-quality", "192K",
                "--no-playlist", "--no-warnings",
                "-o", str(target.with_suffix(".%(ext)s")),
            ]
        else:
            target = out_dir / f"yt_{video_id}.mp4"
            arguments = [
                "-f", f"bestvideo[height<={args.max_height}]+bestaudio/best[height<={args.max_height}]",
                "--merge-output-format", "mp4",
                "--no-playlist", "--no-warnings",
                "-o", str(target),
            ]
        if section:
            # Only the used range. `--force-keyframes-at-cuts` re-encodes the cut
            # points so the clip actually starts on the requested frame instead of
            # on the previous keyframe, which can be seconds earlier.
            arguments += ["--download-sections",
                          f"*{section[0]:.2f}-{section[1]:.2f}",
                          "--force-keyframes-at-cuts"]
        arguments.append(facts["url"])
        done = run_yt_dlp(binary, arguments)
        if done.returncode or not target.is_file():
            refused.append({**facts, "refused_because":
                            f"yt-dlp: {(done.stderr or '').strip()[-500:]}"})
            continue

        info = probe(target)
        record = {
            "provider": "youtube",
            "id": video_id,
            "url": facts["url"],
            "title": facts.get("title"),
            "uploader": facts.get("uploader"),
            "channel_url": facts.get("channel_url"),
            "upload_date": facts.get("upload_date"),
            "license": licence,
            "license_verified": is_reusable(licence),
            "rights_asserted_by_operator": not is_reusable(licence),
            "attribution_required": True,
            "attribution_line": (f"{facts.get('title')} — {facts.get('uploader')} "
                                 f"({facts['url']}), CC BY"),
            "query": args.query,
            "section": {"start": section[0], "end": section[1]} if section else None,
            "path": str(target),
            "bytes": target.stat().st_size,
            "sha256": sha256(target),
            "downloaded_at": datetime.now(timezone.utc).isoformat(timespec="seconds"),
            # display_* rather than stored_*: a clip carrying a rotation matrix
            # reports its stored frame sideways, and a b-roll shortlist that says
            # "1920x1080" for a portrait phone clip is worse than no shortlist.
            "probe": {
                "duration": info.get("duration"),
                "width": (info.get("video") or {}).get("display_width"),
                "height": (info.get("video") or {}).get("display_height"),
                "orientation": (info.get("video") or {}).get("orientation"),
                "fps": (info.get("video") or {}).get("avg_frame_rate"),
                "has_audio": info.get("has_audio"),
            },
        }
        manifest["items"] = [item for item in manifest["items"]
                             if item.get("path") != record["path"]]
        manifest["items"].append(record)
        downloaded.append(record)

    manifest_path.write_text(json.dumps(manifest, ensure_ascii=False, indent=2),
                             encoding="utf-8")
    emit({
        "status": "ok" if downloaded else "nothing_downloaded",
        "mode": "download",
        "downloaded": downloaded,
        "refused": refused,
        "manifest": str(manifest_path),
        "attribution": [item["attribution_line"] for item in downloaded],
        "reminder": ("CC BY требует указания автора в самом ролике или в описании. "
                     "Строки выше — готовые титры; без них лицензия нарушена, "
                     "даже если материал скачан законно."),
    })


if __name__ == "__main__":
    main()
