"""Crash-safe JSON file helpers shared by the on-disk stores (sessions,
dashboards, modes, outputs).

Two jobs:
- `atomic_write_json`: write via a temp file in the same dir + os.replace, so a
  crash or power-loss mid-write can never leave a half-written file that bricks
  the next load.
- `read_json_or_none`: read+parse one file, returning None instead of throwing
  on a garbled/unreadable file, so a single corrupt file can't crash a whole
  load-all path (and take down boot or a page with it).

Settings/seq_log/auth keep their own inlined atomic writers; this is for the
stores that were still doing plain open()+dump().
"""
import json
import logging
import os
import tempfile
import threading
import time

logger = logging.getLogger(__name__)

_write_lock = threading.Lock()


def atomic_write_json(path: str, payload, *, indent: int = 2) -> None:
    directory = os.path.dirname(path) or "."
    with _write_lock:
        os.makedirs(directory, exist_ok=True)
        fd, tmp = tempfile.mkstemp(prefix=".tmp-", suffix=".json", dir=directory)
        try:
            with os.fdopen(fd, "w", encoding="utf-8") as f:
                json.dump(payload, f, indent=indent)
            # Windows: Defender can briefly hold the destination open; a couple of retries covers every real case.
            for attempt in range(3):
                try:
                    os.replace(tmp, path)
                    return
                except PermissionError:
                    if attempt == 2:
                        raise
                    time.sleep(0.1)
        except Exception:
            try:
                os.unlink(tmp)
            except OSError:
                pass
            raise


def read_json_or_none(path: str) -> dict | None:
    """Parse `path`; return None (and log) on a missing/garbled file rather than
    raising. Schema validation is the caller's job, kept separate so a real
    corruption is skipped while a future-schema file isn't mistaken for one."""
    try:
        with open(path, encoding="utf-8") as f:
            return json.load(f)
    except FileNotFoundError:
        return None
    except (json.JSONDecodeError, UnicodeDecodeError, OSError) as e:
        logger.warning("Skipping unreadable JSON file %s: %s", os.path.basename(path), e)
        return None
