"""Homebrew : Brewfile et services."""

from __future__ import annotations

import json
import re
from dataclasses import asdict, dataclass

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

BREW_MISSING = "Homebrew absent : https://brew.sh"
RSYNC_LINE_RE = re.compile(r'^brew\s+"rsync"', re.MULTILINE)


@dataclass(frozen=True)
class Service:
    name: str
    status: str


def parse_services(text: str) -> list[Service]:
    try:
        raw = json.loads(text or "[]")
    except json.JSONDecodeError:
        return []
    return [Service(str(s["name"]), str(s.get("status") or "none")) for s in raw]


def ensure_rsync_in_brewfile(text: str) -> str:
    """Force `brew "rsync"` : le rsync GNU est un prérequis des modules github/documents."""
    if RSYNC_LINE_RE.search(text):
        return text
    if text and not text.endswith("\n"):
        text += "\n"
    return text + 'brew "rsync"\n'


def _lines(ctx: Context, cmd: list[str]) -> list[str]:
    res = ctx.runner.run(cmd, check=False, readonly=True)
    return [line for line in res.stdout.splitlines() if line.strip()] if res.ok else []


def _services(ctx: Context) -> list[Service]:
    res = ctx.runner.run(["brew", "services", "list", "--json"], check=False, readonly=True)
    return parse_services(res.stdout)


@register
class BrewModule(Module):
    name = "brew"
    description = "Homebrew : formulae, casks, taps et services"
    order = 10

    def inventory(self, ctx: Context) -> Inventory:
        if ctx.runner.which("brew") is None:
            return Inventory(available=False, reason=BREW_MISSING)
        formulae = _lines(ctx, ["brew", "list", "--formula"])
        casks = _lines(ctx, ["brew", "list", "--cask"])
        taps = _lines(ctx, ["brew", "tap"])
        started = sum(1 for s in _services(ctx) if s.status == "started")
        return Inventory(
            items=len(formulae) + len(casks) + len(taps),
            details={
                "formulae": len(formulae),
                "casks": len(casks),
                "taps": len(taps),
                "services_started": started,
            },
        )

    def backup(self, ctx: Context, progress: ProgressReporter) -> ModuleResult:
        out = ctx.module_dir(self.name)
        brewfile = out / "Brewfile"
        task = progress.task("brew", total=2)
        res = ctx.runner.run(
            ["brew", "bundle", "dump", "--file", str(brewfile), "--force", "--describe"],
            check=False,
        )
        if not res.ok:
            task.finish()
            # Sans Brewfile le module n'a rien sauvegardé : c'est un échec, pas un warning.
            return ModuleResult.failed(
                f"brew bundle dump a échoué (code {res.returncode})"
                f"{stderr_detail(ctx, 'brew bundle dump', res)}"
            )
        if not ctx.dry_run and brewfile.exists():
            brewfile.write_text(
                ensure_rsync_in_brewfile(brewfile.read_text(encoding="utf-8")), encoding="utf-8"
            )
        task.advance()
        services = _services(ctx)
        ctx.write_json(out / "services.json", [asdict(s) for s in services])
        task.advance()
        task.finish()
        return ModuleResult.ok("Brewfile et services exportés", items=len(services))

    def restore(self, ctx: Context, progress: ProgressReporter) -> ModuleResult:
        brewfile = ctx.dest / self.name / "Brewfile"
        if not brewfile.exists():
            return ModuleResult.skipped("pas de Brewfile dans la sauvegarde")
        warnings: list[str] = []
        task = progress.task("brew bundle install", total=None)
        # `brew bundle install` continue après une formule cassée : on garde son code
        # de retour comme un simple avertissement, le reste de la restauration doit suivre.
        res = ctx.runner.run(["brew", "bundle", "install", "--file", str(brewfile)], check=False)
        if not res.ok:
            warnings.append(
                f"brew bundle a signalé des erreurs (code {res.returncode})"
                f"{stderr_detail(ctx, 'brew bundle install', res)}"
            )
        task.finish()
        started = [s for s in self._saved_services(ctx) if s.status == "started"]
        task = progress.task("services brew", total=len(started))
        for service in started:
            r = ctx.runner.run(["brew", "services", "start", service.name], check=False)
            if not r.ok:
                warnings.append(
                    f"service {service.name} : démarrage impossible"
                    f"{stderr_detail(ctx, f'brew services start {service.name}', r)}"
                )
            task.advance()
        task.finish()
        return ModuleResult.ok(
            f"Brewfile installé, {len(started)} service(s) démarré(s)",
            items=len(started),
            warnings=warnings,
        )

    def _saved_services(self, ctx: Context) -> list[Service]:
        path = ctx.dest / self.name / "services.json"
        return parse_services(path.read_text(encoding="utf-8")) if path.exists() else []
