"""fetch — URL in, audio + a Catch out (engine1 uses yt-dlp).

The pure parts (slug, parsing yt-dlp's info JSON into a Catch, building the
download argv) are split out so they're unit-testable without the network. The
single side-effecting wrapper `fetch()` ties them together.
"""
from __future__ import annotations

import datetime as _dt
import json
import os
import shutil
import subprocess
from pathlib import Path
from typing import Callable, Optional

from .model import Catch, slugify

YTDLP = os.environ.get("FOUNDRY_YTDLP", shutil.which("yt-dlp") or "yt-dlp")


def default_workspace() -> Path:
    """Where catches live. Override with $FOUNDRY_HOME."""
    return Path(os.environ.get("FOUNDRY_HOME", str(Path.home() / "Downloads" / "foundry")))


# ── pure helpers (tested without network) ──────────────────────────────────
def catch_from_info(info: dict, *, source_path: str) -> Catch:
    """Build a Catch from yt-dlp's `--dump-json` info dict."""
    title = info.get("title") or info.get("id") or "untitled"
    vid = info.get("id")
    slug = slugify(title)
    if vid and vid.lower() not in slug:
        slug = f"{slug}_{slugify(vid)}"
    return Catch(
        slug=slug,
        title=title,
        source_url=info.get("webpage_url") or info.get("original_url"),
        source_id=vid,
        duration=info.get("duration"),
        source_path=source_path,
        created=_dt.datetime.now().isoformat(timespec="seconds"),
    )


def probe_argv(url: str) -> list[str]:
    """yt-dlp argv to fetch metadata only (no download)."""
    return [YTDLP, "--no-playlist", "--dump-single-json", "--no-download", url]


def download_argv(url: str, catch_dir: Path, *, fmt: str = "bestaudio/best") -> list[str]:
    """yt-dlp argv to download bestaudio into `catch_dir/source.<ext>`."""
    return [
        YTDLP, "--no-playlist", "-f", fmt,
        "--write-info-json",
        "-o", str(catch_dir / "source.%(ext)s"),
        "-o", f"infojson:{catch_dir / 'source'}",
        url,
    ]


def find_source(catch_dir: Path) -> Optional[Path]:
    """Locate the downloaded audio (source.* but not the .info.json)."""
    for p in sorted(catch_dir.glob("source.*")):
        if p.suffix != ".json" and not p.name.endswith(".info.json"):
            return p
    return None


# ── the one side-effecting step ─────────────────────────────────────────────
def fetch(
    url: str,
    workspace: Optional[Path] = None,
    *,
    log: Callable[[str], None] = print,
) -> Catch:
    """Download `url` into the workspace and return a saved Catch."""
    workspace = Path(workspace) if workspace else default_workspace()

    log(f"⚓ probing {url}")
    info = json.loads(subprocess.check_output(probe_argv(url), text=True))
    # Build the catch (gives us the slug) before downloading, so files land in
    # the managed per-catch folder rather than a flat dump.
    catch = catch_from_info(info, source_path="source")
    catch_dir = catch.dir(workspace)
    catch_dir.mkdir(parents=True, exist_ok=True)

    log(f"⬇  downloading → {catch_dir}")
    subprocess.run(download_argv(url, catch_dir), check=True)

    src = find_source(catch_dir)
    if src is None:
        raise RuntimeError(f"download produced no source.* in {catch_dir}")
    catch.source_path = src.name
    catch.save(workspace)
    log(f"✓ caught '{catch.title}' → {catch_dir / 'catch.json'}")
    return catch
