"""Ubuntu Studio Installer backend."""

from __future__ import annotations

import json
import re
import subprocess
from collections.abc import Callable
from dataclasses import dataclass
from pathlib import Path

from i18n import _

OutputCallback = Callable[[str], None] | None
ProgressCallback = Callable[[float], None] | None

# Matches the apt summary line, e.g.
# "0 upgraded, 5 newly installed, 0 to remove and 0 not upgraded."
_APT_SUMMARY_RE = re.compile(
    r"(\d+)\s+upgraded,\s*(\d+)\s+newly installed,\s*(\d+)\s+to remove"
)

# ---------------------------------------------------------------------------

APP_TITLE = _("Ubuntu Studio Installer")

ICON_PATH = "/usr/share/icons/hicolor/scalable/apps/ubuntustudio-installer.svg"

HELPER = "/usr/sbin/ubuntustudio-installer-helper"

METAPACKAGE_CONFIG_PATH = (
    Path(__file__).resolve().parent / "metapackages.json"
)


# ---------------------------------------------------------------------------


@dataclass
class PackageInfo:
    """A metapackage row for the checklist."""

    name: str
    description: str
    installed: bool
    show_depends: bool = True
    show_recommends: bool = True


@dataclass
class MetapackageConfig:
    """Config for one metapackage row and relation visibility."""

    name: str
    show_depends: bool = True
    show_recommends: bool = True


@dataclass
class PackageRelation:
    """One direct dependency relation from apt-cache depends output."""

    relation: str
    package: str


# ---------------------------------------------------------------------------


def dpkg_installed(package: str) -> bool:
    """Return True if *package* is currently installed."""
    try:
        result = subprocess.run(
            ["dpkg", "-s", package],
            capture_output=True,
            text=True,
        )
        return result.returncode == 0
    except FileNotFoundError:
        return False


def apt_cache_description(package: str) -> str:
    """Return the short description from apt-cache."""
    try:
        result = subprocess.run(
            ["apt-cache", "search", f"^{package}$"],
            capture_output=True,
            text=True,
        )
        for line in result.stdout.strip().splitlines():
            if line.startswith(package):
                # Format: "package-name - Some description"
                parts = line.split(" - ", 1)
                if len(parts) == 2:
                    return parts[1].strip()
        return ""
    except FileNotFoundError:
        return ""


def apt_cache_depends(package: str) -> list[str]:
    """Return direct dependency names of *package*."""
    try:
        result = subprocess.run(
            ["apt-cache", "depends", package],
            capture_output=True,
            text=True,
        )
        deps: list[str] = []
        for line in result.stdout.strip().splitlines():
            if ":" in line:
                dep = line.split(":", 1)[1].strip()
                if dep and dep != "386":
                    deps.append(dep)
        return deps
    except FileNotFoundError:
        return []


def apt_cache_depends_recommends(
    package: str,
    *,
    include_depends: bool = True,
    include_recommends: bool = True,
) -> list[PackageRelation]:
    """Return direct Depends/Recommends relations for *package*."""
    if not include_depends and not include_recommends:
        return []

    try:
        result = subprocess.run(
            ["apt-cache", "depends", package],
            capture_output=True,
            text=True,
        )
    except FileNotFoundError:
        return []

    rows: list[PackageRelation] = []
    seen: set[tuple[str, str]] = set()

    for raw_line in result.stdout.splitlines():
        line = raw_line.strip()
        if not line:
            continue

        relation = ""
        if line.startswith("|Depends:") or line.startswith("Depends:"):
            if not include_depends:
                continue
            relation = "Depends"
        elif line.startswith("|Recommends:") or line.startswith("Recommends:"):
            if not include_recommends:
                continue
            relation = "Recommends"
        else:
            continue

        dep = line.split(":", 1)[1].strip()
        # Skip virtual package names like "<mail-transport-agent>".
        if not dep or dep == "386" or dep.startswith("<"):
            continue

        key = (relation, dep)
        if key in seen:
            continue
        seen.add(key)
        rows.append(PackageRelation(relation=relation, package=dep))

    return rows


