"""Manifeste d'un run : quels modules, quel statut, quelle machine."""

from __future__ import annotations

import getpass
import json
import os
import socket
import tempfile
from dataclasses import asdict, dataclass, field
from pathlib import Path
from typing import Any

from osx_backup import __version__
from osx_backup.context import Context
from osx_backup.modules.base import ModuleResult


@dataclass
class Manifest:
    created_at: str
    action: str
    hostname: str
    user: str
    home: str
    tool_version: str
    modules: dict[str, dict[str, Any]] = field(default_factory=dict)

    @property
    def module_names(self) -> list[str]:
        return [n for n, m in self.modules.items() if m.get("status") == "ok"]

    def to_dict(self) -> dict[str, Any]:
        return asdict(self)

    @classmethod
    def from_dict(cls, d: dict[str, Any]) -> Manifest:
        return cls(
            created_at=d["created_at"],
            action=d.get("action", "backup"),
            hostname=d.get("hostname", ""),
            user=d.get("user", ""),
            home=d.get("home", ""),
            tool_version=d.get("tool_version", ""),
            modules=d.get("modules", {}),
        )


def build_manifest(
    ctx: Context,
    action: str,
    results: dict[str, ModuleResult],
    *,
    previous: Manifest | None = None,
) -> Manifest:
    """Manifeste du run, complété des entrées de `previous` que ce run n'a pas rejouées.

    Le disque est un miroir cumulatif : un `backup --modules github` ne fait pas disparaître
    le brew, les secrets ou les bases déjà sauvegardés. Sans ce report, le manifeste ne
    décrirait plus que le dernier run et la restauration ne les proposerait plus.
    Chaque entrée porte le `created_at` du run qui l'a produite.
    """
    modules = {name: {**r.to_dict(), "created_at": ctx.timestamp} for name, r in results.items()}
    if previous is not None:
        for name, entry in previous.modules.items():
            if name not in modules:
                modules[name] = {
                    **entry,
                    "created_at": entry.get("created_at", previous.created_at),
                }
    return Manifest(
        created_at=ctx.timestamp,
        action=action,
        hostname=socket.gethostname(),
        user=getpass.getuser(),
        home=str(ctx.home),
        tool_version=__version__,
        modules=modules,
    )


def _atomic_write(path: Path, text: str) -> None:
    """Écrit via un temporaire du même dossier puis renomme.

    Un Ctrl-C au milieu laisse alors le fichier précédent intact plutôt qu'un
    JSON tronqué : `os.replace` est atomique sur le même système de fichiers.
    """
    fd, name = tempfile.mkstemp(dir=path.parent, prefix=f"{path.name}.", suffix=".tmp")
    tmp = Path(name)
    try:
        with os.fdopen(fd, "w", encoding="utf-8") as fh:
            fh.write(text)
            fh.flush()
            os.fsync(fh.fileno())
        os.replace(tmp, path)
    except BaseException:
        tmp.unlink(missing_ok=True)
        raise


def write_manifest(dest: Path, manifest: Manifest, *, latest: bool = True) -> Path:
    """Écrit l'historique, et `manifest.json` sauf si `latest=False`.

    Une restauration ne doit pas écraser le `manifest.json` de la sauvegarde : c'est lui
    qui décrit le contenu du disque. Elle n'écrit donc que son entrée d'historique.
    """
    text = json.dumps(manifest.to_dict(), indent=2, ensure_ascii=False)
    history = dest / "manifests"
    history.mkdir(parents=True, exist_ok=True)
    stamp = manifest.created_at.replace(":", "-")
    history_path = history / (f"{stamp}.json" if latest else f"{stamp}-{manifest.action}.json")
    _atomic_write(history_path, text)
    if not latest:
        return history_path
    path = dest / "manifest.json"
    _atomic_write(path, text)
    return path


def read_manifest(dest: Path) -> Manifest | None:
    path = dest / "manifest.json"
    if not path.exists():
        return None
    return Manifest.from_dict(json.loads(path.read_text(encoding="utf-8")))
