"""Contexte d'exécution partagé par tous les modules."""

from __future__ import annotations

import json
from collections.abc import Sequence
from dataclasses import dataclass, field
from datetime import datetime
from pathlib import Path
from typing import Any

from rich.console import Console
from rich.markup import escape

from osx_backup.fsutil import copy_tree, move_aside
from osx_backup.runner import CommandRunner


def _now() -> str:
    return datetime.now().strftime("%Y-%m-%d_%H-%M-%S")


@dataclass
class Context:
    home: Path
    dest: Path
    runner: CommandRunner
    console: Console = field(default_factory=Console)
    dry_run: bool = False
    yes: bool = False
    verbose: bool = False
    force: bool = False
    docker_no_stop: bool = False
    docker_include_anonymous: bool = False
    passphrase: str | None = None
    timestamp: str = field(default_factory=_now)
    log_path: Path | None = None
    applications_dir: Path = Path("/Applications")
    map_path: Path | None = None
    old_home: Path | None = None

    @property
    def pre_restore_dir(self) -> Path:
        return self.home / ".osx-backup-pre-restore" / self.timestamp

    def module_dir(self, name: str) -> Path:
        d = self.dest / name
        if not self.dry_run:
            d.mkdir(parents=True, exist_ok=True)
        return d

    def log(self, message: str) -> None:
        if self.log_path is not None:
            self.log_path.parent.mkdir(parents=True, exist_ok=True)
            stamp = datetime.now().strftime("%H:%M:%S")
            with self.log_path.open("a", encoding="utf-8") as fh:
                fh.write(f"[{stamp}] {message}\n")
        if self.verbose:
            self.console.print(message, markup=False, highlight=False)

    def warn(self, message: str) -> None:
        self.console.print(f"[yellow]⚠ {escape(message)}[/]")
        self.log("WARN " + message)

    def place(self, src: Path, dst: Path, excludes: Sequence[str] = ()) -> int:
        """Copie src vers dst dans $HOME en mettant l'existant de côté. Retourne les octets.

        La source est vérifiée **avant** de toucher à dst : sans cela un src manquant
        déplacerait l'existant puis n'écrirait rien, faisant disparaître le fichier.
        """
        if self.dry_run:
            self.log(f"[dry-run] place {src} -> {dst}")
            return 0
        if not src.exists() and not src.is_symlink():
            raise FileNotFoundError(f"source introuvable, {dst} laissé intact : {src}")
        moved = move_aside(dst, self.pre_restore_dir, self.home)
        if moved is not None:
            self.log(f"existant déplacé : {dst} -> {moved}")
        return copy_tree(src, dst, excludes)

    def write_text(self, path: Path, text: str) -> None:
        if self.dry_run:
            self.log(f"[dry-run] write {path} ({len(text)} chars)")
            return
        path.parent.mkdir(parents=True, exist_ok=True)
        path.write_text(text, encoding="utf-8")

    def write_json(self, path: Path, obj: Any) -> None:
        self.write_text(path, json.dumps(obj, indent=2, ensure_ascii=False, sort_keys=True))