def _run_streaming(
    cmd: list[str],
    on_output: OutputCallback = None,
) -> bool:
    """Run *cmd*, streaming stdout/stderr to *on_output*. Returns True on success."""
    try:
        proc = subprocess.Popen(
            cmd,
            stdout=subprocess.PIPE,
            stderr=subprocess.STDOUT,
            text=True,
        )
        assert proc.stdout is not None
        for line in proc.stdout:
            if on_output is not None:
                on_output(line)
        proc.wait()
        return proc.returncode == 0
    except FileNotFoundError:
        return False


def update_apt_cache(on_output: OutputCallback = None) -> bool:
    """Run apt-get update via pkexec."""
    return _run_streaming(["pkexec", HELPER, "update"], on_output)


def load_metapackage_configs() -> list[MetapackageConfig]:
    """Load metapackage configuration from JSON."""
    try:
        raw = json.loads(METAPACKAGE_CONFIG_PATH.read_text())
    except (FileNotFoundError, OSError, json.JSONDecodeError):
        return []

    entries = raw.get("metapackages")
    if not isinstance(entries, list):
        return []

    configs: list[MetapackageConfig] = []
    for entry in entries:
        if not isinstance(entry, dict):
            continue
        name = entry.get("name")
        if not isinstance(name, str) or not name.strip():
            continue

        configs.append(
            MetapackageConfig(
                name=name.strip(),
                show_depends=bool(entry.get("show_depends", True)),
                show_recommends=bool(entry.get("show_recommends", True)),
            )
        )

    return configs


def build_package_table() -> list[PackageInfo]:
    """Return a PackageInfo list for every metapackage."""
    table: list[PackageInfo] = []
    for cfg in load_metapackage_configs():
        installed = dpkg_installed(cfg.name)
        description = apt_cache_description(cfg.name)
        table.append(
            PackageInfo(
                name=cfg.name,
                description=description,
                installed=installed,
                show_depends=cfg.show_depends,
                show_recommends=cfg.show_recommends,
            )
        )
    return table


def install_packages(
    packages: list[str],
    on_output: OutputCallback = None,
    on_progress: ProgressCallback = None,
) -> bool:
    """Install *packages* via pkexec, optionally tracking progress."""
    if not packages:
        return True
    cmd = ["pkexec", HELPER, "install"] + packages

    if on_progress is None:
        return _run_streaming(cmd, on_output)

    total = 0
    done = 0

    def _track(line: str) -> None:
        nonlocal total, done
        if on_output is not None:
            on_output(line)
        m = _APT_SUMMARY_RE.search(line)
        if m:
            total = int(m.group(1)) + int(m.group(2))  # upgraded + newly installed
        if total > 0 and line.startswith("Setting up "):
            done += 1
            on_progress(min(done / total, 1.0))

    return _run_streaming(cmd, _track)


def run_installer_fix() -> bool:
    """Run the audio-configuration fix via pkexec."""
    try:
        result = subprocess.run(["pkexec", HELPER, "fix"])
        return result.returncode == 0
    except FileNotFoundError:
        return False


def autoremove_packages(
    packages: list[str],
    on_output: OutputCallback = None,
    on_progress: ProgressCallback = None,
) -> bool:
    """Auto-mark deps and autoremove *packages* via pkexec."""
    if not packages:
        return True
    cmd = ["pkexec", HELPER, "autoremove"] + packages

    if on_progress is None:
        return _run_streaming(cmd, on_output)

    total = 0
    done = 0

    def _track(line: str) -> None:
        nonlocal total, done
        if on_output is not None:
            on_output(line)
        m = _APT_SUMMARY_RE.search(line)
        if m:
            total = int(m.group(3))  # "to remove"
        if total > 0 and line.startswith("Removing "):
            done += 1
            on_progress(min(done / total, 1.0))

    return _run_streaming(cmd, _track)



