import fs from 'node:fs';
import path from 'node:path';
import Redis from 'ioredis';
import { Client } from 'discord.js';

import { loadConfig } from '@/util/configLoader.js';
import {
  logManagerNotification,
  NotificationType,
  notifyManagers,
} from '@/util/notificationHandler.js';

const config = loadConfig();

// Redis connection state
let isRedisAvailable = false;
let redis: Redis;
let connectionAttempts = 0;
const MAX_RETRY_ATTEMPTS = config.redis.retryAttempts;
const INITIAL_RETRY_DELAY = config.redis.initialRetryDelay;
let hasNotifiedDisconnect = false;
let discordClient: Client | null = null;

// ========================
// Redis Utility Classes and Helper Functions
// ========================

/**
 * Custom error class for Redis errors
 */
class RedisError extends Error {
  constructor(
    message: string,
    public originalError?: Error,
  ) {
    super(message);
    this.name = 'RedisError';
  }
}

/**
 * Redis error handler
 * @param errorMessage - The error message to log
 * @param error - The error object
 */
const handleRedisError = (errorMessage: string, error: Error): null => {
  console.error(`${errorMessage}:`, error);
  throw new RedisError(errorMessage, error);
};

/**
 * Sets the Discord client for sending notifications
 * @param client - The Discord client
 */
export function setDiscordClient(client: Client): void {
  discordClient = client;
}

/**
 * Initializes the Redis connection with retry logic
 */
async function initializeRedisConnection() {
  try {
    if (redis && redis.status !== 'end' && redis.status !== 'close') {
      return;
    }

    redis = new Redis(config.redis.redisConnectionString, {
      retryStrategy(times) {
        connectionAttempts = times;
        if (times >= MAX_RETRY_ATTEMPTS) {
          const message = `Failed to connect to Redis after ${times} attempts. Caching will be disabled.`;
          console.warn(message);

          if (!hasNotifiedDisconnect && discordClient) {
            logManagerNotification(NotificationType.REDIS_CONNECTION_LOST);
            notifyManagers(
              discordClient,
              NotificationType.REDIS_CONNECTION_LOST,
              `Connection attempts exhausted after ${times} tries. Caching is now disabled.`,
            );
            hasNotifiedDisconnect = true;
          }

          return null;
        }

        const delay = Math.min(INITIAL_RETRY_DELAY * Math.pow(2, times), 30000);
        console.log(
          `Retrying Redis connection in ${delay}ms... (Attempt ${times + 1}/${MAX_RETRY_ATTEMPTS})`,
        );
        return delay;
      },
      maxRetriesPerRequest: 3,
      enableOfflineQueue: true,
      tls: (() => {
        try {
          return {
            ca: fs.readFileSync(path.resolve('./certs/rootCA.pem')),
            key: fs.readFileSync(path.resolve('./certs/cache-key.pem')),
            cert: fs.readFileSync(path.resolve('./certs/cache-cert.pem')),
          };
        } catch (error) {
          console.warn(
            'Failed to load certificates for cache, using insecure connection:',
            error,
          );
          return undefined;
        }
      })(),
    });

    // ========================
    // Redis Events
    // ========================
    redis.on('error', (error: Error) => {
      console.error('Redis Connection Error:', error);
      isRedisAvailable = false;
    });

    redis.on('connect', () => {
      console.info('Successfully connected to Redis');
      isRedisAvailable = true;
      connectionAttempts = 0;

      if (hasNotifiedDisconnect && discordClient) {
        logManagerNotification(NotificationType.REDIS_CONNECTION_RESTORED);
        notifyManagers(
          discordClient,
          NotificationType.REDIS_CONNECTION_RESTORED,
        );
        hasNotifiedDisconnect = false;
      }
    });

    redis.on('close', () => {
      console.warn('Redis connection closed');
      isRedisAvailable = false;

      // Try to reconnect after some time if we've not exceeded max attempts
      if (connectionAttempts < MAX_RETRY_ATTEMPTS) {
        const delay = Math.min(
          INITIAL_RETRY_DELAY * Math.pow(2, connectionAttempts),
          30000,
        );
        setTimeout(initializeRedisConnection, delay);
      } else if (!hasNotifiedDisconnect && discordClient) {
        logManagerNotification(NotificationType.REDIS_CONNECTION_LOST);
        notifyManagers(
          discordClient,
          NotificationType.REDIS_CONNECTION_LOST,
          'Connection closed and max retry attempts reached.',
        );
        hasNotifiedDisconnect = true;
      }
    });

    redis.on('reconnecting', () => {
      console.info('Attempting to reconnect to Redis...');
    });
  } catch (error) {
    console.error('Failed to initialize Redis:', error);
    isRedisAvailable = false;

    if (!hasNotifiedDisconnect && discordClient) {
      logManagerNotification(
        NotificationType.REDIS_CONNECTION_LOST,
        `Error: ${error}`,
      );
      notifyManagers(
        discordClient,
        NotificationType.REDIS_CONNECTION_LOST,
        `Initialization error: ${error}`,
      );
      hasNotifiedDisconnect = true;
    }
  }
}

