"""Linux distro detection and per-distro profile registry.

Replaces the v1 250-line if/elif chain with a tuple-of-dataclasses
table (``DISTRO_REGISTRY``). Adding a new distro is a one-row change.
Pure stdlib; no internal package dependencies.

Extracted from ``open-transcode.v3.py`` (QA item v4-03 — package split).
This is a pure code-organization refactor; behavior is identical to v3.
"""

import os
import platform
import time
from dataclasses import dataclass, field
from pathlib import Path

# ──────────────────────────────────────────────
#  DISTRO DETECTION & PROFILES
# ──────────────────────────────────────────────

@dataclass
class DistroProfile:
    family: str              # Canonical family: arch, debian, redhat, suse, nixos, unknown
    name: str                # Pretty name: "Arch Linux", "Fedora 40", etc.
    version_id: str          # e.g. "40", "15.6", "24.05"
    pkg_manager: str         # e.g. "pacman", "dnf", "zypper", "apt", "nix"
    install_cmd_template: str  # e.g. "sudo pacman -S {packages}"
    binary_extra_paths: list[str]  # Distro-specific dirs to search for binaries
    av1an_known_encoder_names: list[str]  # Names this distro's av1an build may accept
    ffmpeg_pkg: str          # Package name providing ffmpeg
    av1an_pkg: str           # Package name providing av1an
    notes: str               # Distro-specific quirks worth showing the user
    # Runtime dependency packages (key = generic name, value = distro package name)
    dep_pkgs: dict[str, str] = field(default_factory=dict)
    # Binaries that av1an invokes directly (not via ffmpeg)
    encoder_binaries: dict[str, list[str]] = field(default_factory=dict)
    # VSScript package name — on most distros this is bundled into 'vapoursynth',
    # but Debian/Ubuntu split it into a separate -script-dev package.
    # If set, this takes priority over dep_pkgs["vapoursynth"] for the VS check.
    vsscript_pkg: str = ""


def _read_os_release() -> dict[str, str]:
    """Parse /etc/os-release into a dict. Falls back to empty dict."""
    os_release = Path("/etc/os-release")
    fallback = Path("/usr/lib/os-release")
    target = os_release if os_release.exists() else fallback
    if not target.exists():
        return {}
    data = {}
    for line in target.read_text(encoding="utf-8", errors="replace").splitlines():
        line = line.strip()
        if "=" in line and not line.startswith("#"):
            key, _, val = line.partition("=")
            data[key.strip()] = val.strip().strip('"')
    return data


# ──────────────────────────────────────────────────────────────────────────────
# DISTRO_REGISTRY — data-driven distro detection (v3, OTC-014).
#
# v1/v2 had a 250-line if/elif chain in detect_distro() with one branch per
# distro family. Each branch constructed a DistroProfile with mostly-identical
# fields — a classic SEI CERT MSC04-C violation (no single source of truth).
#
# v3 collapses the chain into a tuple-of-dicts table. Each entry has:
#   ids:           tuple of distro_id strings that match this family
#   id_likes:      tuple of ID_LIKE substrings that also match this family
#   family:        canonical family name
#   pkg_manager:   package manager binary name
#   install_cmd:   template with {packages} placeholder
#   extra_paths:   list of distro-specific binary search paths
#   dep_pkgs:      map of generic name -> distro package name
#   notes:         distro-specific quirks string
#   vsscript_pkg:  (optional) separate VSScript package name
#
# Adding a new distro is now a single-table-row change — no code modification.
# The encoder_binaries field is identical across all distros and lives in the
# function body (it's the same dict literal every time).
# ──────────────────────────────────────────────────────────────────────────────

# encoder_binaries is identical for every distro — define once.
_ENCODER_BINARIES: dict[str, list[str]] = {
    "svt_av1": ["SvtAv1EncApp", "svt_av1"],
    "vpx": ["vpxenc"],
    "x265": ["x265"],
}

# Common av1an encoder names known across distros.
_AV1AN_KNOWN_ENCODERS: list[str] = ["svt_av1", "svt", "aom", "rav1e", "vpx", "x265"]


@dataclass(frozen=True)
class _DistroEntry:
    """One row in the DISTRO_REGISTRY table."""
    ids: tuple[str, ...]              # exact distro_id matches
    id_likes: tuple[str, ...]         # ID_LIKE substring matches
    family: str
    pkg_manager: str
    install_cmd: str                  # template with {packages}
    extra_paths: tuple[str, ...]
    dep_pkgs: dict[str, str]
    notes: str
    vsscript_pkg: str = ""


