import asyncio
import time
from typing import Any, Optional, TYPE_CHECKING

from .base import CacheClient

if TYPE_CHECKING:
    from ..serializer import Serializer


class MemoryCacheClient(CacheClient):
    """In-process cache backed by Python :class:`dict`.

    Every data structure is supported.  Concurrent access is protected by an
    :class:`asyncio.Lock`.  TTL is enforced via lazy eviction — expired keys
    are removed on the next read or write attempt.

    Parameters
    ----------
    serializer:
        Optional serializer.  Defaults to
        :class:`~cache_core.serializer.SmartSerializer` when ``None``.
    """

    def __init__(self, serializer: "Serializer | None" = None) -> None:
        self._serializer = serializer
        self._lock = asyncio.Lock()
        self._sweep_interval = 60  # seconds between expiry sweeps
        self._sweep_task: asyncio.Task | None = None

        # Core storage
        self._kv: dict[str, Any] = {}               # key → value
        self._ttl: dict[str, float] = {}            # key → expiry timestamp (monotonic)

        # Data-structure storage
        self._hashes: dict[str, dict[str, Any]] = {}
        self._lists: dict[str, list[Any]] = {}
        self._sets: dict[str, set[Any]] = {}
        self._zsets: dict[str, dict[Any, float]] = {}

    # -- helpers --------------------------------------------------------------

    def _now(self) -> float:
        return time.monotonic()

    def _is_expired(self, key: str) -> bool:
        expiry = self._ttl.get(key)
        return expiry is not None and self._now() >= expiry

    def _evict_if_expired(self, key: str) -> None:
        if self._is_expired(key):
            self._kv.pop(key, None)
            self._ttl.pop(key, None)
            self._hashes.pop(key, None)
            self._lists.pop(key, None)
            self._sets.pop(key, None)
            self._zsets.pop(key, None)

    def _maybe_serialize(self, value: Any) -> Any:
        if self._serializer is None:
            return value
        return self._serializer.serialize(value)

    def _maybe_deserialize(self, value: Any) -> Any:
        if self._serializer is None:
            return value
        return self._serializer.deserialize(value)

    async def _sweep_expired(self) -> None:
        """Periodically scan all keys and evict any that are expired."""
        while True:
            await asyncio.sleep(self._sweep_interval)
            async with self._lock:
                all_keys: set[str] = set(self._ttl)
                for key in all_keys:
                    self._evict_if_expired(key)

    # -- Connection lifecycle ------------------------------------------------

    async def connect(self) -> None:
        if self._sweep_task is None:
            self._sweep_task = asyncio.create_task(self._sweep_expired())

    async def disconnect(self) -> None:
        if self._sweep_task is not None:
            self._sweep_task.cancel()
            try:
                await self._sweep_task
            except asyncio.CancelledError:
                pass
            self._sweep_task = None

    async def ping(self) -> bool:
        return True

    # -- Key-Value -----------------------------------------------------------

    async def get(self, key: str) -> Any:
        async with self._lock:
            self._evict_if_expired(key)
            value = self._kv.get(key)
            if value is None:
                return None
            return self._maybe_deserialize(value)

    async def set(self, key: str, value: Any, ttl: int | None = None) -> None:
        async with self._lock:
            self._evict_if_expired(key)
            self._kv[key] = self._maybe_serialize(value)
            if ttl is not None:
                self._ttl[key] = self._now() + ttl
            else:
                self._ttl.pop(key, None)

    async def setnx(self, key: str, value: Any, ttl: int | None = None) -> bool:
        """Set *key* to *value* only if it does **not** already exist.

        Returns ``True`` if the key was set, ``False`` if it already existed.
        """
        async with self._lock:
            self._evict_if_expired(key)
            if key in self._kv:
                return False
            self._kv[key] = self._maybe_serialize(value)
            if ttl is not None:
                self._ttl[key] = self._now() + ttl
            return True

    async def delete(self, *keys: str) -> int:
        count = 0
        async with self._lock:
            for key in keys:
                if key in self._kv:
                    del self._kv[key]
                    self._ttl.pop(key, None)
                    self._hashes.pop(key, None)
                    self._lists.pop(key, None)
                    self._sets.pop(key, None)
                    self._zsets.pop(key, None)
                    count += 1
        return count

    async def exists(self, *keys: str) -> int:
        count = 0
        async with self._lock:
            for key in keys:
                self._evict_if_expired(key)
                if key in self._kv or key in self._hashes or key in self._lists or key in self._sets or key in self._zsets:
                    count += 1
        return count

    async def expire(self, key: str, ttl: int) -> bool:
        async with self._lock:
            self._evict_if_expired(key)
            if key in self._kv or key in self._hashes or key in self._lists or key in self._sets or key in self._zsets:
                self._ttl[key] = self._now() + ttl
                return True
            return False

    async def ttl(self, key: str) -> int:
        async with self._lock:
            self._evict_if_expired(key)
            expiry = self._ttl.get(key)
            if expiry is None:
                return -1
            remaining = expiry - self._now()
            return max(0, int(remaining))

    async def incr(self, key: str, amount: int = 1) -> int:
        async with self._lock:
            self._evict_if_expired(key)
            current = self._kv.get(key, 0)
            if not isinstance(current, (int, float)):
                try:
                    current = int(current)
                except (ValueError, TypeError):
                    current = 0
            current += amount
            self._kv[key] = current
            return current

    async def decr(self, key: str, amount: int = 1) -> int:
        return await self.incr(key, -amount)

    # -- Hash ----------------------------------------------------------------

    async def hget(self, key: str, field: str) -> Any:
        async with self._lock:
            self._evict_if_expired(key)
            h = self._hashes.get(key, {})
            value = h.get(field)
            if value is None:
                return None
            return self._maybe_deserialize(value)

    async def hset(self, key: str, mapping: dict[str, Any]) -> int:
        async with self._lock:
            self._evict_if_expired(key)
            h = self._hashes.setdefault(key, {})
            added = 0
            for k, v in mapping.items():
                if k not in h:
                    added += 1
                h[k] = self._maybe_serialize(v)
            return added

    async def hgetall(self, key: str) -> dict[str, Any]:
        async with self._lock:
            self._evict_if_expired(key)
            h = self._hashes.get(key, {})
            return {k: self._maybe_deserialize(v) for k, v in h.items()}

    async def hdel(self, key: str, *fields: str) -> int:
        async with self._lock:
            h = self._hashes.get(key, {})
            count = 0
            for f in fields:
                if f in h:
                    del h[f]
                    count += 1
            if not h:
                self._hashes.pop(key, None)
            return count

    async def hexists(self, key: str, field: str) -> bool:
        async with self._lock:
            self._evict_if_expired(key)
            h = self._hashes.get(key, {})
            return field in h

    async def hkeys(self, key: str) -> list[str]:
        async with self._lock:
            self._evict_if_expired(key)
            return list(self._hashes.get(key, {}).keys())

    async def hlen(self, key: str) -> int:
        async with self._lock:
            self._evict_if_expired(key)
            return len(self._hashes.get(key, {}))

    # -- List ----------------------------------------------------------------

    async def lpush(self, key: str, *values: Any) -> int:
        async with self._lock:
            self._evict_if_expired(key)
            lst = self._lists.setdefault(key, [])
            serialized = [self._maybe_serialize(v) for v in values]
            for v in serialized:
                lst.insert(0, v)
            return len(lst)

    async def rpush(self, key: str, *values: Any) -> int:
        async with self._lock:
            self._evict_if_expired(key)
            lst = self._lists.setdefault(key, [])
            serialized = [self._maybe_serialize(v) for v in values]
            lst.extend(serialized)
            return len(lst)

    async def lpop(self, key: str) -> Any:
        async with self._lock:
            self._evict_if_expired(key)
            lst = self._lists.get(key)
            if not lst:
                return None
            value = lst.pop(0)
            if not lst:
                del self._lists[key]
            return self._maybe_deserialize(value)

    async def rpop(self, key: str) -> Any:
        async with self._lock:
            self._evict_if_expired(key)
            lst = self._lists.get(key)
            if not lst:
                return None
            value = lst.pop()
            if not lst:
                del self._lists[key]
            return self._maybe_deserialize(value)

    async def lrange(self, key: str, start: int, stop: int) -> list[Any]:
        async with self._lock:
            self._evict_if_expired(key)
            lst = self._lists.get(key, [])
            return [self._maybe_deserialize(v) for v in lst[start : (stop + 1 if stop >= 0 else None)]]

    async def llen(self, key: str) -> int:
        async with self._lock:
            self._evict_if_expired(key)
            return len(self._lists.get(key, []))

    # -- Set -----------------------------------------------------------------

    async def sadd(self, key: str, *members: Any) -> int:
        async with self._lock:
            self._evict_if_expired(key)
            s = self._sets.setdefault(key, set())
            before = len(s)
            for m in members:
                s.add(self._maybe_serialize(m))
            return len(s) - before

    async def srem(self, key: str, *members: Any) -> int:
        async with self._lock:
            s = self._sets.get(key)
            if s is None:
                return 0
            count = 0
            for m in members:
                serialized = self._maybe_serialize(m)
                if serialized in s:
                    s.remove(serialized)
                    count += 1
            if not s:
                del self._sets[key]
            return count

    async def smembers(self, key: str) -> list[Any]:
        async with self._lock:
            self._evict_if_expired(key)
            s = self._sets.get(key, set())
            return [self._maybe_deserialize(v) for v in s]

    async def sismember(self, key: str, member: Any) -> bool:
        async with self._lock:
            self._evict_if_expired(key)
            s = self._sets.get(key, set())
            return self._maybe_serialize(member) in s

    async def scard(self, key: str) -> int:
        async with self._lock:
            self._evict_if_expired(key)
            return len(self._sets.get(key, set()))

    # -- Sorted-set ----------------------------------------------------------

    async def zadd(self, key: str, mapping: dict[Any, float]) -> int:
        async with self._lock:
            self._evict_if_expired(key)
            zs = self._zsets.setdefault(key, {})
            added = 0
            for m, score in mapping.items():
                sm = self._maybe_serialize(m)
                if sm not in zs:
                    added += 1
                zs[sm] = score
            return added

    async def zrem(self, key: str, *members: Any) -> int:
        async with self._lock:
            zs = self._zsets.get(key)
            if zs is None:
                return 0
            count = 0
            for m in members:
                sm = self._maybe_serialize(m)
                if sm in zs:
                    del zs[sm]
                    count += 1
            if not zs:
                del self._zsets[key]
            return count

    async def zrange(
        self, key: str, start: int, stop: int, withscores: bool = False
    ) -> list[Any]:
        async with self._lock:
            self._evict_if_expired(key)
            zs = self._zsets.get(key, {})
            sorted_items = sorted(zs.items(), key=lambda x: x[1])
            # Handle negative indices
            if start < 0:
                start = max(0, len(sorted_items) + start)
            if stop < 0:
                stop = len(sorted_items) + stop
            window = sorted_items[start : stop + 1]
            if withscores:
                return [(self._maybe_deserialize(m), score) for m, score in window]
            return [self._maybe_deserialize(m) for m, _ in window]

    async def zcard(self, key: str) -> int:
        async with self._lock:
            self._evict_if_expired(key)
            return len(self._zsets.get(key, {}))

    # -- Utility -------------------------------------------------------------

    async def keys(self, pattern: str) -> list[str]:
        import fnmatch

        async with self._lock:
            all_keys = set(self._kv.keys())
            all_keys.update(self._hashes.keys())
            all_keys.update(self._lists.keys())
            all_keys.update(self._sets.keys())
            all_keys.update(self._zsets.keys())
            return [k for k in all_keys if fnmatch.fnmatch(k, pattern)]

    async def flush(self) -> None:
        async with self._lock:
            self._kv.clear()
            self._ttl.clear()
            self._hashes.clear()
            self._lists.clear()
            self._sets.clear()
            self._zsets.clear()
