from __future__ import annotations

import fnmatch
import importlib
from typing import TYPE_CHECKING, Any, Optional

import redis

from mockmesh.core.fallback import FallbackConfig
from mockmesh.core.logger import get_logger
from mockmesh.interceptors.common import encode_value_bytes as _to_bytes
from mockmesh.plugins.base import BasePlugin

if TYPE_CHECKING:
    from mockmesh.response.engine import ResponseEngine
    from mockmesh.storage.manager import StorageManager

_log = get_logger("interceptors.redis")


class _MockPipeline:

    def __init__(self, mock_redis: "_MockRedis") -> None:
        self._redis = mock_redis
        self._commands: list[tuple[str, tuple, dict]] = []

    def __getattr__(self, name: str):
        if hasattr(self._redis, name) and callable(getattr(self._redis, name)):
            def _queue(*args: Any, **kwargs: Any) -> "_MockPipeline":
                self._commands.append((name, args, kwargs))
                return self
            return _queue
        raise AttributeError(name)

    def execute(self) -> list:
        results = []
        for name, args, kwargs in self._commands:
            fn = getattr(self._redis, name)
            results.append(fn(*args, **kwargs))
        self._commands.clear()
        return results

    def __enter__(self) -> "_MockPipeline":
        return self

    def __exit__(self, *args: Any) -> None:
        pass


