"""Base des modules miroir rsync (github, documents)."""

from __future__ import annotations

from pathlib import Path
from typing import ClassVar

from osx_backup.context import Context
from osx_backup.modules.base import (
    Inventory,
    Module,
    ModuleResult,
    ProgressReporter,
    stderr_detail,
)
from osx_backup.rsync import RSYNC_MISSING, build_command, estimate, find_gnu_rsync, parse_progress2

# « some files vanished before they could be transferred » : banal sur un $HOME vivant
# (caches, fichiers temporaires), le miroir produit reste exploitable.
VANISHED_FILES = 24
VANISHED_FILES_WARNING = "des fichiers ont disparu pendant la copie (rsync code 24)"


class RsyncModule(Module):
    """Miroir d'un dossier de $HOME vers `<dest>/<module>/` et retour."""

    source_rel: ClassVar[str]
    excludes: ClassVar[list[str]]
    delete_on_backup: ClassVar[bool] = True

    def source(self, ctx: Context) -> Path:
        return ctx.home / self.source_rel

    def inventory(self, ctx: Context) -> Inventory:
        src = self.source(ctx)
        if not src.is_dir():
            return Inventory(available=False, reason=f"~/{self.source_rel} absent")
        rsync = find_gnu_rsync(ctx.runner)
        if rsync is None:
            return Inventory(available=False, reason=RSYNC_MISSING)
        files, size, code = estimate(ctx.runner, rsync, src, ctx.dest / self.name, self.excludes)
        inv = Inventory(items=files, size_bytes=size, details={"rsync": rsync})
        if code != 0:
            # Le module reste utilisable : seule la prévision de taille manque.
            inv.warnings.append(f"estimation rsync impossible (code {code})")
        return inv

    def _mirror(
        self,
        ctx: Context,
        progress: ProgressReporter,
        src: Path,
        dst: Path,
        delete: bool,
        backup_dir: Path | None = None,
    ) -> ModuleResult:
        rsync = find_gnu_rsync(ctx.runner)
        if rsync is None:
            return ModuleResult.failed(RSYNC_MISSING)
        if not ctx.dry_run:
            dst.mkdir(parents=True, exist_ok=True)
        cmd = build_command(rsync, src, dst, self.excludes, delete=delete, backup_dir=backup_dir)
        task = progress.task(self.name, total=100)
        transferred = 0

        def on_line(line: str) -> None:
            nonlocal transferred
            parsed = parse_progress2(line)
            if parsed:
                octets, percent = parsed
                if octets:
                    transferred = octets
                task.set_completed(percent)

        res = ctx.runner.stream(cmd, on_line)
        task.finish()
        # Appelé hors de la branche d'échec : le stderr d'un code 24 est tout aussi utile.
        detail = stderr_detail(ctx, f"rsync ({self.name})", res)
        if res.returncode == VANISHED_FILES:
            return ModuleResult.ok(
                "miroir à jour", size_bytes=transferred, warnings=[VANISHED_FILES_WARNING]
            )
        if not res.ok:
            return ModuleResult.failed(
                f"rsync a échoué (code {res.returncode}){detail}",
                size_bytes=transferred,
            )
        return ModuleResult.ok("miroir à jour", size_bytes=transferred)

    def backup(self, ctx: Context, progress: ProgressReporter) -> ModuleResult:
        return self._mirror(
            ctx, progress, self.source(ctx), ctx.module_dir(self.name), self.delete_on_backup
        )

    def restore(self, ctx: Context, progress: ProgressReporter) -> ModuleResult:
        src = ctx.dest / self.name
        if not src.is_dir():
            return ModuleResult.skipped(f"pas de dossier {self.name} dans la sauvegarde")
        # Jamais de --delete au retour : le miroir complète $HOME, il ne l'élague pas.
        # --backup-dir : un fichier local modifié part dans ~/.osx-backup-pre-restore/<ts>/
        # au lieu d'être écrasé, comme le fait `ctx.place` pour les autres modules.
        return self._mirror(
            ctx,
            progress,
            src,
            self.source(ctx),
            delete=False,
            backup_dir=ctx.pre_restore_dir / self.source_rel,
        )
