"""Atomic JSON persistence for small state files.

Every file this package writes (tokens, session ledgers, cursors) is read by
sibling processes that may be mid-read when we write. Writing through a temp
file and `os.replace` makes the swap atomic, so a reader either sees the whole
previous file or the whole new one — never a half-written one.
"""

import json
import os
import tempfile
from pathlib import Path
from typing import Any

SECRET_FILE_MODE = 0o600


def write_json_atomic(path: Path, payload: Any, *, mode: int = SECRET_FILE_MODE) -> None:
    """Write `payload` as JSON to `path`, atomically, with restrictive permissions."""
    path.parent.mkdir(parents=True, exist_ok=True)
    fd, temp_name = tempfile.mkstemp(dir=str(path.parent), prefix=f".{path.name}.", suffix=".tmp")
    try:
        os.fchmod(fd, mode)
        with os.fdopen(fd, "w", encoding="utf-8") as handle:
            json.dump(payload, handle)
            handle.flush()
            os.fsync(handle.fileno())
        os.replace(temp_name, path)
    except BaseException:
        _discard(temp_name)
        raise


def read_json(path: Path) -> Any | None:
    """Return the parsed contents of `path`, or None if unreadable.

    A corrupt or missing state file is never fatal: the caller re-derives the
    state instead. Raising here would turn a stale cache into a dead server.
    """
    try:
        with path.open(encoding="utf-8") as handle:
            return json.load(handle)
    except (OSError, ValueError):
        return None


def _discard(name: str) -> None:
    try:
        os.unlink(name)
    except OSError:
        pass
