import json
import logging
import os
import re
from typing import Optional
from urllib.parse import urlparse

import pytest
from redis import RedisCluster

from redis.backoff import NoBackoff, ExponentialBackoff
from redis.event import EventDispatcher, EventListenerInterface
from redis.multidb.client import MultiDBClient
from redis.multidb.config import (
    DatabaseConfig,
    MultiDbConfig,
    DEFAULT_HEALTH_CHECK_INTERVAL,
)
from redis.multidb.event import ActiveDatabaseChanged
from redis.multidb.failure_detector import DEFAULT_MIN_NUM_FAILURES
from redis.asyncio.multidb.healthcheck import DEFAULT_HEALTH_CHECK_DELAY
from redis.backoff import ExponentialWithJitterBackoff
from redis.client import Redis
from redis.maint_notifications import EndpointType, MaintNotificationsConfig
from redis.retry import Retry
from tests.test_scenario.fault_injector_client import (
    ProxyServerFaultInjector,
    REFaultInjector,
)

RELAXED_TIMEOUT = 30
CLIENT_TIMEOUT = 5

DEFAULT_ENDPOINT_NAME = "m-standard"
DEFAULT_OSS_API_ENDPOINT_NAME = "maint-notifications-oss-api"


class CheckActiveDatabaseChangedListener(EventListenerInterface):
    def __init__(self):
        self.is_changed_flag = False

    def listen(self, event: ActiveDatabaseChanged):
        self.is_changed_flag = True


def use_mock_proxy():
    return os.getenv("REDIS_ENTERPRISE_TESTS", "true").lower() == "false"


# Module-level singleton for fault injector client used in parametrize
# This ensures we create only ONE instance that's shared between parametrize and fixture
_FAULT_INJECTOR_CLIENT_OSS_API = (
    ProxyServerFaultInjector(oss_cluster=True)
    if use_mock_proxy()
    else REFaultInjector(os.getenv("FAULT_INJECTION_API_URL", "http://127.0.0.1:20324"))
)

# Module-level singleton for fault injector client used in parametrize
# This ensures we create only ONE instance that's shared between parametrize and fixture
_FAULT_INJECTOR_STANDALONE_CLIENT = (
    ProxyServerFaultInjector(oss_cluster=False)
    if use_mock_proxy()
    else REFaultInjector(os.getenv("FAULT_INJECTION_API_URL", "http://127.0.0.1:20324"))
)


@pytest.fixture()
def endpoint_name(request):
    return request.config.getoption("--endpoint-name") or os.getenv(
        "REDIS_ENDPOINT_NAME", DEFAULT_ENDPOINT_NAME
    )


@pytest.fixture()
def cluster_endpoint_name(request):
    return request.config.getoption("--cluster-endpoint-name") or os.getenv(
        "REDIS_CLUSTER_ENDPOINT_NAME", DEFAULT_OSS_API_ENDPOINT_NAME
    )


def get_endpoints_config(endpoint_name: str):
    endpoints_config = os.getenv("REDIS_ENDPOINTS_CONFIG_PATH", None)

    if not (endpoints_config and os.path.exists(endpoints_config)):
        raise FileNotFoundError(f"Endpoints config file not found: {endpoints_config}")

    try:
        with open(endpoints_config, "r") as f:
            data = json.load(f)
            db = data[endpoint_name]
            return db
    except Exception as e:
        raise ValueError(
            f"Failed to load endpoints config file: {endpoints_config}"
        ) from e


def get_bdbs_config(endpoint_name: str):
    bdbs_config = os.getenv("REDIS_BDBS_CONFIG_PATH", None)

    if not (bdbs_config and os.path.exists(bdbs_config)):
        raise FileNotFoundError(f"BDBs config file not found: {bdbs_config}")

    try:
        with open(bdbs_config, "r") as f:
            data = json.load(f)
            dbs = data["databases"]
            for db in dbs:
                if db["name"] == endpoint_name:
                    return db
            pytest.fail(f"Failed to find bdb config for {endpoint_name}")
    except Exception as e:
        raise ValueError(f"Failed to load bdbs config file: {bdbs_config}") from e


@pytest.fixture()
def endpoints_config(endpoint_name: str):
    return get_endpoints_config(endpoint_name)


