"""Overview/Dashboard — Landing-View mit OS at-a-glance."""
from __future__ import annotations

import os
import subprocess
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path

from textual import work
from textual.app import ComposeResult
from textual.binding import Binding
from textual.containers import VerticalScroll
from textual.widgets import Static

from taninux.core import pacman as pac
from taninux.core import sysinfo
from taninux.core import theme as T
from taninux.core.paths import snapshots_dir, trash_dir


def _human_size(n: int) -> str:
    f = float(n)
    for unit in ("B", "K", "M", "G", "T"):
        if f < 1024 or unit == "T":
            return f"{f:.0f}{unit}" if unit == "B" else f"{f:.1f}{unit}"
        f /= 1024
    return f"{f:.1f}T"


def _human_duration(seconds: float) -> str:
    s = int(seconds)
    if s < 60:
        return f"{s}s"
    if s < 3600:
        return f"{s // 60}m"
    h, rem = divmod(s, 3600)
    if h < 24:
        return f"{h}h {rem // 60}m"
    d, h = divmod(h, 24)
    return f"{d}d {h}h"


@dataclass
class PackageStats:
    total: int
    explicit: int
    aur: int
    deps: int
    orphans: int


def _pacman_count(args: list[str]) -> int:
    try:
        r = subprocess.run(
            ["pacman", *args],
            capture_output=True, text=True, check=False, timeout=10,
        )
        return len([l for l in r.stdout.splitlines() if l.strip()])
    except (FileNotFoundError, subprocess.TimeoutExpired):
        return 0


def _package_stats() -> PackageStats:
    return PackageStats(
        total=_pacman_count(["-Qq"]),
        explicit=_pacman_count(["-Qenq"]),
        aur=_pacman_count(["-Qmq"]),
        deps=_pacman_count(["-Qdq"]),
        orphans=_pacman_count(["-Qdtq"]),
    )


def _trash_stats() -> tuple[int, int]:
    """(entries, total_bytes)"""
    try:
        entries = list(trash_dir().iterdir())
    except OSError:
        return (0, 0)
    count = 0
    total_size = 0
    for e in entries:
        if not e.is_dir():
            continue
        count += 1
        mf = e / "manifest.json"
        if mf.exists():
            try:
                import json
                data = json.loads(mf.read_text())
                total_size += int(data.get("size_bytes", 0))
            except (OSError, ValueError):
                pass
    return (count, total_size)


def _last_snapshot() -> str | None:
    try:
        snaps = sorted(snapshots_dir().glob("*.json"))
    except OSError:
        return None
    return snaps[-1].stem if snaps else None


def _flatpak_count() -> tuple[int, int]:
    """(installed apps, installed runtimes)"""
    try:
        r1 = subprocess.run(
            ["flatpak", "list", "--app", "--columns=application"],
            capture_output=True, text=True, check=False, timeout=5,
        )
        r2 = subprocess.run(
            ["flatpak", "list", "--runtime", "--columns=application"],
            capture_output=True, text=True, check=False, timeout=5,
        )
    except (FileNotFoundError, subprocess.TimeoutExpired):
        return (0, 0)
    apps = len([l for l in r1.stdout.splitlines() if l.strip() and l.strip() != "Application ID"])
    rts = len([l for l in r2.stdout.splitlines() if l.strip() and l.strip() != "Application ID"])
    return (apps, rts)


