/**
 * Rate Limiter Service
 *
 * Zentraler Rate-Limiting-Service für alle API-Endpoints.
 * Verwendet Redis als primären Store mit In-Memory-Fallback.
 *
 * Features:
 * - Sliding Window Counter mit Redis
 * - Automatischer Fallback auf In-Memory bei Redis-Ausfall
 * - Vordefinierte Limiter für verschiedene Endpoint-Typen
 * - Thread-safe durch atomare Redis-Operationen
 */

import { getRedisClient, isRedisAvailable, setRedisAvailable } from '../redis'
import { recordSecurityEvent } from './security-observability'

export interface RateLimitConfig {
  /** Eindeutiger Name für diesen Limiter */
  name: string
  /** Maximale Anzahl von Requests */
  maxRequests: number
  /** Zeitfenster in Millisekunden */
  windowMs: number
  /** Optional: Prefix für Redis-Keys */
  keyPrefix?: string
}

export interface RateLimitResult {
  allowed: boolean
  remaining: number
  resetIn: number
  retryAfter?: number
}

interface RateLimitEntry {
  count: number
  windowStart: number
}

// In-Memory Store als Fallback
const memoryStores = new Map<string, Map<string, RateLimitEntry>>()

// Cleanup-Interval für Memory-Stores (alle 5 Minuten)
let cleanupIntervalId: NodeJS.Timeout | null = null

function startCleanupInterval() {
  if (cleanupIntervalId) return

  cleanupIntervalId = setInterval(
    () => {
      const now = Date.now()
      for (const [storeName, store] of memoryStores.entries()) {
        for (const [key, entry] of store.entries()) {
          // Entferne Einträge die älter als 2 Fenster sind
          const config = getConfigByName(storeName)
          if (config && now - entry.windowStart > config.windowMs * 2) {
            store.delete(key)
          }
        }
      }
    },
    5 * 60 * 1000,
  )
}

// Config-Registry
const configRegistry = new Map<string, RateLimitConfig>()

function getConfigByName(name: string): RateLimitConfig | undefined {
  return configRegistry.get(name)
}

/**
 * Erstellt einen Rate-Limiter mit der gegebenen Konfiguration
 */
export function createRateLimiter(config: RateLimitConfig) {
  configRegistry.set(config.name, config)

  // Memory-Store für diesen Limiter initialisieren
  if (!memoryStores.has(config.name)) {
    memoryStores.set(config.name, new Map())
  }

  startCleanupInterval()

  return {
    /**
     * Prüft ob ein Request erlaubt ist
     * @param identifier - Eindeutige ID (z.B. IP-Adresse oder User-ID)
     */
    check: async (identifier: string): Promise<RateLimitResult> => {
      return checkRateLimit(config, identifier)
    },

    /**
     * Setzt das Limit für einen Identifier zurück
     */
    reset: async (identifier: string): Promise<void> => {
      return resetRateLimit(config, identifier)
    },
  }
}

// Logging nur einmal pro Limiter
const loggedRedisUsage = new Set<string>()

async function checkRateLimit(
  config: RateLimitConfig,
  identifier: string,
): Promise<RateLimitResult> {
  const key = `${config.keyPrefix || 'ratelimit'}:${config.name}:${identifier}`
  const now = Date.now()

  // Versuche Redis, falls verfügbar
  if (isRedisAvailable()) {
    try {
      const result = await checkRateLimitRedis(config, key, now)
      // Log nur einmal pro Limiter-Typ
      if (!loggedRedisUsage.has(config.name)) {
        console.log(`[RateLimiter] ${config.name}: Using Redis store`)
        loggedRedisUsage.add(config.name)
      }
      return result
    } catch (error) {
      console.warn(`[RateLimiter] ${config.name}: Redis error, falling back to memory:`, (error as Error).message)
      loggedRedisUsage.delete(config.name) // Reset für nächsten erfolgreichen Redis-Aufruf
    }
  }

  // Fallback: In-Memory
  if (!loggedRedisUsage.has(`${config.name}:memory`)) {
    console.log(`[RateLimiter] ${config.name}: Using in-memory store (Redis unavailable)`)
    loggedRedisUsage.add(`${config.name}:memory`)
  }
  return checkRateLimitMemory(config, identifier, now)
}

