import { createClient } from 'redis';

/**
 * Per-product rate limiting for extraction service.
 *
 * Tracks request rates per productId to prevent abuse and ensure
 * fair resource allocation across products.
 *
 * Limits:
 *   - Default: 100 requests/min per product
 *   - Configurable via env vars per tier
 */

// ── Configuration ───────────────────────────────────────────────

/** Configuration for product rate limiting */
export interface ProductRateLimitConfig {
  productId: string;
  windowMs: number;
  maxRequests: number;
}

// Default limits (can be overridden via env)
const DEFAULT_PRODUCT_RPM = parseInt(process.env.PRODUCT_RATE_LIMIT_RPM || '100', 10);
const WINDOW_MS = 60_000; // 1 minute
const STORE_MODE = process.env.PRODUCT_RATE_LIMIT_STORE === 'valkey' ? 'valkey' : 'memory';
const VALKEY_URL = process.env.VALKEY_URL || 'redis://valkey:6379';
const VALKEY_PREFIX = 'extraction:product-rate-limit';
type ValkeyClient = ReturnType<typeof createClient>;

// ── In-memory rate limit store ──────────────────────────────────

interface RateLimitEntry {
  count: number;
  windowStart: number;
  resetAt: number;
}

const rateLimitStore = new Map<string, RateLimitEntry>();
let valkeyClient: ValkeyClient | null = null;
let valkeyConnectPromise: Promise<ValkeyClient | null> | null = null;
let loggedValkeyFallback = false;

function getWindowIndex(now = Date.now()): number {
  return Math.floor(now / WINDOW_MS);
}

function getStoreKey(productId: string, now = Date.now()): string {
  return `${productId}:${getWindowIndex(now)}`;
}

function getValkeyKey(productId: string, now = Date.now()): string {
  return `${VALKEY_PREFIX}:${getWindowIndex(now)}:${encodeURIComponent(productId)}`;
}

function getWindowResetAt(now = Date.now()): number {
  return (getWindowIndex(now) + 1) * WINDOW_MS;
}

function shouldUseValkey(): boolean {
  return STORE_MODE === 'valkey' && process.env.NODE_ENV !== 'test';
}

function logValkeyFallback(err: unknown): void {
  if (loggedValkeyFallback) {
    return;
  }
  loggedValkeyFallback = true;
  const detail = err instanceof Error ? err.message : String(err);
  // eslint-disable-next-line no-console
  console.warn(`[product-rate-limit] Falling back to in-memory store: ${detail}`);
}

async function getValkeyClient(): Promise<ValkeyClient | null> {
  if (!shouldUseValkey()) {
    return null;
  }

  if (valkeyClient?.isOpen) {
    return valkeyClient;
  }

  if (!valkeyConnectPromise) {
    const client = createClient({ url: VALKEY_URL });
    client.on('error', err => {
      logValkeyFallback(err);
    });
    valkeyConnectPromise = client
      .connect()
      .then(() => {
        loggedValkeyFallback = false;
        valkeyClient = client;
        return client;
      })
      .catch(err => {
        logValkeyFallback(err);
        return null;
      })
      .finally(() => {
        valkeyConnectPromise = null;
      });
  }

  return valkeyConnectPromise;
}

function buildResult(now: number, count: number, resetAt: number): RateLimitResult {
  return {
    allowed: count <= DEFAULT_PRODUCT_RPM,
    limit: DEFAULT_PRODUCT_RPM,
    remaining: Math.max(0, DEFAULT_PRODUCT_RPM - count),
    resetAt,
    retryAfter:
      count > DEFAULT_PRODUCT_RPM ? Math.ceil(Math.max(0, resetAt - now) / 1000) : undefined,
  };
}

// ── Rate limiting functions ─────────────────────────────────────

export interface RateLimitResult {
  allowed: boolean;
  limit: number;
  remaining: number;
  resetAt: number;
  retryAfter?: number;
}

function checkProductRateLimitMemory(productId: string): RateLimitResult {
  const now = Date.now();
  const key = getStoreKey(productId, now);
  const resetAt = getWindowResetAt(now);
  const entry = rateLimitStore.get(key);

  if (!entry || now >= entry.resetAt) {
    rateLimitStore.set(key, {
      count: 1,
      windowStart: now,
      resetAt,
    });
    return buildResult(now, 1, resetAt);
  }

  entry.count++;
  return buildResult(now, entry.count, entry.resetAt);
}

