"""Maintain-Pillar View — der bisherige archmaint-Code, jetzt als embeddable Widget."""
from __future__ import annotations

from collections import Counter
from pathlib import Path

from textual import work
from textual.app import ComposeResult
from textual.binding import Binding
from textual.containers import Horizontal, Vertical
from textual.widgets import DataTable, Static, Tab, Tabs

from taninux.core import theme as T
from taninux.core.confirm import ConfirmModal
from taninux.core.state import State
from taninux.core.trash import move_to_trash
from taninux.maintain.scanners import SCANNERS, ScannerSpec, by_id
from taninux.maintain.scanners.dotfolders import DotfoldersScanner
from taninux.maintain.types import (
    CLASS_COLOR,
    CLASS_LABEL,
    CLASSIFICATION_ORDER,
    Classification,
    Finding,
    glyph,
)


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 _format_days(d: int | None) -> str:
    if d is None:
        return "—"
    if d == 0:
        return "heute"
    if d < 30:
        return f"{d}d"
    if d < 365:
        return f"{d // 30}mo"
    return f"{d // 365}y"


def _display_path(f: Finding) -> str:
    s = str(f.path)
    if s.startswith("pkg://"):
        return s[6:]
    home = str(Path.home())
    return "~" + s[len(home):] if s.startswith(home) else s