// Initialize Redis connection
initializeRedisConnection();

/**
 * Check if Redis is currently available, and attempt to reconnect if not
 * @returns - True if Redis is connected and available
 */
export async function ensureRedisConnection(): Promise<boolean> {
  if (!isRedisAvailable) {
    await initializeRedisConnection();
  }
  return isRedisAvailable;
}

// ========================
// Redis Functions
// ========================

/**
 * Function to set a key in Redis
 * @param key - The key to set
 * @param value - The value to set
 * @param ttl - The time to live for the key
 * @returns - 'OK' if successful
 */
export async function set(
  key: string,
  value: string,
  ttl?: number,
): Promise<'OK' | null> {
  if (!(await ensureRedisConnection())) {
    console.warn('Redis unavailable, skipping set operation');
    return null;
  }

  try {
    await redis.set(`bot:${key}`, value);
    if (ttl) await redis.expire(`bot:${key}`, ttl);
    return 'OK';
  } catch (error) {
    return handleRedisError(`Failed to set key: ${key}`, error as Error);
  }
}

/**
 * Function to set a key in Redis with a JSON value
 * @param key - The key to set
 * @param value - The value to set
 * @param ttl - The time to live for the key
 * @returns - 'OK' if successful
 */
export async function setJson<T>(
  key: string,
  value: T,
  ttl?: number,
): Promise<'OK' | null> {
  return await set(key, JSON.stringify(value), ttl);
}

/**
 * Increments a key in Redis
 * @param key - The key to increment
 * @returns - The new value of the key, or null if Redis is unavailable
 */
export async function incr(key: string): Promise<number | null> {
  if (!(await ensureRedisConnection())) {
    console.warn('Redis unavailable, skipping increment operation');
    return null;
  }

  try {
    return await redis.incr(`bot:${key}`);
  } catch (error) {
    return handleRedisError(`Failed to increment key: ${key}`, error as Error);
  }
}

/**
 * Checks if a key exists in Redis
 * @param key - The key to check
 * @returns - True if the key exists, false otherwise, or null if Redis is unavailable
 */
export async function exists(key: string): Promise<boolean | null> {
  if (!(await ensureRedisConnection())) {
    console.warn('Redis unavailable, skipping exists operation');
    return null;
  }

  try {
    return (await redis.exists(`bot:${key}`)) === 1;
  } catch (error) {
    return handleRedisError(
      `Failed to check if key exists: ${key}`,
      error as Error,
    );
  }
}

/**
 * Gets the value of a key in Redis
 * @param key - The key to get
 * @returns - The value of the key, or null if the key does not exist or Redis is unavailable
 */
export async function get(key: string): Promise<string | null> {
  if (!(await ensureRedisConnection())) {
    console.warn('Redis unavailable, skipping get operation');
    return null;
  }

  try {
    return await redis.get(`bot:${key}`);
  } catch (error) {
    return handleRedisError(`Failed to get key: ${key}`, error as Error);
  }
}

/**
 * Gets the values of multiple keys in Redis
 * @param keys - The keys to get
 * @returns - The values of the keys, or null if Redis is unavailable
 */
export async function mget(
  ...keys: string[]
): Promise<(string | null)[] | null> {
  if (!(await ensureRedisConnection())) {
    console.warn('Redis unavailable, skipping mget operation');
    return null;
  }

  try {
    return await redis.mget(...keys.map((key) => `bot:${key}`));
  } catch (error) {
    return handleRedisError('Failed to get keys', error as Error);
  }
}

/**
 * Gets the value of a key in Redis and parses it as a JSON object
 * @param key - The key to get
 * @returns - The parsed JSON value of the key, or null if the key does not exist or Redis is unavailable
 */
export async function getJson<T>(key: string): Promise<T | null> {
  const value = await get(key);
  if (!value) return null;
  try {
    return JSON.parse(value) as T;
  } catch {
    return null;
  }
}

/**
 * Deletes a key in Redis
 * @param key - The key to delete
 * @returns - The number of keys that were deleted, or null if Redis is unavailable
 */
export async function del(key: string): Promise<number | null> {
  if (!(await ensureRedisConnection())) {
    console.warn('Redis unavailable, skipping delete operation');
    return null;
  }

  try {
    return await redis.del(`bot:${key}`);
  } catch (error) {
    return handleRedisError(`Failed to delete key: ${key}`, error as Error);
  }
}

/**
 * Check if Redis is currently available
 * @returns - True if Redis is connected and available
 */
export function isRedisConnected(): boolean {
  return isRedisAvailable;
}