DISTRO_REGISTRY: tuple[_DistroEntry, ...] = (
    _DistroEntry(
        ids=("arch", "manjaro", "endeavouros", "garuda", "cachyos"),
        id_likes=("arch",),
        family="arch",
        pkg_manager="pacman",
        install_cmd="sudo pacman -S {packages}",
        extra_paths=("/usr/bin", "/usr/local/bin", "~/.local/bin", "~/.cargo/bin"),
        dep_pkgs={
            "vapoursynth": "vapoursynth",
            "svt-av1": "svt-av1",
            "x265": "x265",
            "vpx": "libvpx",
            "opus": "libopus",
            "vorbis": "libvorbis",
            "flac": "flac",
        },
        notes=(
            "Arch/Manjaro: av1an is in the AUR (yay -S av1an) or community repo. "
            "SVT-AV1 encoder name is typically 'svt_av1'. "
            "Cargo-installed av1an may live in ~/.cargo/bin."
        ),
    ),
    _DistroEntry(
        ids=("fedora",),
        id_likes=("fedora",),
        family="redhat",
        pkg_manager="dnf",
        install_cmd="sudo dnf install {packages}",
        extra_paths=("/usr/bin", "/usr/local/bin", "~/.cargo/bin"),
        dep_pkgs={
            "vapoursynth": "vapoursynth",
            "svt-av1": "svt-av1",
            "x265": "x265",
            "vpx": "libvpx-tools",
            "opus": "opus",
            "vorbis": "libvorbis",
            "flac": "flac",
        },
        notes=(
            "Fedora: av1an may require COPR enablement first: "
            "sudo dnf copr enable sergiomb/av1an  (or build from source). "
            "SVT-AV1 is in the main repos as 'svt-av1'. "
            "Ensure RPM Fusion is enabled for full codec support."
        ),
    ),
    _DistroEntry(
        ids=("rhel", "centos", "rocky", "almalinux", "ol"),
        id_likes=("rhel", "centos"),
        family="redhat",
        # RHEL-family: dnf if present, fall back to yum
        pkg_manager="",  # resolved at runtime in detect_distro()
        install_cmd="",  # resolved at runtime in detect_distro()
        extra_paths=("/usr/bin", "/usr/local/bin", "~/.cargo/bin"),
        dep_pkgs={
            "vapoursynth": "vapoursynth",
            "svt-av1": "svt-av1",
            "x265": "x265",
            "vpx": "libvpx-tools",
            "opus": "opus",
            "vorbis": "libvorbis",
            "flac": "flac",
        },
        notes=(
            "RHEL/CentOS/Rocky/Alma: av1an is NOT in default repos. "
            "Options: (1) cargo install av1an, (2) build from GitHub source, "
            "(3) use pre-built binary from releases. "
            "Enable EPEL + RPM Fusion for FFmpeg codec support."
        ),
    ),
    _DistroEntry(
        ids=("opensuse-leap", "opensuse-tumbleweed", "sles"),
        id_likes=("suse",),
        family="suse",
        pkg_manager="zypper",
        install_cmd="sudo zypper install {packages}",
        extra_paths=("/usr/bin", "/usr/local/bin", "~/.cargo/bin"),
        dep_pkgs={
            "vapoursynth": "vapoursynth",
            "svt-av1": "svt-av1",
            "x265": "x265",
            "vpx": "libvpx",
            "opus": "libopus",
            "vorbis": "libvorbis",
            "flac": "flac",
        },
        notes=(
            "openSUSE: av1an may be available via OBS (Open Build Service). "
            "Check: https://build.opensuse.org/package/show/multimedia:apps/av1an. "
            "Packman repo provides FFmpeg with full codec support."
        ),
    ),
    _DistroEntry(
        ids=("nixos",),
        id_likes=("nixos",),
        family="nixos",
        pkg_manager="nix",
        install_cmd="nix-shell -p {packages}",
        extra_paths=("/run/current-system/sw/bin", "~/.nix-profile/bin"),
        dep_pkgs={
            "vapoursynth": "vapoursynth",
            "svt-av1": "svt-av1",
            "x265": "x265",
            "vpx": "libvpx",
            "opus": "opus",
            "vorbis": "libvorbis",
            "flac": "flac",
        },
        notes=(
            "NixOS: Use 'nix-shell -p ffmpeg av1an' or add to configuration.nix. "
            "Binaries live under /run/current-system/sw/bin or ~/.nix-profile/bin. "
            "av1an CLI flags may differ from other distros depending on the nixpkgs channel."
        ),
    ),
    _DistroEntry(
        ids=("debian", "ubuntu", "linuxmint", "pop"),
        id_likes=("debian",),
        family="debian",
        pkg_manager="apt",
        install_cmd="sudo apt install {packages}",
        extra_paths=("/usr/bin", "/usr/local/bin", "~/.cargo/bin"),
        dep_pkgs={
            "vapoursynth": "vapoursynth",
            "svt-av1": "svtav1",
            "x265": "x265",
            "vpx": "libvpx-tools",
            "opus": "libopus-dev",
            "vorbis": "libvorbis-dev",
            "flac": "flac",
        },
        notes=(
            "Debian/Ubuntu: av1an is in the repos (apt install av1an). "
            "Debian repo builds may use 'svt' as encoder name instead of 'svt_av1'. "
            "VSScript is in a separate package: libvapoursynth-script-dev. "
            "For newer builds, consider cargo install av1an."
        ),
        vsscript_pkg="libvapoursynth-script-dev",
    ),
)


