from __future__ import annotations

import copy
import json
import logging
import os
import threading
from typing import Any, Callable

from persistence import MUGI_DIR, secure_atomic_write

_log = logging.getLogger(__name__)

_CACHE_LOCK = threading.Lock()
_CACHE: dict[str, Any] | None = None
_CACHE_MTIME: float | None = None
_WRITE_LOCK = threading.RLock()


def app_prefs_path() -> str:
    return os.path.join(MUGI_DIR, "app_prefs.json")


def invalidate_app_prefs_cache() -> None:
    global _CACHE, _CACHE_MTIME
    with _CACHE_LOCK:
        _CACHE = None
        _CACHE_MTIME = None


def load_app_prefs_cached() -> dict[str, Any]:
    global _CACHE, _CACHE_MTIME
    path = app_prefs_path()
    try:
        mtime = os.path.getmtime(path)
    except OSError:
        return {}
    with _CACHE_LOCK:
        if _CACHE is not None and _CACHE_MTIME == mtime:
            return copy.deepcopy(_CACHE)
    try:
        with open(path, "r", encoding="utf-8") as f:
            data = json.load(f)
    except Exception:
        _log.warning("Failed to load app_prefs.json, using empty defaults", exc_info=True)
        return {}
    result = data if isinstance(data, dict) else {}
    with _CACHE_LOCK:
        _CACHE = result
        _CACHE_MTIME = mtime
    return copy.deepcopy(result)


def save_app_prefs(prefs: dict[str, Any]) -> None:
    if not isinstance(prefs, dict):
        return
    path = app_prefs_path()
    with _WRITE_LOCK:
        os.makedirs(os.path.dirname(path), exist_ok=True)
        secure_atomic_write(path, json.dumps(prefs, indent=2))
        invalidate_app_prefs_cache()


def mutate_app_prefs(mutator: Callable[[dict[str, Any]], None]) -> dict[str, Any]:
    """Load app prefs, apply an in-memory mutation, then atomically persist.

    The full read-modify-write path is serialized so concurrent writers do not
    overwrite each other's changes.
    """
    if not callable(mutator):
        return load_app_prefs_cached()
    with _WRITE_LOCK:
        prefs = load_app_prefs_cached()
        if not isinstance(prefs, dict):
            prefs = {}
        updated = copy.deepcopy(prefs)
        mutator(updated)
        save_app_prefs(updated)
        return copy.deepcopy(updated)


def merge_app_prefs(updates: dict[str, Any]) -> dict[str, Any]:
    """Merge top-level keys into app prefs and persist atomically."""
    if not isinstance(updates, dict):
        return load_app_prefs_cached()

    def _merge(target: dict[str, Any]) -> None:
        target.update(updates)

    return mutate_app_prefs(_merge)

