import shutil
import tempfile
from pathlib import Path

from caelestia.utils.paths import cache_dir, config_dir, data_dir, dots_dir, state_dir

# Dirs to never prune even if empty
_PROTECTED_DIRS = frozenset({Path.home(), config_dir, data_dir, state_dir, cache_dir})


class Deployer:
    """Places files from the dots clone into their destinations."""

    def __init__(self):
        self.deployed_files: dict[str, str] = {}

    def place(self, src: Path, dest: Path) -> None:
        """Place a whole entry (file or directory tree), replacing any existing dest."""

        if src.is_dir():
            self.place_dir(src, dest)
        else:
            self.place_file(src, dest)

    def place_dir(self, src: Path, dest: Path) -> None:
        """Place a directory tree recursively, overwriting any existing dest files."""

        if dest.is_symlink() or dest.is_file():
            self.remove(dest)

        dest.mkdir(parents=True, exist_ok=True)
        for path in src.rglob("*"):
            if path.is_file():
                self.place_file(path, dest / path.relative_to(src))
            elif path.is_dir():
                (dest / path.relative_to(src)).mkdir(parents=True, exist_ok=True)

    def place_file(self, src: Path, dest: Path, record: bool = True) -> None:
        """Atomically place a single file, replacing any existing dest."""

        if dest.is_dir() and not dest.is_symlink():
            self.remove(dest)

        dest.parent.mkdir(parents=True, exist_ok=True)
        f = tempfile.NamedTemporaryFile(dir=dest.parent, delete=False)
        f.close()
        try:
            shutil.copyfile(src, f.name)
            shutil.copymode(src, f.name)
            Path(f.name).replace(dest)
        except BaseException:
            Path(f.name).unlink()
            raise

        if record:
            # Keep relative to dots dir
            self.deployed_files[str(dest)] = str(src.relative_to(dots_dir))

    def write_new(self, src: Path, dest: Path) -> Path:
        """Write the upstream version alongside dest as <dest>.new and return that path."""

        new_path = dest.parent / f"{dest.name}.new"
        self.place_file(src, new_path, record=False)
        return new_path

    def remove(self, path: Path) -> None:
        if path.is_symlink() or path.is_file():
            path.unlink()
        elif path.is_dir():
            shutil.rmtree(path)

    def prune_empty_dirs(self, start: Path, stop: Path) -> None:
        """Removes dirs recursively from start to stop.

        Will never prune protected dirs (home, config, cache, etc).
        """

        parent = start.parent
        while parent != stop and stop in parent.parents and parent not in _PROTECTED_DIRS:
            try:
                parent.rmdir()
            except OSError:
                break
            parent = parent.parent
