"""Base class for SQLite-backed entity stores.

Drop-in replacement for BaseJsonStore with the same public API.
Each store keeps an in-memory cache (``_items``) for fast reads;
writes go through to SQLite immediately (write-through cache).
"""

import asyncio
import threading
from typing import Callable, Dict, Generic, List, TypeVar

from wled_controller.storage.database import Database
from wled_controller.utils import get_logger

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


class BaseSqliteStore(Generic[T]):
    """SQLite-backed entity store with the same API as BaseJsonStore.

    Subclasses must set class attributes:
    - ``_table_name``:    SQL table name (e.g. ``"sync_clocks"``)
    - ``_entity_name``:   human label for errors (e.g. ``"Sync clock"``)
    """

    _table_name: str
    _entity_name: str

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

    # -- I/O -----------------------------------------------------------------

    def _load(self) -> None:
        """Load all rows from SQLite into the in-memory cache."""
        rows = self._db.load_all(self._table_name)
        loaded = 0
        for item_dict in rows:
            item_id = item_dict.get("id")
            if not item_id:
                logger.error(f"Skipping {self._entity_name} row with no id")
                continue
            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._table_name} from database")
        logger.info(
            f"{self._entity_name} store initialized with {len(self._items)} items"
        )

    def _save_item(self, item_id: str, item: T) -> None:
        """Persist a single item to SQLite (write-through)."""
        data = item.to_dict()
        name = data.get("name", "")
        self._db.upsert(self._table_name, item_id, name, data)

    def _delete_item(self, item_id: str) -> None:
        """Delete a single item from SQLite."""
        self._db.delete_row(self._table_name, item_id)

    def _save_all(self, *, force: bool = False) -> None:
        """No-op — SQLite stores use write-through caching.

        Every create/update calls ``_save_item()`` and every delete calls
        ``_delete_item()``, so the database is always up to date.
        A bulk DELETE + re-INSERT here would be destructive if in-memory
        state diverged from the DB (e.g., partial load on startup).
        """

    # -- Backward compat: _save() used by subclass create/update methods -----

    def _save(self, *, force: bool = False) -> None:
        """Compatibility shim: save all items.

        Subclasses that call ``self._save()`` after mutating ``self._items``
        will trigger a full flush.  For better performance, prefer calling
        ``self._save_item(id, item)`` for single-entity mutations.
        """
        self._save_all(force=force)

    async def _save_async(self) -> None:
        """Async wrapper — runs ``_save()`` in a thread."""
        await asyncio.to_thread(self._save)

    # -- Common CRUD (identical API to BaseJsonStore) ------------------------

    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:
                from wled_controller.storage.base_store import EntityNotFoundError
                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:
                from wled_controller.storage.base_store import EntityNotFoundError
                raise EntityNotFoundError(f"{self._entity_name} not found: {item_id}")
            del self._items[item_id]
            self._delete_item(item_id)
        logger.info(f"Deleted {self._entity_name}: {item_id}")

    async def async_delete(self, item_id: str) -> None:
        """Async version of ``delete()``."""
        with self._lock:
            if item_id not in self._items:
                from wled_controller.storage.base_store import EntityNotFoundError
                raise EntityNotFoundError(f"{self._entity_name} not found: {item_id}")
            del self._items[item_id]
            await asyncio.to_thread(self._delete_item, item_id)
        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"
                )