class _MockRedis:

    def __init__(self, host: str = "localhost", port: int = 6379, db: int = 0,
                 _storage: "StorageManager | None" = None, **kwargs: Any) -> None:
        self._db = db
        self._sm = _storage
        self._collection = f"redis_{db}"

    def _cache(self):
        return self._sm.cache

    def _get_entry(self, name: str) -> Optional[dict]:
        return self._cache().get(self._collection, str(name))

    def _put_entry(self, name: str, entry: dict) -> None:
        self._cache().put(self._collection, str(name), entry)

    def _del_entry(self, name: str) -> bool:
        return self._cache().delete(self._collection, str(name))

    def _all_keys(self) -> list[str]:
        return [r["key"] for r in self._cache().list(self._collection)]

    # ------------------------------------------------------------------
    # String operations
    # ------------------------------------------------------------------

    def set(self, name: str, value: Any, ex: Any = None, px: Any = None,
            nx: bool = False, xx: bool = False) -> bool:
        name = str(name)
        existing = self._get_entry(name)
        if nx and existing is not None:
            return False
        if xx and existing is None:
            return False
        ttl = -1
        if ex is not None:
            ttl = int(ex)
        elif px is not None:
            ttl = int(px) // 1000
        self._put_entry(name, {"type": "string", "value": str(value), "ttl": ttl})
        return True

    def get(self, name: str) -> Optional[bytes]:
        entry = self._get_entry(str(name))
        if entry is None or entry.get("type") != "string":
            return None
        return _to_bytes(entry["value"])

    def mset(self, mapping: dict) -> bool:
        for k, v in mapping.items():
            self.set(k, v)
        return True

    def mget(self, keys: list) -> list:
        return [self.get(k) for k in keys]

    def setnx(self, name: str, value: Any) -> bool:
        return self.set(name, value, nx=True)

    def setex(self, name: str, time: int, value: Any) -> bool:
        return self.set(name, value, ex=time)

    def incr(self, name: str, amount: int = 1) -> int:
        name = str(name)
        entry = self._get_entry(name)
        if entry is None:
            val = amount
        else:
            val = int(entry["value"]) + amount
        self._put_entry(name, {"type": "string", "value": str(val), "ttl": entry.get("ttl", -1) if entry else -1})
        return val

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

    def append(self, name: str, value: Any) -> int:
        name = str(name)
        entry = self._get_entry(name)
        if entry is None:
            new_val = str(value)
        else:
            new_val = str(entry["value"]) + str(value)
        self._put_entry(name, {"type": "string", "value": new_val, "ttl": entry.get("ttl", -1) if entry else -1})
        return len(new_val)

    # ------------------------------------------------------------------
    # Key operations
    # ------------------------------------------------------------------

    def delete(self, *names: str) -> int:
        count = 0
        for n in names:
            if self._del_entry(str(n)):
                count += 1
        return count

    def exists(self, *names: str) -> int:
        count = 0
        for n in names:
            if self._get_entry(str(n)) is not None:
                count += 1
        return count

    def keys(self, pattern: str = "*") -> list[bytes]:
        all_keys = self._all_keys()
        return [_to_bytes(k) for k in all_keys if fnmatch.fnmatch(k, pattern)]

    def type(self, name: str) -> str:
        entry = self._get_entry(str(name))
        if entry is None:
            return "none"
        return entry.get("type", "none")

    def expire(self, name: str, time: int) -> bool:
        entry = self._get_entry(str(name))
        if entry is None:
            return False
        entry["ttl"] = int(time)
        self._put_entry(str(name), entry)
        return True

    def ttl(self, name: str) -> int:
        entry = self._get_entry(str(name))
        if entry is None:
            return -2
        return entry.get("ttl", -1)

    def persist(self, name: str) -> bool:
        entry = self._get_entry(str(name))
        if entry is None:
            return False
        entry["ttl"] = -1
        self._put_entry(str(name), entry)
        return True

    def rename(self, src: str, dst: str) -> bool:
        entry = self._get_entry(str(src))
        if entry is None:
            raise redis.exceptions.ResponseError("no such key")
        self._del_entry(str(src))
        self._put_entry(str(dst), entry)
        return True

    def dbsize(self) -> int:
        return len(self._all_keys())

    def flushdb(self) -> bool:
        for k in self._all_keys():
            self._del_entry(k)
        return True

    def flushall(self) -> bool:
        collections = self._cache().list_collections()
        for col in collections:
            if col.startswith("redis_"):
                records = self._cache().list(col)
                for r in records:
                    self._cache().delete(col, r["key"])
        return True

    # ------------------------------------------------------------------
    # Hash operations
    # ------------------------------------------------------------------

    def hset(self, name: str, key: str = None, value: Any = None,
             mapping: dict = None) -> int:
        name = str(name)
        entry = self._get_entry(name)
        if entry is None or entry.get("type") != "hash":
            entry = {"type": "hash", "value": {}, "ttl": -1}
        count = 0
        if key is not None and value is not None:
            if key not in entry["value"]:
                count += 1
            entry["value"][str(key)] = str(value)
        if mapping:
            for k, v in mapping.items():
                if str(k) not in entry["value"]:
                    count += 1
                entry["value"][str(k)] = str(v)
        self._put_entry(name, entry)
        return count

    def hget(self, name: str, key: str) -> Optional[bytes]:
        entry = self._get_entry(str(name))
        if entry is None or entry.get("type") != "hash":
            return None
        val = entry["value"].get(str(key))
        if val is None:
            return None
        return _to_bytes(val)

    def hgetall(self, name: str) -> dict:
        entry = self._get_entry(str(name))
        if entry is None or entry.get("type") != "hash":
            return {}
        return {_to_bytes(k): _to_bytes(v) for k, v in entry["value"].items()}

    def hdel(self, name: str, *keys: str) -> int:
        entry = self._get_entry(str(name))
        if entry is None or entry.get("type") != "hash":
            return 0
        count = 0
        for k in keys:
            if str(k) in entry["value"]:
                del entry["value"][str(k)]
                count += 1
        self._put_entry(str(name), entry)
        return count

    def hexists(self, name: str, key: str) -> bool:
        entry = self._get_entry(str(name))
        if entry is None or entry.get("type") != "hash":
            return False
        return str(key) in entry["value"]

    def hkeys(self, name: str) -> list[bytes]:
        entry = self._get_entry(str(name))
        if entry is None or entry.get("type") != "hash":
            return []
        return [_to_bytes(k) for k in entry["value"].keys()]

    def hvals(self, name: str) -> list[bytes]:
        entry = self._get_entry(str(name))
        if entry is None or entry.get("type") != "hash":
            return []
        return [_to_bytes(v) for v in entry["value"].values()]

    def hlen(self, name: str) -> int:
        entry = self._get_entry(str(name))
        if entry is None or entry.get("type") != "hash":
            return 0
        return len(entry["value"])

    def hincrby(self, name: str, key: str, amount: int = 1) -> int:
        name = str(name)
        entry = self._get_entry(name)
        if entry is None or entry.get("type") != "hash":
            entry = {"type": "hash", "value": {}, "ttl": -1}
        cur = int(entry["value"].get(str(key), 0))
        new_val = cur + amount
        entry["value"][str(key)] = str(new_val)
        self._put_entry(name, entry)
        return new_val

    # ------------------------------------------------------------------
    # List operations
    # ------------------------------------------------------------------

    def lpush(self, name: str, *values: Any) -> int:
        name = str(name)
        entry = self._get_entry(name)
        if entry is None or entry.get("type") != "list":
            entry = {"type": "list", "value": [], "ttl": -1}
        for v in values:
            entry["value"].insert(0, str(v))
        self._put_entry(name, entry)
        return len(entry["value"])

    def rpush(self, name: str, *values: Any) -> int:
        name = str(name)
        entry = self._get_entry(name)
        if entry is None or entry.get("type") != "list":
            entry = {"type": "list", "value": [], "ttl": -1}
        for v in values:
            entry["value"].append(str(v))
        self._put_entry(name, entry)
        return len(entry["value"])

    def lpop(self, name: str) -> Optional[bytes]:
        name = str(name)
        entry = self._get_entry(name)
        if entry is None or entry.get("type") != "list" or not entry["value"]:
            return None
        val = entry["value"].pop(0)
        self._put_entry(name, entry)
        return _to_bytes(val)

    def rpop(self, name: str) -> Optional[bytes]:
        name = str(name)
        entry = self._get_entry(name)
        if entry is None or entry.get("type") != "list" or not entry["value"]:
            return None
        val = entry["value"].pop()
        self._put_entry(name, entry)
        return _to_bytes(val)

    def lrange(self, name: str, start: int, end: int) -> list[bytes]:
        entry = self._get_entry(str(name))
        if entry is None or entry.get("type") != "list":
            return []
        lst = entry["value"]
        if end == -1:
            end = len(lst) - 1
        return [_to_bytes(v) for v in lst[start:end + 1]]

    def llen(self, name: str) -> int:
        entry = self._get_entry(str(name))
        if entry is None or entry.get("type") != "list":
            return 0
        return len(entry["value"])

    def lindex(self, name: str, index: int) -> Optional[bytes]:
        entry = self._get_entry(str(name))
        if entry is None or entry.get("type") != "list":
            return None
        lst = entry["value"]
        try:
            return _to_bytes(lst[index])
        except IndexError:
            return None

    # ------------------------------------------------------------------
    # Set operations
    # ------------------------------------------------------------------

    def sadd(self, name: str, *values: Any) -> int:
        name = str(name)
        entry = self._get_entry(name)
        if entry is None or entry.get("type") != "set":
            entry = {"type": "set", "value": [], "ttl": -1}
        members = set(entry["value"])
        count = 0
        for v in values:
            s = str(v)
            if s not in members:
                count += 1
                members.add(s)
        entry["value"] = list(members)
        self._put_entry(name, entry)
        return count

    def srem(self, name: str, *values: Any) -> int:
        name = str(name)
        entry = self._get_entry(name)
        if entry is None or entry.get("type") != "set":
            return 0
        members = set(entry["value"])
        count = 0
        for v in values:
            s = str(v)
            if s in members:
                members.discard(s)
                count += 1
        entry["value"] = list(members)
        self._put_entry(name, entry)
        return count

    def smembers(self, name: str) -> set[bytes]:
        entry = self._get_entry(str(name))
        if entry is None or entry.get("type") != "set":
            return set()
        return {_to_bytes(v) for v in entry["value"]}

    def sismember(self, name: str, value: Any) -> bool:
        entry = self._get_entry(str(name))
        if entry is None or entry.get("type") != "set":
            return False
        return str(value) in entry["value"]

    def scard(self, name: str) -> int:
        entry = self._get_entry(str(name))
        if entry is None or entry.get("type") != "set":
            return 0
        return len(entry["value"])

    # ------------------------------------------------------------------
    # Pipeline
    # ------------------------------------------------------------------

    def pipeline(self, transaction: bool = True) -> _MockPipeline:
        return _MockPipeline(self)

    # ------------------------------------------------------------------
    # Connection
    # ------------------------------------------------------------------

    def ping(self) -> bool:
        return True

    def close(self) -> None:
        pass

    def __enter__(self) -> "_MockRedis":
        return self

    def __exit__(self, *args: Any) -> None:
        self.close()


