/*
 * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
 *
 * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
 */

use crate::InMemoryStore;
use deadpool::{
    Runtime,
    managed::{Manager, Pool},
};
use redis::{
    Client, IntoConnectionInfo, ProtocolVersion,
    cluster::{ClusterClient, ClusterClientBuilder},
    cluster_read_routing::RandomReplicaStrategy,
    sentinel::{SentinelClient, SentinelClientBuilder, SentinelServerType},
};
use registry::{
    schema::{enums::RedisProtocol, structs},
    types::duration::Duration,
};
use std::{fmt::Display, sync::Arc};

pub mod lookup;
pub mod pool;

#[derive(Debug)]
pub struct RedisStore {
    pub pool: RedisPool,
}

pub struct RedisConnectionManager {
    pub client: Client,
    timeout: std::time::Duration,
}

pub struct RedisClusterConnectionManager {
    pub client: ClusterClient,
    timeout: std::time::Duration,
}

pub struct RedisSentinelConnectionManager {
    pub client: tokio::sync::Mutex<SentinelClient>,
    timeout: std::time::Duration,
}

pub enum RedisPool {
    Single(Pool<RedisConnectionManager>),
    Cluster(Pool<RedisClusterConnectionManager>),
    Sentinel(Pool<RedisSentinelConnectionManager>),
}

impl RedisStore {
    pub async fn open_single(config: structs::RedisStore) -> Result<InMemoryStore, String> {
        Ok(InMemoryStore::Redis(Arc::new(RedisStore {
            pool: RedisPool::Single(build_pool(
                RedisConnectionManager {
                    client: Client::open(config.url)
                        .map_err(|err| format!("Failed to open Redis client: {err:?}"))?,
                    timeout: config.timeout.into_inner(),
                },
                config.pool_max_connections,
                config.pool_timeout_create,
                config.pool_timeout_wait,
                config.pool_timeout_recycle,
            )?),
        })))
    }

    pub async fn open_cluster(config: structs::RedisClusterStore) -> Result<InMemoryStore, String> {
        let mut builder = ClusterClientBuilder::new(config.urls);
        if let Some(value) = config.auth_username {
            builder = builder.username(value);
        }
        if let Some(value) = config.auth_secret.secret().await?.map(|v| v.into_owned()) {
            builder = builder.password(value);
        }
        if let Some(value) = config.max_retries {
            builder = builder.retries(value as u32);
        }
        if let Some(value) = config.max_retry_wait {
            builder = builder.max_retry_wait(value.as_millis());
        }
        if let Some(value) = config.min_retry_wait {
            builder = builder.min_retry_wait(value.as_millis());
        }
        if config.read_from_replicas {
            builder = builder.read_routing_strategy(RandomReplicaStrategy);
        }
        if matches!(config.protocol_version, RedisProtocol::Resp3) {
            builder = builder.use_protocol(ProtocolVersion::RESP3);
        }

        let client = builder
            .build()
            .map_err(|err| format!("Failed to open Redis client: {err:?}"))?;

        Ok(InMemoryStore::Redis(Arc::new(RedisStore {
            pool: RedisPool::Cluster(build_pool(
                RedisClusterConnectionManager {
                    client,
                    timeout: config.timeout.into_inner(),
                },
                config.pool_max_connections,
                config.pool_timeout_create,
                config.pool_timeout_wait,
                config.pool_timeout_recycle,
            )?),
        })))
    }

    pub async fn open_sentinel(
        config: structs::RedisSentinelStore,
    ) -> Result<InMemoryStore, String> {
        let mut sentinels = Vec::with_capacity(config.urls.len());
        for url in config.urls {
            let info = url
                .into_connection_info()
                .map_err(|err| format!("Invalid Redis Sentinel URL: {err}"))?;
            sentinels.push(info.addr().clone());
        }

        let mut builder =
            SentinelClientBuilder::new(sentinels, config.service_name, SentinelServerType::Master)
                .map_err(|err| format!("Failed to create Redis Sentinel client: {err:?}"))?;

        if let Some(value) = config.auth_username {
            builder = builder.set_client_to_redis_username(value);
        }
        if let Some(value) = config.auth_secret.secret().await?.map(|v| v.into_owned()) {
            builder = builder.set_client_to_redis_password(value);
        }
        if let Some(value) = config.sentinel_username {
            builder = builder.set_client_to_sentinel_username(value);
        }
        if let Some(value) = config
            .sentinel_secret
            .secret()
            .await?
            .map(|v| v.into_owned())
        {
            builder = builder.set_client_to_sentinel_password(value);
        }
        if matches!(config.protocol_version, RedisProtocol::Resp3) {
            builder = builder.set_client_to_redis_protocol(ProtocolVersion::RESP3);
        }

        let client = builder
            .build()
            .map_err(|err| format!("Failed to open Redis Sentinel client: {err:?}"))?;

        Ok(InMemoryStore::Redis(Arc::new(RedisStore {
            pool: RedisPool::Sentinel(build_pool(
                RedisSentinelConnectionManager {
                    client: tokio::sync::Mutex::new(client),
                    timeout: config.timeout.into_inner(),
                },
                config.pool_max_connections,
                config.pool_timeout_create,
                config.pool_timeout_wait,
                config.pool_timeout_recycle,
            )?),
        })))
    }
}

fn build_pool<M: Manager>(
    manager: M,
    max_size: u64,
    create_timeout: Option<Duration>,
    wait_timeout: Option<Duration>,
    recycle_timeout: Option<Duration>,
) -> Result<Pool<M>, String> {
    Pool::builder(manager)
        .runtime(Runtime::Tokio1)
        .max_size(max_size as usize)
        .create_timeout(create_timeout.map(|v| v.into_inner()))
        .wait_timeout(wait_timeout.map(|v| v.into_inner()))
        .recycle_timeout(recycle_timeout.map(|v| v.into_inner()))
        .build()
        .map_err(|err| format!("Failed to build pool: {err}"))
}

#[inline(always)]
fn into_error(err: impl Display) -> trc::Error {
    trc::StoreEvent::RedisError.reason(err)
}

impl std::fmt::Debug for RedisPool {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Single(_) => f.debug_tuple("Single").finish(),
            Self::Cluster(_) => f.debug_tuple("Cluster").finish(),
            Self::Sentinel(_) => f.debug_tuple("Sentinel").finish(),
        }
    }
}
