import os
import re
import shutil
from datetime import datetime
from pathlib import Path
from typing import Literal

SyncActionLabel = Literal["created", "updated", "skipped", "error", "copied"]

_MAX_BACKUPS = 3

# Characters that must not appear in a component-derived filename segment.
_UNSAFE_FILENAME_CHARS = re.compile(r'[/\\<>:"|?*\x00-\x1f]')


def sanitize_component_name(name: str) -> str:
    """Return a filesystem-safe filename segment derived from *name*.

    Strips characters that could be used for path traversal or that are
    illegal on common operating systems (Windows, macOS, Linux).  The
    resulting string is guaranteed to be non-empty and to contain no path
    separators, null bytes, or control characters.

    Raises ValueError if *name* is empty or reduces to an empty string
    after sanitization (e.g. a name consisting entirely of dots/spaces).
    """
    if not name:
        raise ValueError("Component name must not be empty")
    # Replace all unsafe characters with underscores
    safe = _UNSAFE_FILENAME_CHARS.sub("_", name)
    # Collapse any sequences of dots that would form a relative traversal
    # segment (e.g. "..", "...", "....") into a single underscore.
    safe = re.sub(r"^\.+$", "_", safe)
    # Strip surrounding whitespace and dots that Windows/macOS treat as
    # trailing noise (they can cause subtle issues).
    safe = safe.strip(". ")
    if not safe:
        raise ValueError(f"Component name {name!r} reduces to an empty string after sanitization")
    return safe


def _cleanup_backups(parent: Path, stem: str) -> None:
    """Remove all but the _MAX_BACKUPS most recent backups for a given file stem."""
    backups = sorted(parent.glob(f"{stem}.bak.*"))
    for old in backups[:-_MAX_BACKUPS]:
        if old.is_dir():
            shutil.rmtree(old)
        else:
            old.unlink(missing_ok=True)


def backup(target: Path) -> Path:
    """Move *target* to a timestamped backup file and return the backup path.

    Includes microseconds (%f) to avoid collisions when called multiple times
    within the same second (e.g. in automated scripts or tests).
    """
    stamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
    backup_path = target.parent / f"{target.name}.bak.{stamp}"
    shutil.move(str(target), str(backup_path))
    _cleanup_backups(target.parent, target.name)
    return backup_path


def create_symlink(
    source: Path, target: Path, is_directory: bool = False
) -> tuple[SyncActionLabel, Path | None]:
    """Create a symlink at *target* pointing to *source*.

    Returns a ``(action, backup_path)`` tuple where *action* is one of
    ``"skipped"``, ``"created"``, or ``"copied"`` (Windows fallback) and
    *backup_path* is the path of any backup file created, or ``None``.
    Backs up any existing non-symlink file/directory before replacing.
    """
    backup_path: Path | None = None
    if target.is_symlink():
        try:
            if target.resolve() == source.resolve():
                return "skipped", None
        except OSError:
            pass
        target.unlink()
    elif target.exists():
        backup_path = backup(target)

    target.parent.mkdir(parents=True, exist_ok=True)

    try:
        os.symlink(source, target)
        return "created", backup_path
    except (OSError, NotImplementedError):
        # Windows fallback: copy instead of symlink
        if is_directory:
            shutil.copytree(str(source), str(target))
        else:
            shutil.copy2(str(source), str(target))
        return "copied", backup_path


def write_file(content: str, target: Path) -> tuple[SyncActionLabel, Path | None]:
    """Write *content* to *target*.

    Returns a ``(action, backup_path)`` tuple where *action* is one of
    ``"skipped"``, ``"updated"``, or ``"created"`` and *backup_path* is the
    path of any backup file created, or ``None``.
    """
    backup_path: Path | None = None
    action: SyncActionLabel
    if target.is_symlink():
        target.unlink()
        action = "created"
    elif target.exists():
        if target.read_text() == content:
            return "skipped", None
        backup_path = backup(target)
        action = "updated"
    else:
        action = "created"

    target.parent.mkdir(parents=True, exist_ok=True)
    target.write_text(content)
    return action, backup_path
