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

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

from wled_controller.utils import atomic_write_json, get_logger

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


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"``)
    """

    _json_key: str
    _entity_name: str
    _version: str = "1.0.0"

    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.Lock()
        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)

            items_data = data.get(self._json_key, {})
            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) -> 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.
        """
        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

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

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

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

    def delete(self, item_id: str) -> None:
        if item_id not in self._items:
            raise ValueError(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:
        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.

        Callers should hold ``self._lock`` when calling this + mutating
        ``_items`` to prevent race conditions between concurrent requests.
        """
        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"
                )
