"""Base class for JSON entity stores — eliminates boilerplate across 12+ stores."""

import asyncio
import json
import threading
from pathlib import Path
from typing import Callable, ClassVar, Dict, Generic, List, TypeVar

from wled_controller.utils import atomic_write_json, get_logger

T = TypeVar("T")
logger = get_logger(__name__)

# When True, all store saves are suppressed. Set by the restore flow
# to prevent the old server process from overwriting freshly-restored
# files with stale in-memory data before the restart completes.
_saves_frozen = False


def freeze_saves() -> None:
    """Block all store saves until the process exits (used after restore)."""
    global _saves_frozen
    _saves_frozen = True
    logger.info("Store saves frozen — awaiting server restart")


def unfreeze_saves() -> None:
    """Re-enable store saves (used in tests to reset after restore flow)."""
    global _saves_frozen
    _saves_frozen = False


class EntityNotFoundError(ValueError):
    """Raised when an entity is not found in the store."""
    pass


class BaseJsonStore(Generic[T]):
    """JSON-file-backed entity store with common CRUD helpers.

    Provides:
    - ``_load()`` / ``_save()``: atomic JSON file I/O
    - ``get_all()`` / ``get(id)`` / ``delete(id)`` / ``count()``: read/delete
    - ``_check_name_unique(name, exclude_id)``: duplicate-name guard

    Subclasses must set class attributes:
    - ``_json_key``:     root key in JSON file (e.g. ``"sync_clocks"``)
    - ``_entity_name``:  human label for errors (e.g. ``"Sync clock"``)
    - ``_version``:      schema version string (default ``"1.0.0"``)
    - ``_legacy_json_keys``: fallback root keys for migration (default ``[]``)
    """

    _json_key: str
    _entity_name: str
    _version: str = "1.0.0"
    _legacy_json_keys: ClassVar[List[str]] = []

    def __init__(self, file_path: str, deserializer: Callable[[dict], T]):
        self.file_path = Path(file_path)
        self._items: Dict[str, T] = {}
        self._deserializer = deserializer
        self._lock = threading.RLock()
        self._load()

    # ── I/O ────────────────────────────────────────────────────────

    def _load(self) -> None:
        if not self.file_path.exists():
            logger.info(f"{self._entity_name} store file not found — starting empty")
            return

        try:
            with open(self.file_path, "r", encoding="utf-8") as f:
                data = json.load(f)

            # Try primary key, then legacy keys for migration
            items_data = data.get(self._json_key, {})
            if not items_data:
                for legacy_key in self._legacy_json_keys:
                    items_data = data.get(legacy_key, {})
                    if items_data:
                        logger.info(f"Migrating {self._entity_name} from legacy key '{legacy_key}'")
                        break

            loaded = 0
            for item_id, item_dict in items_data.items():
                try:
                    self._items[item_id] = self._deserializer(item_dict)
                    loaded += 1
                except Exception as e:
                    logger.error(
                        f"Failed to load {self._entity_name} {item_id}: {e}",
                        exc_info=True,
                    )

            if loaded > 0:
                logger.info(f"Loaded {loaded} {self._json_key} from storage")

        except Exception as e:
            logger.error(f"Failed to load {self._json_key} from {self.file_path}: {e}")
            raise

        logger.info(
            f"{self._entity_name} store initialized with {len(self._items)} items"
        )

    def _save(self, *, force: bool = False) -> None:
        """Persist all items to disk atomically.

        Note: This is synchronous blocking I/O. When called from async route
        handlers, it briefly blocks the event loop (typically < 5ms for small
        stores). Acceptable for user-initiated CRUD; not suitable for hot loops.
        Callers must hold ``self._lock``.

        Args:
            force: bypass the ``_saves_frozen`` flag (used by shutdown save).
        """
        if _saves_frozen and not force:
            logger.warning(f"Save blocked (frozen after restore): {self._json_key}")
            return
        try:
            data = {
                "version": self._version,
                self._json_key: {
                    item_id: item.to_dict()
                    for item_id, item in self._items.items()
                },
            }
            atomic_write_json(self.file_path, data)
        except Exception as e:
            logger.error(f"Failed to save {self._json_key} to {self.file_path}: {e}")
            raise

    async def _save_async(self) -> None:
        """Async wrapper around ``_save()`` — runs file I/O in a thread.

        Use from ``async def`` route handlers to avoid blocking the event loop.
        Caller must hold ``self._lock`` (same contract as ``_save``).
        """
        await asyncio.to_thread(self._save)

    async def async_delete(self, item_id: str) -> None:
        """Async version of ``delete()`` — offloads file I/O to a thread."""
        with self._lock:
            if item_id not in self._items:
                raise EntityNotFoundError(f"{self._entity_name} not found: {item_id}")
            del self._items[item_id]
            await self._save_async()
        logger.info(f"Deleted {self._entity_name}: {item_id}")

    # ── Common CRUD ────────────────────────────────────────────────

    def get_all(self) -> List[T]:
        with self._lock:
            return list(self._items.values())

    def get(self, item_id: str) -> T:
        with self._lock:
            if item_id not in self._items:
                raise EntityNotFoundError(f"{self._entity_name} not found: {item_id}")
            return self._items[item_id]

    def delete(self, item_id: str) -> None:
        with self._lock:
            if item_id not in self._items:
                raise EntityNotFoundError(f"{self._entity_name} not found: {item_id}")
            del self._items[item_id]
            self._save()
        logger.info(f"Deleted {self._entity_name}: {item_id}")

    def count(self) -> int:
        with self._lock:
            return len(self._items)

    # ── Helpers ────────────────────────────────────────────────────

    def _check_name_unique(self, name: str, exclude_id: str = None) -> None:
        """Raise ValueError if *name* is empty or already taken.

        Must be called while holding ``self._lock``.
        """
        if not name or not name.strip():
            raise ValueError("Name is required")
        for item_id, item in self._items.items():
            if item_id != exclude_id and getattr(item, "name", None) == name:
                raise ValueError(
                    f"{self._entity_name} with name '{name}' already exists"
                )
