"""Checklist questionary, tables rich, barres de progression."""

from __future__ import annotations

from collections.abc import Callable, Sequence
from pathlib import Path

import questionary
from rich.console import Console
from rich.markup import escape
from rich.progress import (
    BarColumn,
    Progress,
    SpinnerColumn,
    TaskProgressColumn,
    TextColumn,
    TimeElapsedColumn,
)
from rich.table import Table

from osx_backup.fsutil import format_size
from osx_backup.modules.base import (
    Inventory,
    Module,
    ModuleResult,
    ProgressReporter,
    TaskHandle,
)

AskChoices = Callable[[list[questionary.Choice]], list[str] | None]
AskConfirm = Callable[[str], bool | None]

STATUS_STYLE = {"ok": "green", "failed": "red", "skipped": "yellow"}
ACTION_LABEL = {"backup": "sauvegarde", "restore": "restauration"}


def _warning_lines(warnings: Sequence[str]) -> str:
    """Lignes ⚠ d'avertissement, markup rich échappé (texte d'origine externe)."""
    return "".join(f"\n[yellow]⚠ {escape(w)}[/]" for w in warnings)


def _label(module: Module, inv: Inventory) -> str:
    return (
        f"{module.name:<10} {inv.items} élément(s), {format_size(inv.size_bytes)}"
        f"  ({module.description})"
    )


def _default_ask(choices: list[questionary.Choice]) -> list[str] | None:
    return questionary.checkbox(
        "Modules à traiter (espace : cocher, entrée : valider)", choices=choices
    ).ask()


def checklist(
    modules: Sequence[Module],
    inventories: dict[str, Inventory],
    preselected: set[str] | None = None,
    ask: AskChoices | None = None,
) -> list[str]:
    """Demande les modules à traiter ; renvoie les noms dans l'ordre des modules."""
    choices: list[questionary.Choice] = []
    for module in modules:
        inv = inventories[module.name]
        if inv.available:
            checked = preselected is None or module.name in preselected
            choices.append(
                questionary.Choice(_label(module, inv), value=module.name, checked=checked)
            )
        else:
            choices.append(
                questionary.Choice(
                    f"{module.name:<10} indisponible",
                    value=module.name,
                    disabled=inv.reason,
                )
            )
    selected = (ask or _default_ask)(choices)
    if selected is None:
        raise KeyboardInterrupt
    chosen = set(selected)
    return [m.name for m in modules if m.name in chosen]


def confirm(message: str, ask: AskConfirm | None = None) -> bool:
    """Renvoie la réponse ; un Ctrl-C (`None`) lève KeyboardInterrupt, comme `checklist`.

    Le traiter comme un « non » ferait sortir l'appelant en code 1, indiscernable d'un
    refus délibéré : une interruption doit rester une interruption.
    """
    asker = ask or (lambda m: questionary.confirm(m, default=True).ask())
    answer = asker(message)
    if answer is None:
        raise KeyboardInterrupt
    return bool(answer)


def inventory_table(modules: Sequence[Module], inventories: dict[str, Inventory]) -> Table:
    table = Table(title="Inventaire", show_lines=False)
    table.add_column("Module", style="bold")
    table.add_column("Dispo")
    table.add_column("Éléments", justify="right")
    table.add_column("Taille", justify="right")
    table.add_column("Détail")
    for module in modules:
        inv = inventories[module.name]
        detail = module.description if inv.available else f"[red]{escape(inv.reason)}[/]"
        detail += _warning_lines(inv.warnings)
        table.add_row(
            module.name,
            "[green]oui[/]" if inv.available else "[red]non[/]",
            str(inv.items) if inv.available else "-",
            format_size(inv.size_bytes) if inv.available else "-",
            detail,
        )
    return table


def plan_table(
    action: str, names: Sequence[str], inventories: dict[str, Inventory], dest: Path
) -> Table:
    table = Table(title=f"Plan de {ACTION_LABEL.get(action, action)}")
    table.add_column("Module", style="bold")
    table.add_column("Action")
    table.add_column("Source")
    table.add_column("Destination")
    table.add_column("Taille", justify="right")
    for name in names:
        inv = inventories[name]
        module_path = escape(str(dest / name))
        src, dst = ("cette machine", module_path) if action == "backup" else (module_path, "$HOME")
        table.add_row(name, ACTION_LABEL.get(action, action), src, dst, format_size(inv.size_bytes))
    return table


def summary_table(results: dict[str, ModuleResult]) -> Table:
    table = Table(title="Résumé")
    table.add_column("Module", style="bold")
    table.add_column("Statut")
    table.add_column("Durée", justify="right")
    table.add_column("Taille", justify="right")
    table.add_column("Avert.", justify="right")
    table.add_column("Message")
    for name, result in results.items():
        style = STATUS_STYLE.get(result.status, "white")
        message = escape(result.message) + _warning_lines(result.warnings)
        table.add_row(
            name,
            f"[{style}]{result.status}[/]",
            f"{result.duration_s:.1f}s",
            format_size(result.size_bytes),
            str(len(result.warnings)),
            message,
        )
    return table


class RichTask(TaskHandle):
    """Poignée sur une tâche rich ; total et avancement suivis localement.

    `Progress.tasks` est une liste positionnelle : l'indexer par `TaskID` casse dès qu'une
    tâche est retirée. On garde donc l'état ici.
    """

    def __init__(self, progress: Progress, task_id: int, total: float | None = None) -> None:
        self._progress = progress
        self._id = task_id
        self._total = total
        self._completed = 0.0

    def advance(self, n: float = 1) -> None:
        self._completed += n
        self._progress.advance(self._id, n)

    def set_total(self, total: float) -> None:
        self._total = total
        self._progress.update(self._id, total=total)

    def set_completed(self, completed: float) -> None:
        self._completed = completed
        self._progress.update(self._id, completed=completed)

    def finish(self) -> None:
        total = self._total if self._total is not None else max(self._completed, 1)
        self._total = total
        self._completed = total
        self._progress.update(self._id, total=total, completed=total)


class RichProgress(ProgressReporter):
    """Barre globale plus une barre par module actif, rendues par rich."""

    def __init__(self, console: Console) -> None:
        self.progress = Progress(
            SpinnerColumn(),
            TextColumn("[progress.description]{task.description}"),
            BarColumn(),
            TaskProgressColumn(),
            TimeElapsedColumn(),
            console=console,
        )

    def __enter__(self) -> RichProgress:
        self.progress.start()
        return self

    def __exit__(self, *exc: object) -> None:
        self.progress.stop()

    def task(self, description: str, total: float | None = None) -> TaskHandle:
        task_id = self.progress.add_task(description, total=total)
        return RichTask(self.progress, task_id, total)
