import { createLogger } from "@/logger.builder"
import Redis from "ioredis"

/**
 * Configuration options for Redis connection.
 */
export type RedisConfiguration = {
  /** The Redis connection URL */
  redisUrl: string
  /** Optional name for the Redis instance (used for logging) */
  name?: string
  /** Maximum delay between retry attempts in milliseconds */
  maxRetryDelay?: number
  /** Maximum number of retry attempts */
  maxRetries?: number
  /** Connection timeout in milliseconds */
  connectionTimeout?: number
  /** Command execution timeout in milliseconds */
  commandTimeout?: number
}

/**
 * Creates a new Redis client instance based on the provided configuration.
 * Supports both standard Redis and Redis Sentinel connections.
 * 
 * @param options - The Redis configuration options
 * @param options.redisUrl - The Redis connection URL
 * @param options.name - Optional name for the Redis instance
 * @param options.maxRetryDelay - Maximum delay between retry attempts
 * @param options.maxRetries - Maximum number of retry attempts
 * @param options.connectionTimeout - Connection timeout
 * @param options.commandTimeout - Command execution timeout
 * @returns A configured Redis client instance
 */
export const redisFactory = ({
  redisUrl,
  name,
  maxRetryDelay,
  maxRetries,
  connectionTimeout,
  commandTimeout,
}: RedisConfiguration): Redis => {
  const logger = createLogger(name ?? "redis")

  let redis: Redis

  if (redisUrl.startsWith("redis+sentinel://")) {
    const [credentialsAndHost, masterSet] = redisUrl.split("//")[1].split("/")
    let credentials: string | undefined
    let hostInfo: string | undefined
    let port: string | undefined
    if (credentialsAndHost.includes("@")) {
      ;[credentials, hostInfo] = credentialsAndHost.split("@")
    } else {
      ;[hostInfo, port] = credentialsAndHost.split(":")
    }

    let username: string | undefined
    let password: string | undefined
    let host: string

    if (credentials?.includes(":")) {
      ;[username, password] = credentials.split(":")
      ;[host, port] = hostInfo.split(":")
    } else {
      ;[host, port] = credentialsAndHost.split(":")
    }

    redis = redisSentinelFactory({
      sentinelUrl: host,
      port: Number.parseInt(port),
      username,
      password,
      setName: masterSet,
      maxRetryDelay,
      maxRetries,
      connectionTimeout,
      commandTimeout,
    })
  } else {
    redis = new Redis(redisUrl, {
      retryStrategy: (times) => {
        if (times > (maxRetries ?? 10)) {
          return null
        }

        const maxDelay = maxRetryDelay ?? 2000
        const delay = Math.min(times * 50, maxDelay)
        return delay
      },
      reconnectOnError: (err) => {
        const targetError = "READONLY"
        return err.message.includes(targetError)
      },
      maxRetriesPerRequest: maxRetries ?? 10,
      connectTimeout: connectionTimeout ?? 20000,
      commandTimeout: commandTimeout ?? 5000,
    })
  }

  // Connection events
  redis.on("connect", () => {
    logger.info("Connected to Redis server")
  })

  redis.on("ready", () => {
    logger.info("Redis client is ready to handle commands")
  })

  redis.on("error", (error) => {
    logger.error(`Redis failed with ${error.message}`, { error })
  })

  redis.on("close", () => {
    logger.info("Connection to Redis server was closed")
  })

  redis.on("reconnecting", (timeToReconnect: number) => {
    logger.info(`Reconnecting to Redis in ${timeToReconnect}ms...`)
  })

  redis.on("end", () => {
    logger.info("Redis client connection ended")
  })

  // Sentinel-specific events
  redis.on("+node", (node) => {
    logger.debug("New node discovered:", node)
  })

  redis.on("-node", (node) => {
    logger.debug("Node removed:", node)
  })

  redis.on("+sdown", (node) => {
    logger.warn("Node is subjectively down:", node)
  })

  redis.on("-sdown", (node) => {
    logger.warn("Node is subjectively up:", node)
  })

  redis.on("+failover-start", () => {
    logger.warn("Failover process has started")
  })

  redis.on("+failover-end", () => {
    logger.warn("Failover process has completed")
  })

  return redis
}