class RedisInterceptor(BasePlugin):

    name = "redis"

    def __init__(self, response_engine: "ResponseEngine") -> None:
        super().__init__(response_engine)
        self._originals: dict[str, Any] = {}

    def activate(
        self,
        config: dict,
        *,
        storage: "Any | None" = None,
        provider_defaults: Optional[dict] = None,
        fallback_config: Optional[FallbackConfig] = None,
        custom_handlers: Optional[dict] = None,
        **_: Any,
    ) -> None:
        self._fallback_config = fallback_config
        self._custom_handlers = custom_handlers if custom_handlers is not None else {}
        self.storage = storage
        self._defaults: dict = {}
        if provider_defaults is not None:
            self._defaults = provider_defaults.get("redis", {})
        self._patch_redis()
        self._active = True
        _log.info("Redis interceptor activated")

    def deactivate(self) -> None:
        if not self._active:
            return
        for key, original in self._originals.items():
            module_path, attr = key.rsplit(".", 1)
            try:
                mod = importlib.import_module(module_path)
                setattr(mod, attr, original)
            except Exception:
                pass
        self._originals.clear()
        self._active = False
        _log.info("Redis interceptor deactivated")

    def _patch_redis(self) -> None:
        sm = self.storage

        class _Redis(_MockRedis):
            def __init__(self, host: str = "localhost", port: int = 6379,
                         db: int = 0, **kwargs: Any):
                super().__init__(host=host, port=port, db=db, _storage=sm, **kwargs)

        self._originals["redis.Redis"] = redis.Redis
        self._originals["redis.StrictRedis"] = redis.StrictRedis
        redis.Redis = _Redis  # type: ignore
        redis.StrictRedis = _Redis  # type: ignore
        _log.debug("Patched redis.Redis / StrictRedis")
