/**
 * Sehr einfache In-Memory Rate-Limits für Nitro/h3.
 *
 * Hinweis: In-Memory ist pro Prozess/Instance. Für horizontale Skalierung
 * sollte das auf Redis o.ä. umgestellt werden (siehe Doku).
 */

const buckets = globalThis.__HTC_RATE_LIMIT_BUCKETS__ || new Map()
// Persist across hot reloads
globalThis.__HTC_RATE_LIMIT_BUCKETS__ = buckets

function nowMs() {
  return Date.now()
}

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms))
}

export function getClientIp(event) {
  const xff = getHeader(event, 'x-forwarded-for')
  if (xff) {
    // First IP in list is original client
    const first = xff.split(',')[0]?.trim()
    if (first) return first
  }

  const realIp = getHeader(event, 'x-real-ip')
  if (realIp) return realIp.trim()

  return event?.node?.req?.socket?.remoteAddress || 'unknown'
}

function getBucket(key) {
  let b = buckets.get(key)
  if (!b) {
    b = {
      windowStart: nowMs(),
      count: 0,
      consecutiveFails: 0,
      lockedUntil: 0
    }
    buckets.set(key, b)
  }
  return b
}

function normalizeKeyPart(part) {
  return String(part || '')
    .trim()
    .toLowerCase()
    .replace(/\s+/g, ' ')
    .slice(0, 200)
}

function buildKey(name, keyParts) {
  const parts = (Array.isArray(keyParts) ? keyParts : [keyParts]).map(normalizeKeyPart)
  return `${name}:${parts.join(':')}`
}

function resetWindowIfNeeded(bucket, windowMs, now) {
  if (now - bucket.windowStart >= windowMs) {
    bucket.windowStart = now
    bucket.count = 0
    // consecutiveFails bleibt bewusst erhalten (Backoff für "nervige" Clients)
  }
}

export function assertRateLimit(event, options) {
  const {
    name,
    keyParts,
    windowMs = 10 * 60 * 1000,
    maxAttempts = 10,
    lockoutMs = 15 * 60 * 1000,
    statusCode = 429,
    message = 'Zu viele Versuche. Bitte später erneut versuchen.'
  } = options || {}

  const key = buildKey(name, keyParts)
  const bucket = getBucket(key)
  const now = nowMs()

  if (bucket.lockedUntil && bucket.lockedUntil > now) {
    const retryAfterSec = Math.ceil((bucket.lockedUntil - now) / 1000)
    setHeader(event, 'Retry-After', String(retryAfterSec))
    throw createError({ statusCode, statusMessage: message })
  }

  resetWindowIfNeeded(bucket, windowMs, now)

  if (bucket.count >= maxAttempts) {
    bucket.lockedUntil = now + lockoutMs
    const retryAfterSec = Math.ceil(lockoutMs / 1000)
    setHeader(event, 'Retry-After', String(retryAfterSec))
    throw createError({ statusCode, statusMessage: message })
  }

  // Count the attempt
  bucket.count += 1
}

export async function registerRateLimitFailure(event, options) {
  const {
    name,
    keyParts,
    delayBaseMs = 300,
    delayMaxMs = 5000
  } = options || {}

  const key = buildKey(name, keyParts)
  const bucket = getBucket(key)
  bucket.consecutiveFails = Math.min((bucket.consecutiveFails || 0) + 1, 30)

  // Exponential backoff: base * 2^(n-1)
  const delay = Math.min(delayBaseMs * Math.pow(2, bucket.consecutiveFails - 1), delayMaxMs)
  await sleep(delay)
}

export function registerRateLimitSuccess(_event, options) {
  const { name, keyParts } = options || {}
  const key = buildKey(name, keyParts)
  const bucket = getBucket(key)
  bucket.consecutiveFails = 0
  // Nach Erfolg darf es "frisch" starten
  bucket.count = 0
  bucket.windowStart = nowMs()
  bucket.lockedUntil = 0
}