async function checkProductRateLimitValkey(productId: string): Promise<RateLimitResult> {
  const now = Date.now();
  const client = await getValkeyClient();
  if (!client) {
    return checkProductRateLimitMemory(productId);
  }

  const resetAt = getWindowResetAt(now);
  const key = getValkeyKey(productId, now);
  const ttlSeconds = Math.max(1, Math.ceil((resetAt - now) / 1000) + 5);
  const count = await client.incr(key);

  if (count === 1) {
    await client.expire(key, ttlSeconds);
  }

  return buildResult(now, count, resetAt);
}

/**
 * Check if request is within product rate limit.
 */
export async function checkProductRateLimit(productId: string): Promise<RateLimitResult> {
  if (shouldUseValkey()) {
    return checkProductRateLimitValkey(productId);
  }
  return checkProductRateLimitMemory(productId);
}

function getProductRateLimitStatusMemory(productId: string): RateLimitResult {
  const now = Date.now();
  const key = getStoreKey(productId, now);
  const resetAt = getWindowResetAt(now);
  const entry = rateLimitStore.get(key);

  if (!entry || now >= entry.resetAt) {
    return buildResult(now, 0, resetAt);
  }

  return buildResult(now, entry.count, entry.resetAt);
}

async function getProductRateLimitStatusValkey(productId: string): Promise<RateLimitResult> {
  const now = Date.now();
  const client = await getValkeyClient();
  if (!client) {
    return getProductRateLimitStatusMemory(productId);
  }

  const count = parseInt((await client.get(getValkeyKey(productId, now))) || '0', 10);
  return buildResult(now, count, getWindowResetAt(now));
}

/**
 * Get current rate limit status for a product (without incrementing).
 */
export async function getProductRateLimitStatus(productId: string): Promise<RateLimitResult> {
  if (shouldUseValkey()) {
    return getProductRateLimitStatusValkey(productId);
  }
  return getProductRateLimitStatusMemory(productId);
}

function getRateLimitSummaryMemory(): {
  products: Array<{
    productId: string;
    currentWindow: number;
    requests: number;
    limit: number;
    resetAt: number;
  }>;
  totalProducts: number;
} {
  const now = Date.now();
  const products = new Map<string, { count: number; resetAt: number }>();

  for (const [key, entry] of rateLimitStore.entries()) {
    if (now < entry.resetAt) {
      const productId = key.split(':')[0];
      const existing = products.get(productId);
      if (existing) {
        existing.count += entry.count;
      } else {
        products.set(productId, { count: entry.count, resetAt: entry.resetAt });
      }
    }
  }

  return {
    products: [...products.entries()].map(([productId, data]) => ({
      productId,
      currentWindow: getWindowIndex(now),
      requests: data.count,
      limit: DEFAULT_PRODUCT_RPM,
      resetAt: data.resetAt,
    })),
    totalProducts: products.size,
  };
}

async function getRateLimitSummaryValkey(): Promise<{
  products: Array<{
    productId: string;
    currentWindow: number;
    requests: number;
    limit: number;
    resetAt: number;
  }>;
  totalProducts: number;
}> {
  const client = await getValkeyClient();
  if (!client) {
    return getRateLimitSummaryMemory();
  }

  const products = new Map<string, { count: number; resetAt: number; currentWindow: number }>();
  for await (const key of client.scanIterator({ MATCH: `${VALKEY_PREFIX}:*`, COUNT: 100 })) {
    const value = await client.get(key);
    if (!value) {
      continue;
    }

    const [, , , windowIndexRaw, ...productParts] = key.split(':');
    const windowIndex = parseInt(windowIndexRaw || '0', 10);
    const productId = decodeURIComponent(productParts.join(':'));
    const count = parseInt(value, 10);
    const resetAt = (windowIndex + 1) * WINDOW_MS;
    const existing = products.get(productId);

    if (existing) {
      existing.count += count;
      existing.resetAt = Math.max(existing.resetAt, resetAt);
      existing.currentWindow = Math.max(existing.currentWindow, windowIndex);
    } else {
      products.set(productId, { count, resetAt, currentWindow: windowIndex });
    }
  }

  return {
    products: [...products.entries()].map(([productId, data]) => ({
      productId,
      currentWindow: data.currentWindow,
      requests: data.count,
      limit: DEFAULT_PRODUCT_RPM,
      resetAt: data.resetAt,
    })),
    totalProducts: products.size,
  };
}