def _match_distro_entry(distro_id: str, id_like: list[str]) -> _DistroEntry | None:
    """Find the first DISTRO_REGISTRY entry whose ids or id_likes match.

    SEI CERT MSC04-C spirit: the matching logic is one flat loop over a
    table — no nested if/elif chain. Adding a new distro is a one-line
    table change in DISTRO_REGISTRY above; this function never needs
    modification.
    """
    for entry in DISTRO_REGISTRY:
        if distro_id in entry.ids:
            return entry
        if any(like in id_like for like in entry.id_likes):
            return entry
    return None


def detect_distro() -> DistroProfile:
    """
    Detect the running Linux distribution via /etc/os-release.
    Returns a DistroProfile with distro-specific package manager,
    install commands, binary search paths, and known quirks.

    v3 (OTC-014): the per-distro data lives in DISTRO_REGISTRY above.
    This function is now ~30 lines of glue instead of a 250-line
    if/elif chain.
    """
    info = _read_os_release()
    id_like = info.get("ID_LIKE", "").lower().split()
    distro_id = info.get("ID", "").lower()
    pretty = info.get("PRETTY_NAME", info.get("NAME", platform.system()))
    version = info.get("VERSION_ID", "?")

    entry = _match_distro_entry(distro_id, id_like)

    if entry is None:
        # Fallback: unknown distro
        return DistroProfile(
            family="unknown",
            name=pretty,
            version_id=version,
            pkg_manager="unknown",
            install_cmd_template="# Unknown distro — install ffmpeg and av1an manually",
            binary_extra_paths=["/usr/bin", "/usr/local/bin", "~/.cargo/bin", "~/.local/bin"],
            av1an_known_encoder_names=list(_AV1AN_KNOWN_ENCODERS),
            ffmpeg_pkg="ffmpeg",
            av1an_pkg="av1an",
            dep_pkgs={},
            encoder_binaries=dict(_ENCODER_BINARIES),
            notes="Unknown distro detected. Ensure ffmpeg and av1an are in PATH.",
        )

    # Resolve runtime-determined fields (RHEL family: dnf vs yum)
    pkg_manager = entry.pkg_manager
    install_cmd = entry.install_cmd
    if not pkg_manager:
        # RHEL/CentOS family: pick dnf if installed, else yum
        has_dnf = Path("/usr/bin/dnf").exists()
        pkg_manager = "dnf" if has_dnf else "yum"
        install_cmd = (
            "sudo dnf install {packages}" if has_dnf
            else "sudo yum install {packages}"
        )

    return DistroProfile(
        family=entry.family,
        name=pretty,
        version_id=version,
        pkg_manager=pkg_manager,
        install_cmd_template=install_cmd,
        binary_extra_paths=list(entry.extra_paths),
        av1an_known_encoder_names=(
            # Arch family includes the additional 'svt-av1' alias
            ["svt_av1", "svt", "svt-av1", "aom", "rav1e", "vpx", "x265"]
            if entry.family == "arch"
            else list(_AV1AN_KNOWN_ENCODERS)
        ),
        ffmpeg_pkg="ffmpeg",
        av1an_pkg="av1an",
        dep_pkgs=dict(entry.dep_pkgs),
        encoder_binaries=dict(_ENCODER_BINARIES),
        vsscript_pkg=entry.vsscript_pkg,
        notes=entry.notes,
    )

