"""Atomic file writes for every accumulated artifact.

A kill mid-write must never leave a torn file that poisons future runs —
the same tmp + os.replace guarantee write_matches gives matches.csv,
available to JSON artifacts and the XML response cache.
"""

from __future__ import annotations

import csv
import os
import uuid
from pathlib import Path


def _tmp_for(path: Path) -> Path:
    # unique per writer: a FIXED tmp name lets two concurrent processes
    # interleave writes into one inode and replace garbage into place
    return path.with_name(f"{path.name}.{uuid.uuid4().hex[:8]}.tmp")


def atomic_write_bytes(path: Path, data: bytes) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    tmp = _tmp_for(path)
    tmp.write_bytes(data)
    os.replace(tmp, path)


def atomic_write_text(path: Path, text: str) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    tmp = _tmp_for(path)
    tmp.write_text(text)
    os.replace(tmp, path)


def atomic_write_csv(path: Path, columns: list[str], rows: list[dict]) -> int:
    """Atomic CSV rewrite (tmp + os.replace). Returns the written file's
    mtime_ns so callers tracking their own writes avoid a re-stat race."""
    path.parent.mkdir(parents=True, exist_ok=True)
    tmp = _tmp_for(path)
    with tmp.open("w", newline="") as f:
        writer = csv.DictWriter(
            f, fieldnames=columns, extrasaction="ignore", restval=""
        )
        writer.writeheader()
        writer.writerows(rows)
    # stat the TMP inode before the replace: statting the destination after
    # could hand back a FOREIGN writer's mtime landing in the gap, and the
    # caller would record someone else's write as its own
    mtime = tmp.stat().st_mtime_ns
    os.replace(tmp, path)
    return mtime
