"""JSON-file helpers: tolerant load + atomic write.

Three repos (aionetiface updater/keystore/nat_cache, sidewire
broker_hint_cache, warpgate demo keystore_cleanup) had drifted copies
of the same try/open/json.load/except + tmp/open/json.dump/os.replace
pattern.  Centralised here.

Note: these are sync; callers that want to keep them off the loop wrap
them with run_in_executor.  updater.py already uses that pattern for
read_text_sync / write_text_atomic_sync.
"""
import json
import os

from .error_logger import log_exception


__all__ = [
    "load_json_or_default",
    "atomic_write_json",
]


def load_json_or_default(path, default):
    """Load JSON from `path`; return `default` if file missing or unparseable.

    Catches OSError (file not present, permission denied) and ValueError
    (json decode error).  No logging on the OSError path -- missing
    sidecars are the common case and not noteworthy.  Decode errors
    fall through silently too; if the caller wants to know they should
    inspect the returned default vs a sentinel themselves.

    If `default` is callable, it is invoked with no args to produce the
    fallback -- mirrors collections.defaultdict semantics so callers
    don't share a mutable dict across invocations.
    """
    try:
        with open(path, "r", encoding="utf-8") as fp:
            data = json.load(fp)
    except (OSError, ValueError):
        return default() if callable(default) else default
    return data


def atomic_write_json(path, data, **dump_kwargs):
    """Write `data` as JSON to `path` atomically.

    Strategy: write to `path + ".tmp"`, then os.replace onto the final
    name -- replace is atomic on POSIX and on Windows since Vista.
    OSError is logged but not raised; callers writing sidecars / caches
    can survive a transient disk-full or permission flap.

    `dump_kwargs` are passed straight to json.dump (indent, sort_keys,
    default, etc.).
    """
    tmp = path + ".tmp"
    try:
        with open(tmp, "w", encoding="utf-8") as fp:
            json.dump(data, fp, **dump_kwargs)
        os.replace(tmp, path)
    except OSError:
        log_exception()
