"""Détection d'un rsync GNU et helpers de commande/progression."""

from __future__ import annotations

import re
from collections.abc import Sequence
from pathlib import Path

from osx_backup.runner import CommandRunner

RSYNC_MISSING = "rsync GNU absent : brew install rsync"
CANDIDATES = ["/opt/homebrew/bin/rsync", "/usr/local/bin/rsync"]
VERSION_RE = re.compile(r"rsync\s+version\s+(\d+)\.")
PROGRESS_RE = re.compile(r"^\s*(\S+)\s+(\d+)%")
BYTES_RE = re.compile(r"^[\d,]+$")
STAT_FILES_RE = re.compile(r"Number of files:\s*([\d,]+)")
STAT_TOTAL_RE = re.compile(r"Total file size:\s*([\d,]+)")


def find_gnu_rsync(runner: CommandRunner) -> str | None:
    """Premier rsync GNU >= 3 parmi les chemins connus, sinon None.

    macOS livre openrsync, qui n'a ni `--info=progress2` ni `--stats` exploitables :
    il s'annonce « openrsync: protocol version 29 / rsync version 2.6.9 compatible »,
    d'où le rejet explicite du nom en plus du numéro de version.
    """
    candidates = list(CANDIDATES)
    found = runner.which("rsync")
    if found and found not in candidates:
        candidates.append(found)
    for candidate in candidates:
        try:
            res = runner.run([candidate, "--version"], check=False, readonly=True)
        except OSError:
            # Chemin candidat inexistant ou non exécutable : simplement pas celui-là.
            continue
        if not res.ok or "openrsync" in res.stdout:
            continue
        match = VERSION_RE.search(res.stdout)
        if match and int(match.group(1)) >= 3:
            return candidate
    return None


def build_command(
    rsync: str,
    src: Path,
    dst: Path,
    excludes: Sequence[str],
    *,
    delete: bool,
    dry_run: bool = False,
    stats: bool = False,
    backup_dir: Path | None = None,
) -> list[str]:
    """Commande de miroir `src/` -> `dst/` (les `/` finaux font le contenu, pas le dossier).

    `backup_dir` : dossier où rsync range les fichiers qu'il écrase ou supprime. Sans lui,
    `-a` remplace en silence un fichier modifié de $HOME (spec §8 : jamais d'écrasement
    sans copie).
    """
    cmd = [rsync, "-a", "--no-inc-recursive", "--info=progress2"]
    if delete:
        cmd.append("--delete")
    if backup_dir is not None:
        cmd += ["--backup", f"--backup-dir={backup_dir}"]
    if dry_run:
        cmd.append("--dry-run")
    if stats:
        cmd.append("--stats")
    for pattern in excludes:
        cmd += ["--exclude", pattern]
    cmd += [f"{src}/", f"{dst}/"]
    return cmd


def parse_progress2(line: str) -> tuple[int, int] | None:
    """(octets transférés, pourcentage) d'une ligne `--info=progress2`, sinon None.

    Les octets valent 0 si le champ n'est pas un nombre : avec `--human-readable`
    (imposé par un popt de l'utilisateur) rsync écrit « 1.23G », et mieux vaut une
    barre qui avance sans total qu'une progression figée.
    """
    match = PROGRESS_RE.match(line)
    if not match:
        return None
    raw, percent = match.group(1), int(match.group(2))
    octets = int(raw.replace(",", "")) if BYTES_RE.match(raw) else 0
    return octets, percent


def _to_int(match: re.Match[str] | None) -> int:
    return int(match.group(1).replace(",", "")) if match else 0


def parse_stats(text: str) -> tuple[int, int]:
    """(nombre de fichiers, octets « Total file size ») d'une sortie `--stats`."""
    return _to_int(STAT_FILES_RE.search(text)), _to_int(STAT_TOTAL_RE.search(text))


def estimate(
    runner: CommandRunner, rsync: str, src: Path, dst: Path, excludes: Sequence[str]
) -> tuple[int, int, int]:
    """(fichiers, octets, code de retour) du `--dry-run --stats` qui estime le miroir.

    Le code est rendu pour que l'appelant distingue « rien à copier » d'une estimation
    ratée : les deux donnent (0, 0).
    """
    cmd = build_command(rsync, src, dst, excludes, delete=False, dry_run=True, stats=True)
    res = runner.run(cmd, check=False, readonly=True)
    files, size = parse_stats(res.stdout)
    return files, size, res.returncode