class MaintainView(Vertical):
    """Maintain-Pillar als Widget — Tabs + Tabelle + Detail-Pane."""

    DEFAULT_CSS = f"""
    MaintainView {{
        background: {T.YORU};
        color: {T.YUKI};
        layout: vertical;
    }}

    MaintainView Tabs {{
        background: {T.YORU};
        height: 3;
    }}
    MaintainView Tabs > #tabs-list {{
        background: {T.YORU};
    }}
    MaintainView Tab {{
        background: {T.YORU};
        color: {T.BASE4};
        padding: 0 2;
    }}
    MaintainView Tab.-active {{
        color: {T.UME};
        background: {T.YORU};
    }}
    MaintainView Underline {{
        background: {T.YORU};
        color: {T.BASE2};
    }}
    MaintainView Underline > .underline--bar {{
        background: {T.YORU};
        color: {T.UME};
    }}

    MaintainView #m-stats {{
        height: 1;
        padding: 0 2 0 2;
        background: {T.YORU};
        color: {T.BASE5};
    }}
    MaintainView #m-legend {{
        height: 1;
        padding: 1 2 1 2;
        background: {T.YORU};
        color: {T.BASE4};
    }}
    MaintainView #m-main {{
        height: 1fr;
        background: {T.YORU};
    }}
    MaintainView #m-table {{
        width: 2fr;
        background: {T.YORU};
        color: {T.FG_ALT};
        border: none;
        padding-right: 2;
        overflow-x: hidden;
        scrollbar-size-vertical: 1;
        scrollbar-color: {T.BASE2} {T.YORU};
        scrollbar-color-hover: {T.BASE3} {T.YORU};
        scrollbar-color-active: {T.BASE3} {T.YORU};
        scrollbar-background: {T.YORU};
        scrollbar-background-hover: {T.YORU};
        scrollbar-background-active: {T.YORU};
        scrollbar-corner-color: {T.YORU};
    }}
    MaintainView #m-table:focus {{
        border: none;
    }}
    MaintainView DataTable > .datatable--header {{
        background: {T.YORU};
        color: {T.SHINKAI};
    }}
    MaintainView DataTable > .datatable--cursor {{
        background: {T.BASE1};
        color: {T.YUKI};
    }}
    MaintainView DataTable > .datatable--hover {{
        background: {T.YORU};
    }}
    MaintainView DataTable > .datatable--even-row {{
        background: {T.YORU};
    }}
    MaintainView DataTable > .datatable--odd-row {{
        background: {T.YORU};
    }}
    MaintainView #m-detail {{
        width: 1fr;
        padding: 1 2;
        background: {T.YORU};
        color: {T.FG_ALT};
    }}
    MaintainView #m-flash {{
        height: 1;
        padding: 0 2;
        background: {T.YORU};
        color: {T.BASE4};
    }}
    """

    BINDINGS = [
        Binding("space", "toggle_select", "select"),
        Binding("d", "delete", "trash"),
        Binding("w", "whitelist", "behalten"),
        Binding("i", "ignore", "ignorieren"),
        Binding("o", "open", "öffnen"),
        Binding("r", "rescan", "rescan"),
        Binding("R", "rescan_all", "rescan all", show=False),
        Binding("j", "cursor_down", "down", show=False),
        Binding("k", "cursor_up", "up", show=False),
        Binding("g", "cursor_top", "top", show=False),
        Binding("G", "cursor_bottom", "bottom", show=False),
        Binding("ctrl+1", "select_tab('dotfolders')", "", show=False),
        Binding("ctrl+2", "select_tab('orphans')", "", show=False),
        Binding("ctrl+3", "select_tab('pacnew')", "", show=False),
        Binding("ctrl+4", "select_tab('cache')", "", show=False),
        Binding("ctrl+5", "select_tab('flatpak-runtimes')", "", show=False),
        Binding("ctrl+6", "select_tab('flatpak-data')", "", show=False),
        Binding("ctrl+7", "select_tab('aur-cache')", "", show=False),
        Binding("ctrl+8", "select_tab('journal')", "", show=False),
        Binding("ctrl+9", "select_tab('coredumps')", "", show=False),
        Binding("ctrl+0", "select_tab('kernels')", "", show=False),
    ]

    def __init__(self, **kwargs) -> None:
        super().__init__(**kwargs)
        self.state = State.load()
        self.current_tab: str = SCANNERS[0].id
        self.findings_by_tab: dict[str, list[Finding]] = {}
        self.selected: set[Path] = set()

    # ---- composition ----

    def compose(self) -> ComposeResult:
        yield Tabs(*[Tab(s.label, id=s.id) for s in SCANNERS], id="tabs")
        yield Static("", id="m-stats", markup=True)
        yield Static("", id="m-legend", markup=True)
        with Horizontal(id="m-main"):
            yield DataTable(id="m-table", cursor_type="row", zebra_stripes=False)
            yield Static("", id="m-detail", markup=True)
        yield Static("", id="m-flash", markup=True)

    def on_mount(self) -> None:
        table = self.query_one("#m-table", DataTable)
        table.add_column(" ", width=2, key="sel")
        table.add_column("", width=2, key="icon")
        table.add_column("pfad", key="path")
        table.add_column("grösse", key="size")
        table.add_column("idle", width=7, key="idle")
        table.add_column("status", key="status")

    # ---- properties ----

    @property
    def findings(self) -> list[Finding]:
        return self.findings_by_tab.get(self.current_tab, [])

    @property
    def spec(self) -> ScannerSpec:
        s = by_id(self.current_tab)
        assert s is not None
        return s

    def _current_finding(self) -> Finding | None:
        table = self.query_one("#m-table", DataTable)
        row = table.cursor_row
        if row is None or row < 0 or row >= len(self.findings):
            return None
        return self.findings[row]

    def _targets(self) -> list[Finding]:
        if self.selected:
            return [f for f in self.findings if f.path in self.selected]
        cur = self._current_finding()
        return [cur] if cur else []

    def _flash(self, msg: str, color: str | None = None) -> None:
        color = color or T.BASE4
        self.query_one("#m-flash", Static).update(f"[{color}]{msg}[/]")

    def _make_scanner(self, spec: ScannerSpec):
        if spec.id == "dotfolders":
            return DotfoldersScanner(self.state)
        return spec.factory()

    # ---- actions ----

    def action_select_tab(self, tab_id: str) -> None:
        self.query_one(Tabs).active = tab_id

    def action_rescan(self) -> None:
        spec = self.spec
        self._flash(f"scanne {spec.label}…")
        if spec.id == "dotfolders":
            self.state = State.load()
        self._rescan_worker(spec.id)

    @work(thread=True, exclusive=True, group="maintain-scan")
    def _rescan_worker(self, tab_id: str) -> None:
        spec = by_id(tab_id)
        if spec is None:
            return
        findings = self._make_scanner(spec).scan()
        self.app.call_from_thread(self._after_rescan, tab_id, findings)

    def _after_rescan(self, tab_id: str, findings: list[Finding]) -> None:
        self.findings_by_tab[tab_id] = findings
        if tab_id == self.current_tab:
            existing = {f.path for f in findings}
            self.selected &= existing
            self._refresh_view()
            self._flash(f"{len(findings)} einträge in {self.spec.label}")

    def action_rescan_all(self) -> None:
        self._flash("scanne alle kategorien…")
        self.state = State.load()
        self.selected.clear()
        self._rescan_all_worker()

    @work(thread=True, exclusive=True, group="maintain-scan-all")
    def _rescan_all_worker(self) -> None:
        for spec in SCANNERS:
            findings = self._make_scanner(spec).scan()
            self.app.call_from_thread(self._after_rescan_all_one, spec.id, findings)
        self.app.call_from_thread(self._flash, "alle kategorien gescannt")

    def _after_rescan_all_one(self, tab_id: str, findings: list[Finding]) -> None:
        self.findings_by_tab[tab_id] = findings
        if tab_id == self.current_tab:
            self._refresh_view()

    def action_toggle_select(self) -> None:
        cur = self._current_finding()
        if cur is None:
            return
        if cur.path in self.selected:
            self.selected.discard(cur.path)
        else:
            self.selected.add(cur.path)
        self._render_keep_cursor()
        self._render_summary()

    def action_whitelist(self) -> None:
        if self.spec.actions != "trash":
            self._flash(f"whitelist nicht verfügbar in {self.spec.label}", color=T.BASE4)
            return
        targets = self._targets()
        if not targets:
            return
        for f in targets:
            self.state.whitelist_add(f.path)
        self.selected.clear()
        self._flash(f"{len(targets)} auf whitelist gesetzt", color=T.SHINKAI)
        self.action_rescan()

    def action_ignore(self) -> None:
        if self.spec.actions != "trash":
            self._flash(f"ignore nicht verfügbar in {self.spec.label}", color=T.BASE4)
            return
        targets = self._targets()
        if not targets:
            return
        for f in targets:
            self.state.ignore_add(f.path)
        self.selected.clear()
        self._flash(f"{len(targets)} ignoriert", color=T.BASE5)
        self.action_rescan()

    def action_delete(self) -> None:
        if self.spec.actions != "trash":
            self._flash(
                f"löschen in {self.spec.label} braucht sudo — siehe detail-pane",
                color=T.CHIKYU,
            )
            return
        targets = self._targets()
        if not targets:
            return
        total = sum(f.size_bytes for f in targets)
        body = (
            f"{len(targets)} einträge ({_human_size(total)}) in den trash verschieben?\n"
            f"  ↩ wiederherstellbar via:  taninux trash list / restore"
        )
        self.app.push_screen(
            ConfirmModal("löschen?", body, danger=True),
            self._after_delete_confirm,
        )

    def _after_delete_confirm(self, confirmed: bool | None) -> None:
        if not confirmed:
            self._flash("abgebrochen")
            return
        targets = self._targets()
        moved = 0
        bytes_moved = 0
        errors: list[str] = []
        for f in targets:
            try:
                entry = move_to_trash(f.path)
                moved += 1
                bytes_moved += entry.size_bytes
            except Exception as e:  # noqa: BLE001
                errors.append(f"{f.path.name}: {e}")
        self.selected.clear()
        msg = f"{moved} einträge in trash ({_human_size(bytes_moved)})"
        if errors:
            msg += f"  ·  {len(errors)} fehler"
        self._flash(msg, color=T.TAKE if not errors else T.RED)
        self.action_rescan()

    def action_open(self) -> None:
        cur = self._current_finding()
        if cur is None:
            return
        if str(cur.path).startswith("pkg://"):
            self._flash("paket — kein pfad zum öffnen", color=T.BASE4)
            return
        import subprocess
        try:
            subprocess.Popen(
                ["xdg-open", str(cur.path)],
                stdout=subprocess.DEVNULL,
                stderr=subprocess.DEVNULL,
            )
            self._flash(f"öffne {_display_path(cur)}")
        except FileNotFoundError:
            self._flash("xdg-open nicht verfügbar", color=T.RED)

    def action_cursor_down(self) -> None:
        self.query_one("#m-table", DataTable).action_cursor_down()

    def action_cursor_up(self) -> None:
        self.query_one("#m-table", DataTable).action_cursor_up()

    def action_cursor_top(self) -> None:
        table = self.query_one("#m-table", DataTable)
        if table.row_count:
            table.move_cursor(row=0)

    def action_cursor_bottom(self) -> None:
        table = self.query_one("#m-table", DataTable)
        if table.row_count:
            table.move_cursor(row=table.row_count - 1)

    # ---- render ----

    def _refresh_view(self) -> None:
        self._render_table()
        self._render_summary()

    def _render_keep_cursor(self) -> None:
        table = self.query_one("#m-table", DataTable)
        row = table.cursor_row
        self._render_table()
        if row is not None and 0 <= row < len(self.findings):
            table.move_cursor(row=row)

    def _render_table(self) -> None:
        table = self.query_one("#m-table", DataTable)
        table.clear()
        for f in self.findings:
            color = CLASS_COLOR[f.classification]
            label = CLASS_LABEL[f.classification]
            sel_glyph = (
                f"[{T.UME}]■[/]" if f.path in self.selected
                else f"[{T.BASE3}]·[/]"
            )
            path = _display_path(f)
            if "/" in path:
                head, _, tail = path.rpartition("/")
                path_styled = f"[{T.BASE4}]{head}/[/][{T.YUKI}]{tail}[/]"
            else:
                path_styled = f"[{T.YUKI}]{path}[/]"
            size_str = _human_size(f.size_bytes) if f.size_bytes else "—"
            size_styled = (
                f"[{T.CHIKYU}]{size_str}[/]" if f.size_bytes
                else f"[{T.BASE3}]—[/]"
            )
            idle_styled = f"[{T.BASE5}]{_format_days(f.last_access_days)}[/]"
            status_styled = f"[{color}]{label}[/]"
            table.add_row(
                sel_glyph,
                glyph(f.classification),
                path_styled,
                size_styled,
                idle_styled,
                status_styled,
            )

        if self.findings:
            self._render_detail(self.findings[0])
        else:
            self._render_empty()

    def _render_empty(self) -> None:
        self.query_one("#m-detail", Static).update(
            f"[{T.BASE4}]keine einträge in {self.spec.label}[/]\n\n"
            f"[{T.BASE5}]drücke r zum rescan, oder ctrl+1…0 für anderen scanner[/]"
        )

    def _render_summary(self) -> None:
        by_class: Counter[Classification] = Counter(f.classification for f in self.findings)
        total_reclaim = sum(
            f.size_bytes for f in self.findings
            if f.classification in (Classification.SAFE, Classification.LIKELY_ORPHAN)
        )
        if self.selected:
            sel_bytes = sum(
                f.size_bytes for f in self.findings if f.path in self.selected
            )
            stats = (
                f"[{T.UME}]{len(self.selected)} ausgewählt[/]"
                f"   [{T.BASE3}]·[/]   "
                f"[{T.CHIKYU}]{_human_size(sel_bytes)}[/]"
                f"   [{T.BASE3}]·[/]   "
                f"[{T.BASE5}]{len(self.findings)} {self.spec.label} insgesamt[/]"
            )
        else:
            stats = (
                f"[{T.YUKI}]{len(self.findings)}[/] [{T.BASE5}]{self.spec.label}[/]"
                f"   [{T.BASE3}]·[/]   "
                f"[{T.TAKE}]{_human_size(total_reclaim)}[/] [{T.BASE5}]potentiell wegwerfbar[/]"
            )
        self.query_one("#m-stats", Static).update(stats)

        legend_parts = []
        for cls in CLASSIFICATION_ORDER:
            count = by_class.get(cls, 0)
            if count == 0:
                continue
            legend_parts.append(
                f"{glyph(cls)} [{T.BASE5}]{CLASS_LABEL[cls]}[/] "
                f"[{T.BASE4}]{count}[/]"
            )
        self.query_one("#m-legend", Static).update(
            f"   [{T.BASE3}]│[/]   ".join(legend_parts)
        )

    def _render_detail(self, f: Finding) -> None:
        color = CLASS_COLOR[f.classification]
        label = CLASS_LABEL[f.classification]
        gly = glyph(f.classification)
        path_display = _display_path(f)
        is_pkg = str(f.path).startswith("pkg://")

        lines = [
            f"{gly}  [{color}]{label.upper()}[/]",
            "",
        ]
        if is_pkg:
            lines += [
                f"[{T.SHINKAI}]paket[/]",
                f"  [{T.YUKI}]{path_display}[/]",
            ]
        else:
            lines += [
                f"[{T.SHINKAI}]pfad[/]",
                f"  [{T.YUKI}]{f.path}[/]",
            ]
        lines += [
            "",
            f"[{T.SHINKAI}]grösse[/]      [{T.CHIKYU}]{_human_size(f.size_bytes)}[/]",
        ]
        if f.last_access_days is not None:
            lines.append(
                f"[{T.SHINKAI}]idle[/]        [{T.BASE5}]{_format_days(f.last_access_days)}[/]"
            )
        lines.append(
            f"[{T.SHINKAI}]kategorie[/]   [{T.BASE5}]{self.spec.label}[/]"
        )
        if f.matched_package and not is_pkg:
            lines.append(f"[{T.SHINKAI}]paket[/]       [{T.UME}]{f.matched_package}[/]")
        lines += [
            "",
            f"[{T.SHINKAI}]grund[/]",
            f"  [{T.BASE5}]{f.reason}[/]",
        ]
        if self.spec.advice:
            advice = self.spec.advice.format(
                pkg=f.matched_package or "",
                path=str(f.path),
            )
            lines += [
                "",
                f"[{T.SHINKAI}]hinweis[/]",
                f"  [{T.CHIKYU}]{advice}[/]",
            ]
        if f.path in self.selected:
            lines += ["", f"[{T.UME}]■ ausgewählt[/]"]
        self.query_one("#m-detail", Static).update("\n".join(lines))

    # ---- events ----

    def on_tabs_tab_activated(self, event: Tabs.TabActivated) -> None:
        if event.tab is None:
            return
        self.current_tab = event.tab.id
        self.selected.clear()
        if self.current_tab not in self.findings_by_tab:
            self.action_rescan()
        else:
            self._refresh_view()

    def on_data_table_row_highlighted(self, event: DataTable.RowHighlighted) -> None:
        if event.cursor_row is None or event.cursor_row >= len(self.findings):
            return
        self._render_detail(self.findings[event.cursor_row])
