"""Atomic file-write utilities (U3 — R6).

Provides :func:`write_text_atomic` which writes text content to a file
using the write-temp + fsync + os.replace pattern (KTD-4). This
guarantees that:

- A crash during the write leaves the original file intact (the temp
  file is abandoned).
- Concurrent writers don't produce mixed content (``os.replace`` is
  atomic on POSIX).
- The file's directory entry is durable after a crash (parent-dir
  fsync).

The function is intentionally minimal — it operates on ``str`` content.
Callers that need to serialize YAML/dict/etc. should do so before
calling.
"""

from __future__ import annotations

import os
import tempfile
from pathlib import Path


def write_text_atomic(path: str | Path, content: str, *, encoding: str = "utf-8") -> None:
    """Write ``content`` to ``path`` atomically.

    Flow:
      1. Write content to a temporary file in the same directory.
      2. ``fsync`` the temp file (data durability).
      3. ``os.replace(temp, path)`` — atomic rename on POSIX.
      4. ``fsync`` the parent directory (directory-entry durability).

    Args:
        path: Target file path. The parent directory must exist.
        content: Text content to write.
        encoding: Text encoding (default ``utf-8``).

    Raises:
        PermissionError: If the target directory or file is not writable.
        OSError: For other filesystem errors.
    """
    target = Path(path)
    parent = target.parent
    parent.mkdir(parents=True, exist_ok=True)

    # Create the temp file in the same directory so os.replace stays
    # within a single filesystem (required for atomicity).
    fd, tmp_name = tempfile.mkstemp(
        dir=str(parent),
        prefix=f".{target.name}.tmp.",
        suffix=".partial",
    )
    tmp_path = Path(tmp_name)
    try:
        with os.fdopen(fd, "w", encoding=encoding) as f:
            f.write(content)
            f.flush()
            os.fsync(f.fileno())
        os.replace(tmp_path, target)
        # fsync the parent directory so the rename is durable.
        _fsync_dir(parent)
    except BaseException:
        # On any failure (including crash), clean up the temp file.
        # os.replace already succeeded → tmp_path no longer exists.
        try:
            tmp_path.unlink(missing_ok=True)
        except OSError:
            pass
        raise


def _fsync_dir(path: Path) -> None:
    """Best-effort fsync of a directory (POSIX only, no-op on Windows)."""
    if os.name != "posix":
        return
    try:
        dir_fd = os.open(str(path), os.O_RDONLY)
        try:
            os.fsync(dir_fd)
        finally:
            os.close(dir_fd)
    except OSError:
        # Some filesystems (e.g. network mounts) don't support fsync
        # on directories. Swallow rather than crash — the rename is
        # still atomic, just not guaranteed durable.
        pass
