"""Redis node for data operations.

This module provides a Redis node for performing various Redis operations
including get, set, hget, hset, hgetall, and more.
"""

from __future__ import annotations

import json
import logging
from typing import Any, Dict, List, Optional, Union

try:
    import redis as _redis_mod
except ImportError:
    _redis_mod = None  # type: ignore[assignment]  # Optional dependency

from kailash.nodes.base import Node, NodeParameter, register_node
from kailash.sdk_exceptions import NodeExecutionError

logger = logging.getLogger(__name__)


@register_node()
class RedisNode(Node):
    """Node for Redis operations.

    Supports common Redis operations including:
    - String operations: get, set, delete
    - Hash operations: hget, hset, hgetall
    - List operations: lpush, rpush, lrange
    - Set operations: sadd, smembers
    - Sorted set operations: zadd, zrange

    Example:
        >>> # String operations
        >>> redis_node = RedisNode(
        ...     host="localhost",
        ...     port=6379,
        ...     operation="set",
        ...     key="user:123",
        ...     value="John Doe"
        ... )
        >>> result = redis_node.execute()
        >>> # result = {"success": True, "result": "OK"}

        >>> # Hash operations
        >>> redis_node = RedisNode(
        ...     host="localhost",
        ...     port=6379,
        ...     operation="hgetall",
        ...     key="user:123:profile"
        ... )
        >>> result = redis_node.execute()
        >>> # result = {"name": "John", "age": "30", "city": "NYC"}
    """

    def __init__(
        self,
        host: str = "localhost",
        port: int = 6379,
        db: int = 0,
        password: Optional[str] = None,
        operation: str = "get",
        key: Optional[str] = None,
        value: Optional[Any] = None,
        field: Optional[str] = None,
        fields: Optional[List[str]] = None,
        ttl: Optional[int] = None,
        decode_responses: bool = True,
        **kwargs,
    ):
        """Initialize Redis node.

        Args:
            host: Redis host
            port: Redis port
            db: Redis database number
            password: Redis password (if required)
            operation: Operation to perform (get, set, hget, hset, etc.)
            key: Redis key
            value: Value for set operations
            field: Field name for hash operations
            fields: Multiple fields for hash operations
            ttl: Time to live in seconds
            decode_responses: Whether to decode byte responses to strings
            **kwargs: Additional parameters
        """
        super().__init__(**kwargs)

        # Store all parameters in config for validation
        self.config.update(
            {
                "host": host,
                "port": port,
                "db": db,
                "password": password,
                "operation": operation,
                "key": key,
                "value": value,
                "field": field,
                "fields": fields,
                "ttl": ttl,
                "decode_responses": decode_responses,
            }
        )

        # Also store as instance attributes for backward compatibility
        self.host = host
        self.port = port
        self.db = db
        self.password = password
        self.operation = operation
        self._client = None  # Initialize to avoid __del__ error
        self.key = key
        self.value = value
        self.field = field
        self.fields = fields
        self.ttl = ttl
        self.decode_responses = decode_responses

    def get_parameters(self) -> Dict[str, NodeParameter]:
        """Define node parameters."""
        return {
            "host": NodeParameter(
                name="host",
                type=str,
                required=False,
                default="localhost",
                description="Redis host",
            ),
            "port": NodeParameter(
                name="port",
                type=int,
                required=False,
                default=6379,
                description="Redis port",
            ),
            "db": NodeParameter(
                name="db",
                type=int,
                required=False,
                default=0,
                description="Redis database number",
            ),
            "password": NodeParameter(
                name="password", type=str, required=False, description="Redis password"
            ),
            "operation": NodeParameter(
                name="operation",
                type=str,
                required=True,
                description="Redis operation (get, set, hget, hset, hgetall, etc.)",
            ),
            "key": NodeParameter(
                name="key",
                type=str,
                required=False,
                description="Redis key",
                input=True,
            ),
            "value": NodeParameter(
                name="value",
                type=Any,
                required=False,
                description="Value for set operations",
                input=True,
            ),
            "field": NodeParameter(
                name="field", type=str, required=False, description="Hash field name"
            ),
            "fields": NodeParameter(
                name="fields",
                type=list,
                required=False,
                description="Multiple hash fields",
            ),
            "ttl": NodeParameter(
                name="ttl",
                type=int,
                required=False,
                description="Time to live in seconds",
            ),
            "decode_responses": NodeParameter(
                name="decode_responses",
                type=bool,
                required=False,
                default=True,
                description="Decode byte responses to strings",
            ),
            "result": NodeParameter(
                name="result",
                type=Any,
                required=False,
                description="Operation result",
                output=True,
            ),
            "success": NodeParameter(
                name="success",
                type=bool,
                required=False,
                description="Operation success status",
                output=True,
            ),
        }

    def _get_client(self) -> _redis_mod.Redis:  # type: ignore[reportInvalidTypeForm]
        """Get or create Redis client."""
        if not self._client:
            self._client = _redis_mod.Redis(  # type: ignore[reportOptionalMemberAccess]
                host=self.host,
                port=self.port,
                db=self.db,
                password=self.password,
                decode_responses=self.decode_responses,
            )
        return self._client

    def execute(
        self, key: Optional[str] = None, value: Optional[Any] = None, **kwargs
    ) -> Dict[str, Any]:
        """Execute Redis operation.

        Args:
            key: Override key from config
            value: Override value from config
            **kwargs: Additional runtime parameters

        Returns:
            Dict with result and optional metadata
        """
        # Use runtime parameters if provided
        key = key or self.key or kwargs.get("key")
        value = (
            value
            if value is not None
            else (self.value if self.value is not None else kwargs.get("value"))
        )

        if not key and self.operation not in ["ping", "info", "flushdb"]:
            raise NodeExecutionError("Key is required for this operation")

        # key is guaranteed non-None for all operations except ping/info/flushdb
        key = str(key) if key is not None else ""
        try:
            client = self._get_client()
            result = None

            # String operations
            if self.operation == "get":
                result = client.get(key)

            elif self.operation == "set":
                if value is None:
                    raise NodeExecutionError("Value is required for set operation")

                # Serialize non-string values
                if not isinstance(value, (str, bytes)):
                    value = json.dumps(value)

                if self.ttl:
                    result = client.setex(key, self.ttl, value)
                else:
                    result = client.set(key, value)

            elif self.operation == "delete":
                result = client.delete(key)

            # Hash operations
            elif self.operation == "hget":
                if not self.field:
                    raise NodeExecutionError("Field is required for hget operation")
                result = client.hget(key, self.field)

            elif self.operation == "hset":
                if not self.field:
                    raise NodeExecutionError("Field is required for hset operation")
                if value is None:
                    raise NodeExecutionError("Value is required for hset operation")

                # Serialize non-string values
                if not isinstance(value, (str, bytes)):
                    value = json.dumps(value)

                result = client.hset(key, self.field, value)

            elif self.operation == "hgetall":
                result = client.hgetall(key)
                # Convert to regular dict for JSON serialization
                if result:
                    result = dict(result)

            elif self.operation == "hmget":
                if not self.fields:
                    raise NodeExecutionError("Fields are required for hmget operation")
                values = client.hmget(key, self.fields)
                result = dict(zip(self.fields, values))

            # List operations
            elif self.operation == "lpush":
                if value is None:
                    raise NodeExecutionError("Value is required for lpush operation")
                result = client.lpush(key, value)

            elif self.operation == "rpush":
                if value is None:
                    raise NodeExecutionError("Value is required for rpush operation")
                result = client.rpush(key, value)

            elif self.operation == "lrange":
                start = kwargs.get("start", 0)
                end = kwargs.get("end", -1)
                result = client.lrange(key, start, end)

            # Set operations
            elif self.operation == "sadd":
                if value is None:
                    raise NodeExecutionError("Value is required for sadd operation")
                result = client.sadd(key, value)

            elif self.operation == "smembers":
                result = list(client.smembers(key))

            # Sorted set operations
            elif self.operation == "zadd":
                if value is None or "score" not in kwargs:
                    raise NodeExecutionError(
                        "Value and score are required for zadd operation"
                    )
                result = client.zadd(key, {value: kwargs["score"]})

            elif self.operation == "zrange":
                start = kwargs.get("start", 0)
                end = kwargs.get("end", -1)
                result = client.zrange(
                    key, start, end, withscores=kwargs.get("withscores", False)
                )

            # Utility operations
            elif self.operation == "exists":
                result = client.exists(key)

            elif self.operation == "ttl":
                result = client.ttl(key)

            elif self.operation == "ping":
                result = client.ping()

            elif self.operation == "info":
                result = client.info()

            elif self.operation == "flushdb":
                result = client.flushdb()

            else:
                raise NodeExecutionError(f"Unsupported operation: {self.operation}")

            # Handle Redis OK response
            if (
                result is True
                or (isinstance(result, bytes) and result == b"OK")
                or result == "OK"
            ):
                return {"result": result, "success": True}

            return {"result": result}

        except _redis_mod.RedisError as e:  # type: ignore[reportOptionalMemberAccess]
            logger.error(f"Redis error: {e}")
            raise NodeExecutionError(f"Redis operation failed: {str(e)}")
        except Exception as e:
            logger.error(f"Unexpected error: {e}")
            raise NodeExecutionError(f"Unexpected error: {str(e)}")
        finally:
            # Don't close the connection - keep it for connection pooling
            pass

    def __del__(self):
        """Clean up Redis connection."""
        if self._client:
            try:
                self._client.close()
            except Exception:
                pass