async function checkRateLimitRedis(
  config: RateLimitConfig,
  key: string,
  now: number,
): Promise<RateLimitResult> {
  const redis = getRedisClient()
  if (!redis) throw new Error('Redis not available')

  const windowStart = now - (now % config.windowMs)
  const windowKey = `${key}:${windowStart}`
  const ttlSeconds = Math.ceil(config.windowMs / 1000) + 1

  try {
    // Atomic increment mit TTL in einer Pipeline für bessere Performance
    const pipeline = redis.pipeline()
    pipeline.incr(windowKey)
    pipeline.expire(windowKey, ttlSeconds)
    const results = await pipeline.exec()

    if (!results || !results[0]) {
      throw new Error('Pipeline execution failed')
    }

    const [incrErr, count] = results[0]
    if (incrErr) throw incrErr

    const remaining = Math.max(0, config.maxRequests - (count as number))
    const resetIn = windowStart + config.windowMs - now

    if ((count as number) > config.maxRequests) {
      recordSecurityEvent('rate_limit_blocked', {
        limiter: config.name,
        identifier: key,
      })
      return {
        allowed: false,
        remaining: 0,
        resetIn,
        retryAfter: Math.ceil(resetIn / 1000),
      }
    }

    return {
      allowed: true,
      remaining,
      resetIn,
    }
  } catch (error) {
    // Bei Redis-Fehler: Status aktualisieren für künftige Requests
    setRedisAvailable(false)
    throw error
  }
}

function checkRateLimitMemory(
  config: RateLimitConfig,
  identifier: string,
  now: number,
): RateLimitResult {
  const store = memoryStores.get(config.name)
  if (!store) {
    throw new Error(`Rate limiter store not found: ${config.name}`)
  }

  const entry = store.get(identifier)

  // Neues Fenster oder erster Request
  if (!entry || now - entry.windowStart > config.windowMs) {
    store.set(identifier, { count: 1, windowStart: now })
    return {
      allowed: true,
      remaining: config.maxRequests - 1,
      resetIn: config.windowMs,
    }
  }

  // Limit erreicht
  if (entry.count >= config.maxRequests) {
    const resetIn = entry.windowStart + config.windowMs - now
    recordSecurityEvent('rate_limit_blocked', {
      limiter: config.name,
      identifier,
    })
    return {
      allowed: false,
      remaining: 0,
      resetIn,
      retryAfter: Math.ceil(resetIn / 1000),
    }
  }

  // Request erlaubt
  entry.count++
  const resetIn = entry.windowStart + config.windowMs - now

  return {
    allowed: true,
    remaining: config.maxRequests - entry.count,
    resetIn,
  }
}

async function resetRateLimit(config: RateLimitConfig, identifier: string): Promise<void> {
  const key = `${config.keyPrefix || 'ratelimit'}:${config.name}:${identifier}`

  if (isRedisAvailable()) {
    try {
      const redis = getRedisClient()
      if (redis) {
        // Lösche alle Keys für diesen Identifier (pattern match)
        const keys = await redis.keys(`${key}:*`)
        if (keys.length > 0) {
          await redis.del(...keys)
        }
      }
    } catch (error) {
      console.warn('[RateLimiter] Redis reset error:', error)
    }
  }

  // Memory-Store bereinigen
  const store = memoryStores.get(config.name)
  if (store) {
    store.delete(identifier)
  }
}

// ============================================================================
// Vordefinierte Rate-Limiters für verschiedene Endpoints
// ============================================================================

/**
 * Rate-Limiter für öffentliche API-Endpoints
 * 60 Requests pro Minute pro IP
 */
export const publicApiLimiter = createRateLimiter({
  name: 'public-api',
  maxRequests: 60,
  windowMs: 60 * 1000,
})

/**
 * Rate-Limiter für Authentifizierungs-Endpoints
 * 5 Versuche pro 15 Minuten pro IP (gegen Brute-Force)
 */
