import arrow

from typing import Optional, List, Dict, Union, Iterator

from Controllers.Redis.response import RedisResponse
from Controllers.Redis.connection import redis_cli
from Controllers.Redis.base import RedisRow


class MainConfig:
    DATETIME_FORMAT: str = "YYYY-MM-DD HH:mm:ss"


class RedisActions:
    """Class for handling Redis operations with JSON data."""

    @classmethod
    def get_expiry_time(cls, expiry_kwargs: Dict[str, int]) -> int:
        """
        Calculate expiry time in seconds from kwargs.

        Args:
            expiry_kwargs: Dictionary with time units as keys (days, hours, minutes, seconds)
                          and their respective values.

        Returns:
            Total expiry time in seconds.
        """
        time_multipliers = {"days": 86400, "hours": 3600, "minutes": 60, "seconds": 1}
        return sum(
            int(expiry_kwargs.get(unit, 0)) * multiplier
            for unit, multiplier in time_multipliers.items()
        )

    @classmethod
    def set_expiry_time(cls, expiry_seconds: int) -> Dict[str, int]:
        """
        Convert total seconds back into a dictionary of time units.

        Args:
            expiry_seconds: Total expiry time in seconds.

        Returns:
            Dictionary with time units and their values.
        """
        time_multipliers = {"days": 86400, "hours": 3600, "minutes": 60, "seconds": 1}
        result = {}
        remaining_seconds = expiry_seconds

        if expiry_seconds < 0:
            return {}

        for unit, multiplier in time_multipliers.items():
            if remaining_seconds >= multiplier:
                result[unit], remaining_seconds = divmod(remaining_seconds, multiplier)
        return result

    @classmethod
    def resolve_expires_at(cls, redis_row: RedisRow) -> str:
        """
        Resolve expiry time for Redis key.

        Args:
            redis_row: RedisRow object containing the redis_key.

        Returns:
            Formatted expiry time string or message indicating no expiry.
        """
        expiry_time = redis_cli.ttl(redis_row.redis_key)
        if expiry_time == -1:
            return "Key has no expiry time."
        if expiry_time == -2:
            return "Key does not exist."
        return arrow.now().shift(seconds=expiry_time).format(MainConfig.DATETIME_FORMAT)

    @classmethod
    def key_exists(cls, key: Union[str, bytes]) -> bool:
        """
        Check if a key exists in Redis without retrieving its value.

        Args:
            key: Redis key to check.

        Returns:
            Boolean indicating if key exists.
        """
        return bool(redis_cli.exists(key))

    @classmethod
    def refresh_ttl(
        cls, key: Union[str, bytes], expires: Dict[str, int]
    ) -> RedisResponse:
        """
        Refresh TTL for an existing key.

        Args:
            key: Redis key to refresh TTL.
            expires: Dictionary with time units to set new expiry.

        Returns:
            RedisResponse with operation result.
        """
        try:
            if not cls.key_exists(key):
                return RedisResponse(
                    status=False,
                    message="Cannot refresh TTL: Key does not exist.",
                )

            expiry_time = cls.get_expiry_time(expiry_kwargs=expires)
            redis_cli.expire(name=key, time=expiry_time)

            expires_at_string = (
                arrow.now()
                .shift(seconds=expiry_time)
                .format(MainConfig.DATETIME_FORMAT)
            )

            return RedisResponse(
                status=True,
                message="TTL refreshed successfully.",
                data={"key": key, "expires_at": expires_at_string},
            )
        except Exception as e:
            return RedisResponse(
                status=False,
                message="Failed to refresh TTL.",
                error=str(e),
            )

    @classmethod
    def delete_key(cls, key: Union[Optional[str], Optional[bytes]]) -> RedisResponse:
        """
        Delete a specific key from Redis.

        Args:
            key: Redis key to delete.

        Returns:
            RedisResponse with operation result.
        """
        try:
            deleted_count = redis_cli.delete(key)
            if deleted_count > 0:
                return RedisResponse(
                    status=True,
                    message="Key deleted successfully.",
                    data={"deleted_count": deleted_count},
                )
            return RedisResponse(
                status=False,
                message="Key not found or already deleted.",
                data={"deleted_count": 0},
            )
        except Exception as e:
            return RedisResponse(
                status=False,
                message="Failed to delete key.",
                error=str(e),
            )

    @classmethod
    def delete(
        cls, list_keys: List[Union[Optional[str], Optional[bytes]]]
    ) -> RedisResponse:
        """
        Delete multiple keys matching a pattern.

        Args:
            list_keys: List of key components to form pattern for deletion.

        Returns:
            RedisResponse with operation result.
        """
        try:
            regex = RedisRow().regex(list_keys=list_keys)
            json_get = redis_cli.scan_iter(match=regex)

            deleted_keys, deleted_count = [], 0

            # Use pipeline for batch deletion
            with redis_cli.pipeline() as pipe:
                for row in json_get:
                    pipe.delete(row)
                    deleted_keys.append(row)
                results = pipe.execute()
                deleted_count = sum(results)

            return RedisResponse(
                status=True,
                message="Keys deleted successfully.",
                data={"deleted_count": deleted_count, "deleted_keys": deleted_keys},
            )
        except Exception as e:
            return RedisResponse(
                status=False,
                message="Failed to delete keys.",
                error=str(e),
            )

    @classmethod
    def set_json(
        cls,
        list_keys: List[Union[str, bytes]],
        value: Optional[Union[Dict, List]],
        expires: Optional[Dict[str, int]] = None,
    ) -> RedisResponse:
        """
        Set JSON value in Redis with optional expiry.

        Args:
            list_keys: List of key components to form Redis key.
            value: JSON-serializable data to store.
            expires: Optional dictionary with time units for expiry.

        Returns:
            RedisResponse with operation result.
        """
        redis_row = RedisRow()
        redis_row.merge(set_values=list_keys)
        redis_row.feed(value)
        redis_row.expires_at_string = None
        redis_row.expires_at = None

        try:
            if expires:
                redis_row.expires_at = expires
                expiry_time = cls.get_expiry_time(expiry_kwargs=expires)
                redis_cli.setex(
                    name=redis_row.redis_key,
                    time=expiry_time,
                    value=redis_row.value,
                )
                redis_row.expires_at_string = str(
                    arrow.now()
                    .shift(seconds=expiry_time)
                    .format(MainConfig.DATETIME_FORMAT)
                )
            else:
                redis_cli.set(name=redis_row.redis_key, value=redis_row.value)

            return RedisResponse(
                status=True,
                message="Value set successfully.",
                data=redis_row,
            )
        except Exception as e:
            return RedisResponse(
                status=False,
                message="Failed to set value.",
                error=str(e),
            )

    @classmethod
    def get_json(
        cls,
        list_keys: List[Union[Optional[str], Optional[bytes]]],
        limit: Optional[int] = None,
    ) -> RedisResponse:
        """
        Get JSON values from Redis using pattern matching.

        Args:
            list_keys: List of key components to form pattern for retrieval.
            limit: Optional limit on number of results to return.

        Returns:
            RedisResponse with operation result.
        """
        try:
            list_of_rows, count = [], 0
            regex = RedisRow.regex(list_keys=list_keys)
            json_get = redis_cli.scan_iter(match=regex)

            for row in json_get:
                if limit is not None and count >= limit:
                    break

                redis_row = RedisRow()
                redis_row.set_key(key=row)

                # Use pipeline for batch retrieval
                with redis_cli.pipeline() as pipe:
                    pipe.get(row)
                    pipe.ttl(row)
                    redis_value, redis_value_expire = pipe.execute()
                    redis_row.expires_at = cls.set_expiry_time(
                        expiry_seconds=int(redis_value_expire)
                    )
                    redis_row.expires_at_string = cls.resolve_expires_at(
                        redis_row=redis_row
                    )
                    redis_row.feed(redis_value)
                    list_of_rows.append(redis_row)
                    count += 1

            if list_of_rows:
                return RedisResponse(
                    status=True,
                    message="Values retrieved successfully.",
                    data=list_of_rows,
                )
            return RedisResponse(
                status=False,
                message="No matching keys found.",
                data=list_of_rows,
            )
        except Exception as e:
            return RedisResponse(
                status=False,
                message="Failed to retrieve values.",
                error=str(e),
            )

    @classmethod
    def get_json_iterator(
        cls, list_keys: List[Union[Optional[str], Optional[bytes]]]
    ) -> Iterator[RedisRow]:
        """
        Get JSON values from Redis as an iterator for memory-efficient processing of large datasets.

        Args:
            list_keys: List of key components to form pattern for retrieval.

        Returns:
            Iterator yielding RedisRow objects.

        Raises:
            RedisValueError: If there's an error processing a row
        """
        regex = RedisRow.regex(list_keys=list_keys)
        json_get = redis_cli.scan_iter(match=regex)

        for row in json_get:
            try:
                redis_row = RedisRow()
                redis_row.set_key(key=row)

                # Use pipeline for batch retrieval
                with redis_cli.pipeline() as pipe:
                    pipe.get(row)
                    pipe.ttl(row)
                    redis_value, redis_value_expire = pipe.execute()
                    redis_row.expires_at = cls.set_expiry_time(
                        expiry_seconds=int(redis_value_expire)
                    )
                    redis_row.expires_at_string = cls.resolve_expires_at(
                        redis_row=redis_row
                    )
                    redis_row.feed(redis_value)
                    yield redis_row
            except Exception as e:
                # Log the error and continue with next row
                print(f"Error processing row {row}: {str(e)}")
                continue
