"""Applications de /Applications non gérées par Homebrew : liste à réinstaller à la main."""

from __future__ import annotations

import plistlib
from dataclasses import dataclass
from pathlib import Path

from osx_backup.context import Context
from osx_backup.modules.base import Inventory, Module, ModuleResult, ProgressReporter, register


@dataclass(frozen=True)
class App:
    name: str
    version: str
    source: str


def app_version(app: Path) -> str:
    """Version du bundle, ou "" si l'Info.plist est absent, tronqué ou inattendu.

    Le filet est volontairement large : un plist XML corrompu remonte une
    `xml.parsers.expat.ExpatError` et une racine non-dict un `AttributeError`. Un seul
    bundle abîmé ne doit jamais faire tomber l'inventaire de /Applications.
    """
    plist = app / "Contents" / "Info.plist"
    try:
        with plist.open("rb") as fh:
            data = plistlib.load(fh)
        return str(data.get("CFBundleShortVersionString") or data.get("CFBundleVersion") or "")
    except Exception:
        return ""


def app_source(app: Path) -> str:
    if (app / "Contents" / "_MASReceipt").exists():
        return "App Store"
    if "Setapp" in app.parts:
        return "Setapp"
    return "manuel"


def cask_app_names(caskroom: Path | None) -> set[str]:
    if caskroom is None or not caskroom.is_dir():
        return set()
    return {p.name for p in caskroom.glob("*/*/*.app")} | {
        p.name for p in caskroom.glob("*/*/*/*.app")
    }


def caskroom_path(ctx: Context) -> Path | None:
    if ctx.runner.which("brew") is None:
        return None
    res = ctx.runner.run(["brew", "--caskroom"], check=False, readonly=True)
    return Path(res.stdout.strip()) if res.ok and res.stdout.strip() else None


def list_apps(applications_dir: Path, cask_names: set[str]) -> list[App]:
    candidates = sorted(applications_dir.glob("*.app"), key=lambda p: p.name.casefold()) + sorted(
        (applications_dir / "Setapp").glob("*.app"), key=lambda p: p.name.casefold()
    )
    return [
        App(p.name.removesuffix(".app"), app_version(p), app_source(p))
        for p in candidates
        if p.name not in cask_names
    ]


def _cell(value: str) -> str:
    return value.replace("|", "\\|")


def render_markdown(apps: list[App]) -> str:
    lines = [
        "# Applications à réinstaller manuellement",
        "",
        "Apps présentes dans /Applications mais absentes des casks Homebrew.",
        "",
        "| Application | Version | Source |",
        "|---|---|---|",
    ]
    lines += [f"| {_cell(a.name)} | {_cell(a.version)} | {a.source} |" for a in apps]
    return "\n".join(lines) + "\n"


@register
class AppsModule(Module):
    name = "apps"
    description = "Applications hors Homebrew (App Store, Setapp, dmg) : liste à réinstaller"
    order = 120

    def _apps(self, ctx: Context) -> tuple[list[App], list[str]]:
        """Liste les apps hors Homebrew et les avertissements de version illisible.

        Sans affichage : l'inventaire rend ses avertissements dans la table de `map`,
        les afficher ici les dupliquerait. Seul `backup` les signale au fil de l'eau.
        """
        apps = list_apps(ctx.applications_dir, cask_app_names(caskroom_path(ctx)))
        warnings = [f"version illisible : {app.name}" for app in apps if not app.version]
        return apps, warnings

    def inventory(self, ctx: Context) -> Inventory:
        if not ctx.applications_dir.is_dir():
            return Inventory(available=False, reason=f"{ctx.applications_dir} absent")
        apps, warnings = self._apps(ctx)
        return Inventory(items=len(apps), warnings=warnings)

    def backup(self, ctx: Context, progress: ProgressReporter) -> ModuleResult:
        apps, warnings = self._apps(ctx)
        for warning in warnings:
            ctx.warn(warning)
        task = progress.task("apps", total=1)
        ctx.write_text(ctx.module_dir(self.name) / "applications.md", render_markdown(apps))
        task.advance()
        task.finish()
        return ModuleResult.ok(f"{len(apps)} app(s) listée(s)", items=len(apps), warnings=warnings)

    def restore(self, ctx: Context, progress: ProgressReporter) -> ModuleResult:
        md = ctx.dest / self.name / "applications.md"
        if not md.exists():
            return ModuleResult.skipped("pas de liste d'applications dans la sauvegarde")
        text = md.read_text(encoding="utf-8")
        ctx.console.print(text, markup=False, highlight=False)
        count = (
            sum(1 for line in text.splitlines() if line.startswith("| ") and "---" not in line) - 1
        )
        return ModuleResult.ok(
            f"liste affichée : {max(count, 0)} app(s) à réinstaller à la main ({md})",
            items=max(count, 0),
        )
