"""System-wide Scanner: journal, coredumps, alte Kernel-Module."""
from __future__ import annotations

import json
import re
import subprocess
import time
from datetime import datetime, timezone
from pathlib import Path

from taninux.maintain.scanners._util import dir_size
from taninux.maintain.types import Category, Classification, Finding


_SIZE_RE = re.compile(r"([\d.]+)\s*([KMGT])(?:iB)?", re.IGNORECASE)
_UNITS = {"K": 1024, "M": 1024**2, "G": 1024**3, "T": 1024**4}


def _parse_size_with_unit(s: str) -> int:
    m = _SIZE_RE.search(s)
    if not m:
        return 0
    return int(float(m.group(1)) * _UNITS.get(m.group(2).upper(), 1))


# ----------------------------------------------------------------- journal


class SystemJournalScanner:
    """Gesamtgrösse des systemd-journal via `journalctl --disk-usage`."""
    name = "journal"

    def scan(self) -> list[Finding]:
        try:
            r = subprocess.run(
                ["journalctl", "--disk-usage"],
                capture_output=True, text=True, check=False, timeout=10,
            )
        except (FileNotFoundError, subprocess.TimeoutExpired):
            return []
        if r.returncode != 0:
            return []

        size = _parse_size_with_unit(r.stdout)
        if size <= 0:
            return []

        if size > 2 * 1024**3:
            cls = Classification.SAFE
            reason = f"Journal nimmt {size/1024**3:.1f} GB ein — Vacuum dringend empfohlen"
        elif size > 512 * 1024**2:
            cls = Classification.LIKELY_ORPHAN
            reason = f"Journal nimmt {size/1024**2:.0f} MB ein — Vacuum sinnvoll"
        else:
            cls = Classification.UNKNOWN
            reason = f"Journal nimmt {size/1024**2:.0f} MB ein — unkritisch"

        return [Finding(
            path=Path("journal://system"),
            category=Category.SYSTEM,
            classification=cls,
            size_bytes=size,
            last_access_days=None,
            reason=reason,
            matched_package="systemd-journal",
            extra={"kind": "journal"},
        )]


# ----------------------------------------------------------------- coredumps


class SystemCoredumpScanner:
    """Crash-Dumps via `coredumpctl`. Nutzt JSON-Output wenn verfügbar."""
    name = "coredumps"

    def scan(self) -> list[Finding]:
        entries = self._list_via_json()
        if entries is None:
            entries = self._list_via_text()
        if not entries:
            return []

        now = time.time()
        findings: list[Finding] = []
        for e in entries:
            ts = e.get("timestamp")
            age_days = int((now - ts) / 86400) if ts else None
            exe_name = Path(e.get("exe", "")).name or "?"
            findings.append(Finding(
                path=Path(f"coredump://{e.get('pid', '?')}/{exe_name}"),
                category=Category.SYSTEM,
                classification=Classification.LIKELY_ORPHAN,
                size_bytes=e.get("size", 0),
                last_access_days=age_days,
                reason=f"Coredump von {exe_name} (sig {e.get('signal', '?')})",
                matched_package=exe_name,
                extra={"kind": "coredump", **e},
            ))
        findings.sort(key=lambda f: -(f.size_bytes or 0))
        return findings

    def _list_via_json(self) -> list[dict] | None:
        try:
            r = subprocess.run(
                ["coredumpctl", "--json=short", "list"],
                capture_output=True, text=True, check=False, timeout=10,
            )
        except (FileNotFoundError, subprocess.TimeoutExpired):
            return None
        if r.returncode != 0 or not r.stdout.strip():
            return None
        try:
            data = json.loads(r.stdout)
        except json.JSONDecodeError:
            return None
        if not isinstance(data, list):
            return None
        entries: list[dict] = []
        for item in data:
            # timestamp in microseconds since epoch
            ts_us = item.get("time")
            ts = ts_us / 1_000_000 if isinstance(ts_us, (int, float)) else None
            # coredumpctl reports null/missing size when the corefile is gone
            size_raw = item.get("size")
            size = int(size_raw) if isinstance(size_raw, (int, float)) else 0
            entries.append({
                "pid": item.get("pid"),
                "uid": item.get("uid"),
                "signal": item.get("signal_name") or item.get("signal"),
                "exe": item.get("exe", ""),
                "size": size,
                "timestamp": ts,
            })
        return entries

    def _list_via_text(self) -> list[dict]:
        """Fallback: parse `coredumpctl list` text output."""
        try:
            r = subprocess.run(
                ["coredumpctl", "list", "--no-pager"],
                capture_output=True, text=True, check=False, timeout=10,
            )
        except (FileNotFoundError, subprocess.TimeoutExpired):
            return []
        if r.returncode not in (0, 1):  # exit 1 = no dumps
            return []
        entries: list[dict] = []
        # Header looks like: "TIME PID UID GID SIG COREFILE EXE SIZE"
        for line in r.stdout.splitlines()[1:]:  # skip header
            parts = line.split()
            if len(parts) < 7:
                continue
            try:
                pid = int(parts[4])
                size = _parse_size_with_unit(parts[-1])
                exe = parts[-2]
                sig = parts[7] if len(parts) > 7 else "?"
            except (ValueError, IndexError):
                continue
            entries.append({
                "pid": pid,
                "uid": None,
                "signal": sig,
                "exe": exe,
                "size": size,
                "timestamp": None,
            })
        return entries


# ----------------------------------------------------------------- kernel modules


class SystemKernelsScanner:
    """`/usr/lib/modules/<version>/` ohne pacman-Owner = orphaned Kernel-Module."""
    name = "kernels"

    def scan(self) -> list[Finding]:
        modules_root = Path("/usr/lib/modules")
        if not modules_root.is_dir():
            return []

        kernel_dirs: list[Path] = []
        try:
            for entry in modules_root.iterdir():
                if entry.is_symlink():
                    continue
                if entry.is_dir():
                    kernel_dirs.append(entry)
        except (OSError, PermissionError):
            return []
        if not kernel_dirs:
            return []

        try:
            r = subprocess.run(
                ["pacman", "-Qo", *[str(d) for d in kernel_dirs]],
                capture_output=True, text=True, check=False, timeout=30,
            )
        except (FileNotFoundError, subprocess.TimeoutExpired):
            return []

        # pacman -Qo mit mehreren Args: stdout hat "X is owned by Y",
        # stderr hat "error: No package owns Z" für Orphans.
        owned: set[str] = set()
        for line in r.stdout.splitlines():
            m = re.match(r"^(.+?)\s+is owned by ", line)
            if m:
                owned.add(m.group(1).rstrip("/"))

        findings: list[Finding] = []
        for d in kernel_dirs:
            key = str(d).rstrip("/")
            if key in owned:
                continue
            size = dir_size(d)
            findings.append(Finding(
                path=d,
                category=Category.SYSTEM,
                classification=Classification.SAFE,
                size_bytes=size,
                last_access_days=None,
                reason="Kernel-Modul ohne Pacman-Eigentümer — Kernel-Paket weg, Module verwaist",
                matched_package=None,
                extra={"kind": "old-kernel-modules", "version": d.name},
            ))
        findings.sort(key=lambda f: -f.size_bytes)
        return findings
