"""Pacman-Scanner: orphans, .pacnew/.pacsave Configs, /var/cache/pacman/pkg."""
from __future__ import annotations

import os
import re
import subprocess
from pathlib import Path

from taninux.maintain.types import Category, Classification, Finding


_SIZE_RE = re.compile(r"([\d.]+)\s*(\w+)")
_SIZE_UNITS = {
    "B": 1, "KiB": 1024, "MiB": 1024**2, "GiB": 1024**3, "TiB": 1024**4,
    "K": 1024, "M": 1024**2, "G": 1024**3, "T": 1024**4,
    "KB": 1000, "MB": 1000**2, "GB": 1000**3, "TB": 1000**4,
}


def _parse_size(s: str) -> int:
    m = _SIZE_RE.search(s)
    if not m:
        return 0
    val = float(m.group(1))
    unit = m.group(2)
    return int(val * _SIZE_UNITS.get(unit, 1))


# ----------------------------------------------------------------- orphans


class PacmanOrphansScanner:
    """`pacman -Qdt` — als Dependency installiert, hat aber keinen Owner mehr."""
    name = "orphans"

    def scan(self) -> list[Finding]:
        try:
            r = subprocess.run(
                ["pacman", "-Qdtq"],
                capture_output=True, text=True, check=False, timeout=10,
            )
        except (FileNotFoundError, subprocess.TimeoutExpired):
            return []
        pkgs = [p.strip() for p in r.stdout.splitlines() if p.strip()]
        if not pkgs:
            return []

        sizes = self._installed_sizes(pkgs)
        findings: list[Finding] = []
        for pkg in pkgs:
            findings.append(Finding(
                path=Path(f"pkg://{pkg}"),
                category=Category.PACMAN,
                classification=Classification.LIKELY_ORPHAN,
                size_bytes=sizes.get(pkg, 0),
                last_access_days=None,
                reason="Als Dependency installiert, hat keinen Owner mehr",
                matched_package=pkg,
                extra={"kind": "orphan", "pkg": pkg},
            ))
        findings.sort(key=lambda f: -f.size_bytes)
        return findings

    @staticmethod
    def _installed_sizes(pkgs: list[str]) -> dict[str, int]:
        try:
            r = subprocess.run(
                ["pacman", "-Qi", *pkgs],
                capture_output=True, text=True, check=False, timeout=30,
            )
        except (FileNotFoundError, subprocess.TimeoutExpired):
            return {}
        sizes: dict[str, int] = {}
        current: str | None = None
        for line in r.stdout.splitlines():
            if line.startswith("Name") and ":" in line:
                current = line.split(":", 1)[1].strip()
            elif current and line.startswith("Installed Size") and ":" in line:
                sizes[current] = _parse_size(line.split(":", 1)[1])
        return sizes


# ----------------------------------------------------------------- pacnew/pacsave


class PacnewScanner:
    """`.pacnew` (neue Default-Configs vom Update) + `.pacsave` (Backups bei Removal)."""
    name = "pacnew"

    def scan(self) -> list[Finding]:
        findings: list[Finding] = []
        for root in (Path("/etc"), Path("/usr/share")):
            if not root.is_dir():
                continue
            for dirpath, _dirs, files in os.walk(
                root, followlinks=False, onerror=lambda _e: None
            ):
                for fn in files:
                    if not (fn.endswith(".pacnew") or fn.endswith(".pacsave")):
                        continue
                    p = Path(dirpath) / fn
                    try:
                        size = p.stat(follow_symlinks=False).st_size
                    except OSError:
                        continue
                    kind = "pacnew" if fn.endswith(".pacnew") else "pacsave"
                    reason = (
                        "Neue Default-Config vom Update — mit pacdiff mergen"
                        if kind == "pacnew"
                        else "Backup der alten Config nach Paket-Entfernung"
                    )
                    findings.append(Finding(
                        path=p,
                        category=Category.PACMAN,
                        classification=Classification.UNKNOWN,
                        size_bytes=size,
                        last_access_days=None,
                        reason=reason,
                        extra={"kind": kind},
                    ))
        findings.sort(key=lambda f: str(f.path))
        return findings


# ----------------------------------------------------------------- pacman cache


class PacmanCacheScanner:
    """`/var/cache/pacman/pkg/` — ältere Paket-Versionen + Pakete die nicht mehr installiert sind."""
    name = "cache"

    def scan(self) -> list[Finding]:
        cache_dir = Path("/var/cache/pacman/pkg")
        if not cache_dir.is_dir():
            return []

        installed = self._installed_packages()

        # Files nach Paket-Name gruppieren
        groups: dict[str, list[tuple[Path, int, float]]] = {}
        try:
            for entry in os.scandir(cache_dir):
                if not entry.is_file(follow_symlinks=False):
                    continue
                fn = entry.name
                if not any(fn.endswith(ext) for ext in (
                    ".pkg.tar.zst", ".pkg.tar.xz", ".pkg.tar.gz"
                )):
                    continue
                if fn.endswith(".sig"):
                    continue
                pkg_name = self._extract_pkg_name(fn)
                if not pkg_name:
                    continue
                try:
                    st = entry.stat(follow_symlinks=False)
                except OSError:
                    continue
                groups.setdefault(pkg_name, []).append(
                    (Path(entry.path), st.st_size, st.st_mtime)
                )
        except (OSError, PermissionError):
            return []

        findings: list[Finding] = []
        for pkg, items in groups.items():
            items.sort(key=lambda t: t[2], reverse=True)  # newest first
            is_installed = pkg in installed
            for i, (path, size, _mtime) in enumerate(items):
                # Neueste Version eines installierten Pakets → behalten als Reinstall-Quelle
                if i == 0 and is_installed:
                    continue
                if not is_installed:
                    cls = Classification.SAFE
                    reason = f"Paket {pkg} nicht installiert"
                else:
                    cls = Classification.LIKELY_ORPHAN
                    reason = f"Ältere Version von {pkg} (neuere im Cache)"
                findings.append(Finding(
                    path=path,
                    category=Category.PACMAN,
                    classification=cls,
                    size_bytes=size,
                    last_access_days=None,
                    reason=reason,
                    matched_package=pkg,
                    extra={"kind": "cache", "pkg": pkg},
                ))
        findings.sort(key=lambda f: f.sort_key)
        return findings

    @staticmethod
    def _installed_packages() -> set[str]:
        try:
            r = subprocess.run(
                ["pacman", "-Qq"],
                capture_output=True, text=True, check=False, timeout=10,
            )
            return {l.strip() for l in r.stdout.splitlines() if l.strip()}
        except (FileNotFoundError, subprocess.TimeoutExpired):
            return set()

    @staticmethod
    def _extract_pkg_name(filename: str) -> str | None:
        """`foo-bar-1.2.3-1-x86_64.pkg.tar.zst` → `foo-bar`"""
        for ext in (".pkg.tar.zst", ".pkg.tar.xz", ".pkg.tar.gz"):
            if filename.endswith(ext):
                stem = filename[:-len(ext)]
                break
        else:
            return None
        # strip -<arch>, -<pkgrel>, -<pkgver>
        try:
            stem = stem.rsplit("-", 1)[0]  # arch
            stem = stem.rsplit("-", 1)[0]  # pkgrel
            return stem.rsplit("-", 1)[0]  # pkgver
        except IndexError:
            return None