/**
 * Get rate limit summary across all products.
 */
export async function getRateLimitSummary(): Promise<{
  products: Array<{
    productId: string;
    currentWindow: number;
    requests: number;
    limit: number;
    resetAt: number;
  }>;
  totalProducts: number;
}> {
  if (shouldUseValkey()) {
    return getRateLimitSummaryValkey();
  }
  return getRateLimitSummaryMemory();
}

async function resetProductRateLimitValkey(productId: string): Promise<void> {
  const now = Date.now();
  const currentWindow = getWindowIndex(now);
  const client = await getValkeyClient();
  if (!client) {
    return;
  }

  await client.del([
    `${VALKEY_PREFIX}:${currentWindow - 1}:${encodeURIComponent(productId)}`,
    `${VALKEY_PREFIX}:${currentWindow}:${encodeURIComponent(productId)}`,
    `${VALKEY_PREFIX}:${currentWindow + 1}:${encodeURIComponent(productId)}`,
  ]);
}

/**
 * Reset rate limit for a product (admin operation).
 */
export async function resetProductRateLimit(productId: string): Promise<void> {
  const now = Date.now();
  const currentWindow = getWindowIndex(now);

  for (let i = -1; i <= 1; i++) {
    rateLimitStore.delete(`${productId}:${currentWindow + i}`);
  }

  if (shouldUseValkey()) {
    await resetProductRateLimitValkey(productId);
  }
}

export async function clearAllProductRateLimits(): Promise<void> {
  rateLimitStore.clear();

  const client = await getValkeyClient();
  if (!client) {
    return;
  }

  const keys: string[] = [];
  for await (const key of client.scanIterator({ MATCH: `${VALKEY_PREFIX}:*`, COUNT: 100 })) {
    keys.push(key);
  }

  if (keys.length > 0) {
    await client.del(keys);
  }
}

/**
 * Cleanup old entries from rate limit store.
 * Called periodically to prevent memory growth.
 */
export async function cleanupRateLimitStore(): Promise<number> {
  if (shouldUseValkey()) {
    return 0;
  }

  const now = Date.now();
  let cleaned = 0;

  for (const [key, entry] of rateLimitStore.entries()) {
    if (now >= entry.resetAt) {
      rateLimitStore.delete(key);
      cleaned++;
    }
  }

  return cleaned;
}

// Cleanup interval handle (exported for testing/shutdown)
let cleanupInterval: ReturnType<typeof setInterval> | null = null;

/**
 * Start automatic cleanup of expired rate limit entries.
 * Called during server startup.
 */
export function startRateLimitCleanup(): void {
  if (shouldUseValkey()) {
    return;
  }

  if (cleanupInterval) {
    clearInterval(cleanupInterval);
  }

  cleanupInterval = setInterval(
    async () => {
      const cleaned = await cleanupRateLimitStore();
      if (cleaned > 0 && process.env.NODE_ENV === 'development') {
        // eslint-disable-next-line no-console
        console.log(`[product-rate-limit] Cleaned up ${cleaned} expired entries`);
      }
    },
    5 * 60 * 1000
  );
}

/**
 * Stop automatic cleanup (for graceful shutdown/testing).
 */
export function stopRateLimitCleanup(): void {
  if (cleanupInterval) {
    clearInterval(cleanupInterval);
    cleanupInterval = null;
  }
}

export function getProductRateLimitStoreMode(): 'memory' | 'valkey' {
  return STORE_MODE;
}

export async function closeProductRateLimitStore(): Promise<void> {
  stopRateLimitCleanup();
  if (valkeyClient?.isOpen) {
    await valkeyClient.quit();
  }
  valkeyClient = null;
}

// Auto-start in production/development (not test)
if (process.env.NODE_ENV !== 'test') {
  startRateLimitCleanup();
}
