# -*- coding: utf-8 -*-

"""Redis client wrapper."""

import threading
from typing import Any, Optional

import redis


class RedisClient:
    """
    Thin wrapper around a Redis connection that decouples the rest of the
    ecosystem from the underlying ``redis`` library.

    The connection is created lazily on the first access to
    :attr:`client` and is shared across all method calls.  A
    :class:`threading.Lock` ensures the initialisation is thread-safe.

    :param host: Redis server hostname. Default: ``"localhost"``.
    :param port: Redis server port. Default: ``6379``.
    :param db: Redis logical database index. Default: ``0``.
    :param password: Password for Redis AUTH, or ``None`` for no auth.
    :param kwargs:
        Any additional keyword arguments are forwarded verbatim to the
        underlying ``redis.Redis`` constructor (e.g. ``ssl``,
        ``socket_timeout``, ``decode_responses``).

    Example:
        .. code-block:: python

            from core_redis.client import RedisClient

            redis = RedisClient(host="localhost", port=6379)
            redis.set("key", b"value", ex=60)
            data = redis.get("key")
    """

    def __init__(
        self,
        host: str = "localhost",
        port: int = 6379,
        db: int = 0,
        password: Optional[str] = None,
        **kwargs: Any,
    ) -> None:
        self._host = host
        self._port = port
        self._db = db
        self._password = password
        self._kwargs = kwargs
        self._client: Any = None
        self._lock = threading.Lock()

    @property
    def client(self) -> Any:
        """The underlying ``redis.Redis`` instance (lazy, thread-safe)."""

        if self._client is None:
            with self._lock:
                if self._client is None:
                    self._client = redis.Redis(
                        host=self._host,
                        port=self._port,
                        db=self._db,
                        password=self._password,
                        **self._kwargs,
                    )

        return self._client

    def get(self, key: str) -> Optional[bytes]:
        """Return the value stored at *key*, or ``None`` if absent / expired."""
        return self.client.get(key)

    def set(
        self,
        key: str,
        value: bytes,
        ex: Optional[int] = None,
    ) -> None:
        """
        Store *value* under *key*.

        :param key: Redis key.
        :param value: Raw bytes to store.
        :param ex: Optional TTL in seconds. ``None`` means no expiry.
        """

        if ex is not None:
            self.client.set(key, value, ex=ex)
        else:
            self.client.set(key, value)

    def delete(self, *keys: str) -> int:
        """
        Delete one or more *keys*.
        :returns: Number of keys that were removed.
        """

        return self.client.delete(*keys)

    def exists(self, *keys: str) -> int:
        """
        Return the number of *keys* that exist.
        :returns: Count of existing keys (0 if none exist).
        """

        return self.client.exists(*keys)

    def ping(self) -> bool:
        """
        Send a ``PING`` to the server.
        :returns: ``True`` if the server responds with ``PONG``.
        """

        return self.client.ping()