class OverviewView(VerticalScroll):
    """Landing-Dashboard: OS at-a-glance + quick metrics."""

    DEFAULT_CSS = f"""
    OverviewView {{
        background: {T.YORU};
        padding: 1 2;
        scrollbar-color: {T.BASE2} {T.YORU};
        scrollbar-background: {T.YORU};
        scrollbar-corner-color: {T.YORU};
        scrollbar-size-vertical: 1;
    }}
    OverviewView Static {{
        background: {T.YORU};
        margin-bottom: 1;
    }}
    """

    BINDINGS = [
        Binding("r", "refresh", "refresh"),
    ]

    def __init__(self, **kwargs) -> None:
        super().__init__(**kwargs)

    def compose(self) -> ComposeResult:
        yield Static("", id="o-banner", markup=True)
        yield Static("", id="o-system", markup=True)
        yield Static("", id="o-pkgs", markup=True)
        yield Static("", id="o-updates", markup=True)
        yield Static("", id="o-maintain", markup=True)
        yield Static("", id="o-flatpak", markup=True)
        yield Static("", id="o-recent", markup=True)
        yield Static("", id="o-hints", markup=True)

    def on_mount(self) -> None:
        self._refresh_fast()
        self._refresh_slow()
        self._refresh_maintain()

    def action_refresh(self) -> None:
        pac.invalidate()
        self._refresh_fast()
        self._refresh_slow()
        self._refresh_maintain()

    # ---- fast (synchronous, <100ms) ----

    def _refresh_fast(self) -> None:
        now = datetime.now()
        date_str = now.strftime("%A · %d. %B %Y · %H:%M")
        banner = (
            f"[{T.UME}]▲ TANINUX[/]   "
            f"[{T.BASE4}]·[/]   "
            f"[{T.SHINKAI}]{date_str}[/]"
        )
        self.query_one("#o-banner", Static).update(banner)

        # System
        up = sysinfo.uptime_seconds()
        boot = sysinfo.boot_time_seconds()
        gpus_list = sysinfo.gpus()
        gpu_str = "  ·  ".join(
            f"[{T.YUKI}]{g.vendor}[/] [{T.BASE5}]({g.driver or '—'})[/]"
            for g in gpus_list
        ) or f"[{T.BASE5}]?[/]"

        lines = [
            f"[{T.UME}]◈ system[/]",
            f"  [{T.SHINKAI}]distro[/]    [{T.YUKI}]{sysinfo.distro_pretty()}[/]",
            f"  [{T.SHINKAI}]kernel[/]    [{T.YUKI}]{sysinfo.kernel_release()}[/]",
            f"  [{T.SHINKAI}]uptime[/]    [{T.YUKI}]{_human_duration(up)}[/]"
            + (f"   [{T.BASE4}](boot {boot:.1f}s)[/]" if boot else ""),
            f"  [{T.SHINKAI}]gpu[/]       {gpu_str}",
            f"  [{T.SHINKAI}]audio[/]     [{T.YUKI}]{sysinfo.audio_server()}[/]",
        ]
        self.query_one("#o-system", Static).update("\n".join(lines))

        # Packages
        stats = _package_stats()
        lines = [
            f"[{T.UME}]◆ pakete[/]",
            f"  [{T.SHINKAI}]total[/]       [{T.YUKI}]{stats.total}[/]",
            f"  [{T.SHINKAI}]explicit[/]    [{T.YUKI}]{stats.explicit}[/] [{T.BASE5}](native)[/]   "
            f"[{T.YUKI}]{stats.aur}[/] [{T.BASE5}](AUR)[/]",
            f"  [{T.SHINKAI}]deps[/]        [{T.BASE5}]{stats.deps}[/]",
        ]
        if stats.orphans:
            lines.append(
                f"  [{T.SHINKAI}]orphans[/]     [{T.CHIKYU}]{stats.orphans}[/]   "
                f"[{T.BASE4}](pacman -Qdt — siehe maintain/orphans)[/]"
            )
        else:
            lines.append(f"  [{T.SHINKAI}]orphans[/]     [{T.TAKE}]0[/]")
        self.query_one("#o-pkgs", Static).update("\n".join(lines))

        # Flatpak
        fp_apps, fp_runtimes = _flatpak_count()
        if fp_apps or fp_runtimes:
            lines = [
                f"[{T.UME}]● flatpak[/]",
                f"  [{T.SHINKAI}]apps[/]        [{T.YUKI}]{fp_apps}[/]   "
                f"[{T.SHINKAI}]runtimes[/]    [{T.YUKI}]{fp_runtimes}[/]",
            ]
        else:
            lines = [
                f"[{T.UME}]● flatpak[/]",
                f"  [{T.BASE5}](nicht installiert / leer)[/]",
            ]
        self.query_one("#o-flatpak", Static).update("\n".join(lines))

        # Recent
        trash_count, trash_bytes = _trash_stats()
        last_snap = _last_snapshot()
        lines = [f"[{T.UME}]✎ recent[/]"]
        if trash_count:
            lines.append(
                f"  [{T.SHINKAI}]trash[/]       [{T.CHIKYU}]{trash_count} einträge[/]   "
                f"[{T.BASE5}]({_human_size(trash_bytes)})[/]   "
                f"[{T.BASE4}]via:  taninux trash list[/]"
            )
        else:
            lines.append(f"  [{T.SHINKAI}]trash[/]       [{T.BASE5}]leer[/]")
        if last_snap:
            lines.append(f"  [{T.SHINKAI}]snapshot[/]    [{T.YUKI}]{last_snap}[/]   "
                         f"[{T.BASE4}](taninux snapshot diff)[/]")
        else:
            lines.append(f"  [{T.SHINKAI}]snapshot[/]    [{T.BASE5}]keine — speichere:[/] "
                         f"[{T.BASE4}]taninux snapshot save[/]")
        self.query_one("#o-recent", Static).update("\n".join(lines))

        # Hints
        hints = (
            f"[{T.BASE4}]quick-keys:[/]   "
            f"[{T.BASE5}]m[/] maintain   "
            f"[{T.BASE5}]u[/] update   "
            f"[{T.BASE5}]b[/] bundles   "
            f"[{T.BASE5}]s[/] suggest   "
            f"[{T.BASE5}]f[/] flatpak   "
            f"[{T.BASE5}]k[/] kernel   "
            f"[{T.BASE5}]h[/] health"
        )
        self.query_one("#o-hints", Static).update(hints)

        # Updates: zeige Placeholder bis worker fertig
        self.query_one("#o-updates", Static).update(
            f"[{T.UME}]↻ updates[/]\n  [{T.BASE5}]scanne pacman/AUR/flatpak/fwupd…[/]"
        )

    # ---- slow (background) ----

    @work(thread=True, exclusive=True, group="overview-updates")
    def _refresh_slow(self) -> None:
        from taninux.update.sources import fetch_all
        items = fetch_all()
        self.app.call_from_thread(self._render_updates, items)

    def _render_updates(self, items) -> None:
        from collections import Counter
        counts = Counter(i.source for i in items)
        total = len(items)
        if not items:
            content = (
                f"[{T.UME}]↻ updates[/]\n"
                f"  [{T.TAKE}]✓ system ist aktuell[/]"
            )
        else:
            total_size = sum(i.size_bytes for i in items)
            size_str = f"   [{T.BASE4}]({_human_size(total_size)})[/]" if total_size else ""
            parts = []
            for src in ("pacman", "aur", "flatpak", "fwupd"):
                n = counts.get(src, 0)
                if n:
                    color = T.UME if src == "pacman" else (T.CHIKYU if src == "aur" else (T.TAKE if src == "flatpak" else T.SHINKAI))
                    parts.append(f"[{color}]{src} {n}[/]")
            content = (
                f"[{T.UME}]↻ updates[/]\n"
                f"  [{T.SHINKAI}]pending[/]     [{T.YUKI}]{total}[/]{size_str}\n"
                f"  [{T.SHINKAI}]quellen[/]     " + "  ·  ".join(parts) + "\n"
                f"  [{T.BASE4}]drücke u[/] [{T.BASE5}]→[/] [{T.BASE4}]e zum ausführen[/]"
            )
        try:
            self.query_one("#o-updates", Static).update(content)
        except Exception:
            pass

    @work(thread=True, exclusive=True, group="overview-maintain")
    def _refresh_maintain(self) -> None:
        # Schnelle Variante: nur cache + journal scannen (die sind <100ms)
        from taninux.maintain.scanners.pacman import PacmanCacheScanner
        from taninux.maintain.scanners.system import SystemJournalScanner
        cache_findings = PacmanCacheScanner().scan()
        journal_findings = SystemJournalScanner().scan()
        cache_bytes = sum(
            f.size_bytes for f in cache_findings
            if f.classification.value in ("safe", "likely")
        )
        journal_bytes = sum(f.size_bytes for f in journal_findings)
        self.app.call_from_thread(self._render_maintain, cache_bytes, journal_bytes)

    def _render_maintain(self, cache_bytes: int, journal_bytes: int) -> None:
        if cache_bytes or journal_bytes:
            lines = [
                f"[{T.UME}]▣ maintain — quick wins[/]",
            ]
            if cache_bytes:
                lines.append(
                    f"  [{T.SHINKAI}]pacman cache[/]   "
                    f"[{T.CHIKYU}]{_human_size(cache_bytes)}[/]   "
                    f"[{T.BASE4}](sudo paccache -ruk0)[/]"
                )
            if journal_bytes:
                lines.append(
                    f"  [{T.SHINKAI}]journal[/]        "
                    f"[{T.CHIKYU}]{_human_size(journal_bytes)}[/]   "
                    f"[{T.BASE4}](sudo journalctl --vacuum-time=2weeks)[/]"
                )
            lines.append(
                f"  [{T.BASE4}]für full-scan: drücke m[/]"
            )
        else:
            lines = [
                f"[{T.UME}]▣ maintain — quick wins[/]",
                f"  [{T.TAKE}]✓ nichts auffälliges (full-scan via m)[/]",
            ]
        try:
            self.query_one("#o-maintain", Static).update("\n".join(lines))
        except Exception:
            pass