@pytest.fixture()
def maint_notifications_cluster_bdb_config(cluster_endpoint_name: str):
    """
    Get the bdb config for the cluster used in the maint notifications tests.
    This will be used to create the test database for each test.
    The bdb config is the same for all tests, but the database is created with a random name.
    """
    return get_bdbs_config(cluster_endpoint_name)


@pytest.fixture()
def fault_injector_client():
    if use_mock_proxy():
        return ProxyServerFaultInjector(oss_cluster=False)
    else:
        url = os.getenv("FAULT_INJECTION_API_URL", "http://127.0.0.1:20324")
        return REFaultInjector(url)


@pytest.fixture()
def fault_injector_client_oss_api():
    """Return the singleton instance to ensure parametrize and tests use the same client."""
    return _FAULT_INJECTOR_CLIENT_OSS_API


@pytest.fixture()
def r_multi_db(
    request,
) -> tuple[MultiDBClient, CheckActiveDatabaseChangedListener, dict]:
    client_class = request.param.get("client_class", Redis)

    if client_class == Redis:
        endpoint_config = get_endpoints_config("re-active-active")
    else:
        endpoint_config = get_endpoints_config("re-active-active-oss-cluster")

    username = endpoint_config.get("username", None)
    password = endpoint_config.get("password", None)
    min_num_failures = request.param.get("min_num_failures", DEFAULT_MIN_NUM_FAILURES)
    command_retry = request.param.get(
        "command_retry", Retry(ExponentialBackoff(cap=0.1, base=0.01), retries=10)
    )

    # Retry configuration different for health checks as initial health check require more time in case
    # if infrastructure wasn't restored from the previous test.
    health_check_interval = request.param.get(
        "health_check_interval", DEFAULT_HEALTH_CHECK_INTERVAL
    )
    health_check_delay = request.param.get(
        "health_check_delay", DEFAULT_HEALTH_CHECK_DELAY
    )
    event_dispatcher = EventDispatcher()
    listener = CheckActiveDatabaseChangedListener()
    event_dispatcher.register_listeners(
        {
            ActiveDatabaseChanged: [listener],
        }
    )
    db_configs = []

    db_config = DatabaseConfig(
        weight=1.0,
        from_url=endpoint_config["endpoints"][0],
        client_kwargs={
            "username": username,
            "password": password,
            "decode_responses": True,
        },
        health_check_url=extract_cluster_fqdn(endpoint_config["endpoints"][0]),
    )
    db_configs.append(db_config)

    db_config1 = DatabaseConfig(
        weight=0.9,
        from_url=endpoint_config["endpoints"][1],
        client_kwargs={
            "username": username,
            "password": password,
            "decode_responses": True,
        },
        health_check_url=extract_cluster_fqdn(endpoint_config["endpoints"][1]),
    )
    db_configs.append(db_config1)

    config = MultiDbConfig(
        client_class=client_class,
        databases_config=db_configs,
        command_retry=command_retry,
        min_num_failures=min_num_failures,
        health_check_probes=3,
        health_check_interval=health_check_interval,
        event_dispatcher=event_dispatcher,
        health_check_delay=health_check_delay,
    )

    return MultiDBClient(config), listener, endpoint_config


def extract_cluster_fqdn(url):
    """
    Extract Cluster FQDN from Redis URL
    """
    # Parse the URL
    parsed = urlparse(url)

    # Extract hostname and port
    hostname = parsed.hostname

    # Remove the 'redis-XXXX.' prefix using regex
    # This pattern matches 'redis-' followed by digits and a dot
    cleaned_hostname = re.sub(r"^redis-\d+\.", "", hostname)

    # Reconstruct the URL
    return f"https://{cleaned_hostname}"