/**
 * Creates a Redis client configured for Sentinel mode.
 * 
 * @param options - The Redis Sentinel configuration options
 * @param options.sentinelUrl - The URL of the Sentinel server
 * @param options.port - The port number for the Sentinel server
 * @param options.username - Optional username for authentication
 * @param options.password - Optional password for authentication
 * @param options.setName - The name of the Sentinel set
 * @param options.maxRetryDelay - Maximum delay between retry attempts
 * @param options.maxRetries - Maximum number of retry attempts
 * @param options.connectionTimeout - Connection timeout
 * @param options.commandTimeout - Command execution timeout
 * @returns A configured Redis client instance in Sentinel mode
 */
export const redisSentinelFactory = (options: {
  sentinelUrl: string
  port?: number
  username?: string
  password?: string
  setName?: string
  maxRetryDelay?: number
  maxRetries?: number
  connectionTimeout?: number
  commandTimeout?: number
}) => {
  const configuration = {
    ...options,
    setName: options.setName ?? "mymaster",
  }
  return new Redis({
    sentinels: [{ host: configuration.sentinelUrl, port: configuration.port ?? 26379 }],
    retryStrategy: (times) => {
      if (times > (configuration.maxRetries ?? 10)) {
        return null
      }

      const maxDelay = configuration.maxRetryDelay ?? 2000
      const delay = Math.min(times * 50, maxDelay)
      return delay
    },
    reconnectOnError: (err) => {
      const targetError = "READONLY"
      return err.message.includes(targetError)
    },
    maxRetriesPerRequest: configuration.maxRetries ?? 10,
    connectTimeout: configuration.connectionTimeout ?? 20000,
    commandTimeout: configuration.commandTimeout ?? 5000,
    ...(configuration.password && { sentinelPassword: configuration.password }),
    ...(configuration.password && { password: configuration.password }),
    ...(configuration.username && { username: configuration.username }),
    name: configuration.setName,
  })
}

/**
 * Stores Redis client instances for reuse
 */
const redisPool = new Map<string, Redis>()

/**
 * Stores Redis configurations for each connection URL
 */
const redisConfigurationCache = new Map<string, RedisConfiguration>()

const poolLogger = createLogger("redis-pool-factory")

/**
 * Configures a Redis instance with the specified options.
 * If an instance with the same URL already exists, it won't be reconfigured unless override is true.
 * 
 * @param options - The Redis configuration options
 * @param override - Whether to override existing configuration
 */
export const configureRedisInstance = (options: RedisConfiguration, override?: boolean) => {
  if (redisConfigurationCache.has(options.redisUrl) && !override) {
    poolLogger.warn("Redis configuration already exists", { redisUrl: options.redisUrl })
    return
  }

  redisConfigurationCache.set(options.redisUrl, options)
}

/**
 * Retrieves a Redis instance for the specified URL.
 * If an instance doesn't exist, it creates one using the cached configuration.
 * 
 * @param options - Object containing the Redis URL
 * @param options.redisUrl - The Redis connection URL
 * @returns A Redis client instance
 * @throws Error if no configuration exists for the specified URL
 */
export const getRedisInstance = (options: { redisUrl: string }) => {
  let redis = redisPool.get(options.redisUrl)
  if (!redis) {
    const configuration = redisConfigurationCache.get(options.redisUrl)
    if (!configuration) {
      poolLogger.error("Redis configuration not found", { redisUrl: options.redisUrl })
      throw new Error("Redis configuration not found")
    }

    redis = redisFactory(configuration)
    redisPool.set(options.redisUrl, redis)
  }

  return redis
}
