"""插件目录 tar.gz 备份与恢复。"""

from __future__ import annotations

import re
import tarfile
from datetime import datetime, timezone
from pathlib import Path


def _safe_archive_name(name: str) -> bool:
    return bool(re.match(r"^[a-zA-Z0-9._-]+$", name)) and ".." not in name


def backup_plugin_directory(
    plugin_dir: Path,
    backups_dir: Path,
    *,
    keep: int = 20,
) -> Path:
    """
    将 plugin_dir 整目录打包为 backups_dir/<plugin_id>/<UTC>.tar.gz。
    删除超出 keep 份的旧备份（仅同一插件 id 目录下）。
    """
    plugin_id = plugin_dir.name
    target_root = backups_dir / plugin_id
    target_root.mkdir(parents=True, exist_ok=True)
    ts = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
    archive_path = target_root / f"{ts}.tar.gz"
    with tarfile.open(archive_path, "w:gz") as tf:
        tf.add(plugin_dir, arcname=plugin_id, filter=_tar_reset)
    _prune_old(target_root, keep=max(1, keep))
    return archive_path


def _tar_reset(ti: tarfile.TarInfo) -> tarfile.TarInfo | None:
    ti.uid = ti.gid = 0
    ti.uname = ti.gname = "root"
    return ti


def _prune_old(backup_plugin_dir: Path, *, keep: int) -> None:
    files = sorted(
        [p for p in backup_plugin_dir.iterdir() if p.is_file() and p.suffix == ".gz"],
        key=lambda p: p.name,
    )
    while len(files) > keep:
        oldest = files.pop(0)
        try:
            oldest.unlink()
        except OSError:
            break


def list_plugin_backups(backups_dir: Path, plugin_id: str) -> list[str]:
    d = backups_dir / plugin_id
    if not d.is_dir():
        return []
    return sorted(p.name for p in d.iterdir() if p.is_file() and p.suffix == ".gz")


def restore_plugin_directory(
    plugin_id: str,
    archive_name: str,
    plugins_root: Path,
    backups_dir: Path,
    *,
    backup_current_first: bool = True,
    keep: int = 20,
) -> tuple[bool, str]:
    if not _safe_archive_name(plugin_id) or not _safe_archive_name(archive_name):
        return False, "invalid plugin_id or archive_name"
    arc = backups_dir / plugin_id / archive_name
    if not arc.is_file():
        return False, f"backup not found: {arc}"
    import shutil

    plugin_dir = plugins_root / plugin_id
    plugins_root.mkdir(parents=True, exist_ok=True)
    if plugin_dir.is_dir():
        if backup_current_first:
            backup_plugin_directory(plugin_dir, backups_dir, keep=keep)
        shutil.rmtree(plugin_dir)
    with tarfile.open(arc, "r:gz") as tf:
        if not tf.getmembers():
            return False, "empty archive"
        try:
            tf.extractall(plugins_root, filter="data")  # type: ignore[call-arg]
        except TypeError:
            tf.extractall(plugins_root)
    plugin_dir = plugins_root / plugin_id
    if not (plugin_dir / "plugin.yaml").is_file():
        return False, "restore layout unexpected: missing plugin.yaml after extract"
    return True, f"restored from {archive_name}"