def _prepare_ssl_certificates(cert_chain: bool) -> dict:
    """
    Prepare SSL certificates for Redis cluster connection.

    Args:
        cert_chain: PEM-encoded certificate chain containing client cert + intermediate + CA cert.
                   This is the full certificate chain that will be used to validate the server.

    Returns:
        dict: SSL configuration kwargs for RedisCluster
    """
    certs_config_path = os.environ.get("MTLS_CONFIG_PATH", None)

    if not cert_chain:
        return {
            "ssl_cert_reqs": "none",
            "ssl_check_hostname": False,
        }

    if not certs_config_path:
        raise ValueError(
            "MTLS enabled test is triggered but MTLS_CONFIG_PATH environment variable not set"
        )

    # The cert_chain contains the full chain (client cert + intermediate + root CA)
    # Use it as CA data for validating the server's certificate
    return {
        "ssl_cert_reqs": "none",
        "ssl_keyfile": os.path.join(certs_config_path, "client.key"),
        "ssl_certfile": os.path.join(certs_config_path, "client.crt"),
    }


def _get_client_maint_notifications(
    endpoints_config,
    protocol: int = 3,
    enable_maintenance_notifications: bool = True,
    endpoint_type: Optional[EndpointType] = None,
    enable_relaxed_timeout: bool = True,
    enable_proactive_reconnect: bool = True,
    disable_retries: bool = False,
    auth_ssl_client_certs: bool = False,
    socket_timeout: Optional[float] = None,
    host_config: Optional[str] = None,
):
    """Create Redis client with maintenance notifications enabled."""

    # Get credentials from the configuration
    username = endpoints_config.get("username")
    password = endpoints_config.get("password")

    # Parse host and port from endpoints URL
    endpoints = endpoints_config.get("endpoints", [])
    if not endpoints:
        raise ValueError("No endpoints found in configuration")

    parsed = urlparse(endpoints[0])
    host = parsed.hostname if host_config is None else host_config
    port = parsed.port

    if not host:
        raise ValueError(f"Could not parse host from endpoint URL: {endpoints[0]}")

    logging.info(f"Connecting to Redis Enterprise: {host}:{port} with user: {username}")

    # Configure maintenance notifications
    maintenance_config = MaintNotificationsConfig(
        enabled=enable_maintenance_notifications,
        proactive_reconnect=enable_proactive_reconnect,
        relaxed_timeout=RELAXED_TIMEOUT if enable_relaxed_timeout else -1,
        endpoint_type=endpoint_type,
    )

    if disable_retries:
        retry = Retry(NoBackoff(), 0)
    else:
        retry = Retry(
            backoff=ExponentialWithJitterBackoff(base=0.01, cap=1), retries=10
        )

    tls_enabled = True if parsed.scheme == "rediss" else False
    logging.info(f"TLS enabled: {tls_enabled}")

    tls_kwargs = {"ssl": tls_enabled}
    if tls_enabled:
        ssl_config = _prepare_ssl_certificates(auth_ssl_client_certs)
        tls_kwargs.update(ssl_config)

    # Create Redis client with maintenance notifications config
    # This will automatically create the MaintNotificationsPoolHandler
    client = Redis(
        host=host,
        port=port,
        socket_timeout=CLIENT_TIMEOUT if socket_timeout is None else socket_timeout,
        username=username,
        password=password,
        protocol=protocol,  # RESP3 required for push notifications
        maint_notifications_config=maintenance_config,
        retry=retry,
        **tls_kwargs,
    )
    logging.info("Redis client created with maintenance notifications enabled")
    logging.info(f"Client uses Protocol: {client.connection_pool.get_protocol()}")

    return client