export const authLimiter = createRateLimiter({
  name: 'auth',
  maxRequests: 5,
  windowMs: 15 * 60 * 1000,
})

/**
 * Rate-Limiter für E-Mail-Versand
 * 10 E-Mails pro Minute pro User
 */
export const emailLimiter = createRateLimiter({
  name: 'email',
  maxRequests: 10,
  windowMs: 60 * 1000,
})

/**
 * Rate-Limiter für Such-Endpoints
 * 30 Requests pro Minute pro IP
 */
export const searchLimiter = createRateLimiter({
  name: 'search',
  maxRequests: 30,
  windowMs: 60 * 1000,
})

/**
 * Rate-Limiter für Form-Submissions
 * 5 Submissions pro 10 Minuten pro IP (gegen Spam)
 */
export const formLimiter = createRateLimiter({
  name: 'form',
  maxRequests: 5,
  windowMs: 10 * 60 * 1000,
})

/**
 * Strenger Rate-Limiter für sensible Endpoints
 * 3 Requests pro Stunde pro IP
 */
export const strictLimiter = createRateLimiter({
  name: 'strict',
  maxRequests: 3,
  windowMs: 60 * 60 * 1000,
})

// ============================================================================
// Hilfsfunktionen
// ============================================================================

/**
 * Prüft ob Proxy-Headers vertraut werden sollen
 * TRUST_PROXY muss explizit auf 'true' gesetzt sein
 */
function shouldTrustProxy(): boolean {
  return process.env.TRUST_PROXY === 'true'
}

// Log einmalig bei Startup ob TRUST_PROXY konfiguriert ist
let trustProxyLogged = false
function logTrustProxyStatus() {
  if (trustProxyLogged) return
  trustProxyLogged = true

  if (shouldTrustProxy()) {
    console.log('[Security] TRUST_PROXY=true: Trusting X-Forwarded-For/X-Real-IP headers')
  } else {
    console.log('[Security] TRUST_PROXY not set: Ignoring proxy headers (set TRUST_PROXY=true if behind reverse proxy)')
  }
}

/**
 * Extrahiert die Client-IP aus Request-Headers
 *
 * SECURITY: Nur bei TRUST_PROXY=true werden X-Forwarded-For/X-Real-IP Headers
 * berücksichtigt. Ohne diese Einstellung könnten Angreifer ihre IP spoofing
 * und Rate-Limits sowie IP-Blocklists umgehen.
 *
 * In einer Reverse-Proxy-Umgebung (z.B. hinter Caddy) sollte TRUST_PROXY=true
 * gesetzt werden.
 */
export function getClientIp(headers: Headers): string {
  logTrustProxyStatus()

  // Nur Proxy-Headers vertrauen wenn explizit konfiguriert
  if (shouldTrustProxy()) {
    const forwarded = headers.get('x-forwarded-for')
    if (forwarded) {
      // Nimm nur die erste IP (vom Client-nächsten Proxy)
      return forwarded.split(',')[0]?.trim() || 'unknown'
    }

    const realIp = headers.get('x-real-ip')
    if (realIp) {
      return realIp.trim()
    }
  }

  // Fallback: In Next.js/Vercel wird die echte IP oft in x-real-ip gesetzt
  // Aber ohne TRUST_PROXY geben wir 'direct' zurück um zu signalisieren
  // dass keine vertrauenswürdige IP-Ermittlung möglich war
  //
  // HINWEIS: Für lokale Entwicklung oder wenn kein Proxy verwendet wird,
  // sollten Rate-Limits auf anderen Identifiern basieren (z.B. User-ID)
  return 'direct-connection'
}

/**
 * Erstellt Standard-Rate-Limit-Headers für Response
 */
export function rateLimitHeaders(result: RateLimitResult, maxRequests: number): HeadersInit {
  return {
    'X-RateLimit-Limit': String(maxRequests),
    'X-RateLimit-Remaining': String(result.remaining),
    'X-RateLimit-Reset': String(Math.ceil(result.resetIn / 1000)),
    ...(result.retryAfter ? { 'Retry-After': String(result.retryAfter) } : {}),
  }
}