def get_standalone_client_maint_notifications(
    endpoints_config,
    protocol: int = 3,
    enable_maintenance_notifications: bool = True,
    endpoint_type: Optional[EndpointType] = None,
    enable_relaxed_timeout: bool = True,
    enable_proactive_reconnect: bool = True,
    disable_retries: bool = False,
    auth_ssl_client_certs: bool = False,
    socket_timeout: Optional[float] = None,
):
    """Create Redis stanalone client with maintenance notifications enabled."""
    # Get credentials from the configuration
    username = endpoints_config.get("username")
    password = endpoints_config.get("password")

    # Parse host and port from endpoints URL
    endpoints = endpoints_config.get("endpoints", [])
    if not endpoints:
        raise ValueError("No endpoints found in configuration")

    parsed = urlparse(endpoints[0])
    host = parsed.hostname
    port = parsed.port

    if not host:
        raise ValueError(f"Could not parse host from endpoint URL: {endpoints[0]}")

    logging.info(f"Connecting to Redis Enterprise: {host}:{port} with user: {username}")

    if disable_retries:
        retry = Retry(NoBackoff(), 0)
    else:
        retry = Retry(
            backoff=ExponentialWithJitterBackoff(base=0.01, cap=1), retries=10
        )

    tls_enabled = True if parsed.scheme == "rediss" else False
    logging.info(f"TLS enabled: {tls_enabled}")

    tls_kwargs = {"ssl": tls_enabled}

    if tls_enabled:
        # Prepare SSL certificate configuration
        ssl_config = _prepare_ssl_certificates(auth_ssl_client_certs)
        tls_kwargs.update(ssl_config)

    # Configure maintenance notifications
    maintenance_config = MaintNotificationsConfig(
        enabled=enable_maintenance_notifications,
        proactive_reconnect=enable_proactive_reconnect,
        relaxed_timeout=RELAXED_TIMEOUT if enable_relaxed_timeout else -1,
        endpoint_type=endpoint_type,
    )

    # Create Redis cluster client with maintenance notifications config
    client = Redis(
        host=host,
        port=port,
        socket_timeout=CLIENT_TIMEOUT if socket_timeout is None else socket_timeout,
        username=username,
        password=password,
        protocol=protocol,  # RESP3 required for push notifications
        maint_notifications_config=maintenance_config,
        retry=retry,
        **tls_kwargs,
    )
    logging.info(
        "Redis standalone client created with maintenance notifications enabled"
    )

    return client


def get_cluster_client_maint_notifications(
    endpoints_config,
    protocol: int = 3,
    enable_maintenance_notifications: bool = True,
    endpoint_type: Optional[EndpointType] = None,
    enable_relaxed_timeout: bool = True,
    enable_proactive_reconnect: bool = True,
    disable_retries: bool = False,
    auth_ssl_client_certs: bool = False,
    socket_timeout: Optional[float] = None,
    socket_connect_timeout: Optional[float] = None,
):
    """Create Redis cluster client with maintenance notifications enabled."""
    # Get credentials from the configuration
    username = endpoints_config.get("username")
    password = endpoints_config.get("password")

    # Parse host and port from endpoints URL
    endpoints = endpoints_config.get("endpoints", [])
    if not endpoints:
        raise ValueError("No endpoints found in configuration")

    parsed = urlparse(endpoints[0])
    host = parsed.hostname
    port = parsed.port

    if not host:
        raise ValueError(f"Could not parse host from endpoint URL: {endpoints[0]}")

    logging.info(f"Connecting to Redis Enterprise: {host}:{port} with user: {username}")

    if disable_retries:
        retry = Retry(NoBackoff(), 0)
    else:
        retry = Retry(
            backoff=ExponentialWithJitterBackoff(base=0.01, cap=1), retries=10
        )

    tls_enabled = True if parsed.scheme == "rediss" else False
    logging.info(f"TLS enabled: {tls_enabled}")

    tls_kwargs = {"ssl": tls_enabled}

    if tls_enabled:
        # Prepare SSL certificate configuration
        ssl_config = _prepare_ssl_certificates(auth_ssl_client_certs)
        tls_kwargs.update(ssl_config)

    # Configure maintenance notifications
    maintenance_config = MaintNotificationsConfig(
        enabled=enable_maintenance_notifications,
        proactive_reconnect=enable_proactive_reconnect,
        relaxed_timeout=RELAXED_TIMEOUT if enable_relaxed_timeout else -1,
        endpoint_type=endpoint_type,
    )

    # Create Redis cluster client with maintenance notifications config
    client = RedisCluster(
        host=host,
        port=port,
        socket_timeout=CLIENT_TIMEOUT if socket_timeout is None else socket_timeout,
        socket_connect_timeout=CLIENT_TIMEOUT
        if socket_connect_timeout is None
        else socket_connect_timeout,
        username=username,
        password=password,
        protocol=protocol,  # RESP3 required for push notifications
        maint_notifications_config=maintenance_config,
        retry=retry,
        **tls_kwargs,
    )
    logging.info("Redis cluster client created with maintenance notifications enabled")
    logging.info(
        f"Cluster working with the following nodes: {[(node.name, node.server_type) for node in client.get_nodes()]}"
    )

    return client
