/**
 * Utility functions
 */

import type {
  RecommendRequest,
  ValidationError,
  Server,
  VPSBenchmark,
  TechSpec,
  BenchmarkData,
  AIRecommendationResponse,
  UseCaseConfig,
  BandwidthEstimate,
  BandwidthInfo
} from './types';
import { USE_CASE_CONFIGS, i18n, LIMITS } from './config';

/**
 * JSON response helper
 */
export function jsonResponse(
  data: any,
  status: number,
  headers: Record<string, string> = {}
): Response {
  return new Response(JSON.stringify(data), {
    status,
    headers: {
      'Content-Type': 'application/json',
      'Content-Security-Policy': "default-src 'none'",
      'X-Content-Type-Options': 'nosniff',
      'X-Frame-Options': 'DENY',
      'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
      'Cache-Control': 'no-store',
      ...headers,
    },
  });
}

/**
 * Simple hash function for strings
 */
export function hashString(str: string): string {
  let hash = 0;
  for (let i = 0; i < str.length; i++) {
    const char = str.charCodeAt(i);
    hash = ((hash << 5) - hash) + char;
    hash = hash & hash; // Convert to 32-bit integer
  }
  // Use >>> 0 to convert to unsigned 32-bit integer
  return (hash >>> 0).toString(36);
}

/**
 * Sanitize special characters for cache key
 */
export function sanitizeCacheValue(value: string): string {
  // Use URL-safe base64 encoding to avoid collisions
  try {
    return btoa(value).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
  } catch {
    // Fallback for non-ASCII characters
    return encodeURIComponent(value).replace(/[%]/g, '_');
  }
}

/**
 * Generate cache key from request parameters
 */
export function generateCacheKey(req: RecommendRequest): string {
  // Don't mutate original arrays - create sorted copies
  const sortedStack = [...req.tech_stack].sort();
  const sanitizedStack = sortedStack.map(sanitizeCacheValue).join(',');

  // Hash use_case to avoid special characters and length issues
  const useCaseHash = hashString(req.use_case);

  const parts = [
    `stack:${sanitizedStack}`,
    `users:${req.expected_users}`,
    `case:${useCaseHash}`,
  ];

  if (req.traffic_pattern) {
    parts.push(`traffic:${req.traffic_pattern}`);
  }

  if (req.region_preference) {
    const sortedRegions = [...req.region_preference].sort();
    const sanitizedRegions = sortedRegions.map(sanitizeCacheValue).join(',');
    parts.push(`reg:${sanitizedRegions}`);
  }

  if (req.budget_limit) {
    parts.push(`budget:${req.budget_limit}`);
  }

  if (req.provider_filter && req.provider_filter.length > 0) {
    const sortedProviders = [...req.provider_filter].sort();
    const sanitizedProviders = sortedProviders.map(sanitizeCacheValue).join(',');
    parts.push(`prov:${sanitizedProviders}`);
  }

  // Include language in cache key
  if (req.lang) {
    parts.push(`lang:${req.lang}`);
  }

  return `recommend:${parts.join('|')}`;
}

/**
 * Default region filter SQL for when no region is specified
 * Used in both /api/recommend and /api/servers
 */
export const DEFAULT_REGION_FILTER_SQL = `(
  -- Korea (Seoul)
  r.region_code IN ('icn', 'ap-northeast-2') OR
  LOWER(r.region_name) LIKE '%seoul%' OR
  -- Japan (Tokyo, Osaka)
  r.region_code IN ('nrt', 'itm', 'ap-northeast-1', 'ap-northeast-3') OR
  LOWER(r.region_code) LIKE '%tyo%' OR
  LOWER(r.region_code) LIKE '%osa%' OR
  LOWER(r.region_name) LIKE '%tokyo%' OR
  LOWER(r.region_name) LIKE '%osaka%' OR
  -- Singapore
  r.region_code IN ('sgp', 'ap-southeast-1') OR
  LOWER(r.region_code) LIKE '%sin%' OR
  LOWER(r.region_code) LIKE '%sgp%' OR
  LOWER(r.region_name) LIKE '%singapore%'
)`;

/**
 * Escape LIKE pattern special characters
 */
export function escapeLikePattern(pattern: string): string {
  return pattern.replace(/[%_\\]/g, '\\$&');
}

/**
 * Type guard to validate Server object structure
 */
export function isValidServer(obj: unknown): obj is Server {
  if (!obj || typeof obj !== 'object') return false;
  const s = obj as Record<string, unknown>;
  return (
    typeof s.id === 'number' &&
    typeof s.provider_name === 'string' &&
    typeof s.instance_id === 'string' &&
    typeof s.vcpu === 'number' &&
    typeof s.memory_mb === 'number' &&
    typeof s.monthly_price === 'number'
  );
}

/**
 * Type guard to validate VPSBenchmark object structure
 */
export function isValidVPSBenchmark(obj: unknown): obj is VPSBenchmark {
  if (!obj || typeof obj !== 'object') return false;
  const v = obj as Record<string, unknown>;
  return (
    typeof v.id === 'number' &&
    typeof v.provider_name === 'string' &&
    typeof v.vcpu === 'number' &&
    typeof v.geekbench_single === 'number'
  );
}

/**
 * Type guard to validate TechSpec object structure
 */
export function isValidTechSpec(obj: unknown): obj is TechSpec {
  if (!obj || typeof obj !== 'object') return false;
  const t = obj as Record<string, unknown>;
  return (
    typeof t.id === 'number' &&
    typeof t.name === 'string' &&
    typeof t.vcpu_per_users === 'number' &&
    typeof t.min_memory_mb === 'number'
  );
}

/**
 * Type guard to validate BenchmarkData object structure
 */
export function isValidBenchmarkData(obj: unknown): obj is BenchmarkData {
  if (!obj || typeof obj !== 'object') return false;
  const b = obj as Record<string, unknown>;
  return (
    typeof b.id === 'number' &&
    typeof b.processor_name === 'string' &&
    typeof b.benchmark_name === 'string' &&
    typeof b.score === 'number'
  );
}

/**
 * Type guard to validate AI recommendation structure
 */
export function isValidAIRecommendation(obj: unknown): obj is AIRecommendationResponse['recommendations'][0] {
  if (!obj || typeof obj !== 'object') return false;
  const r = obj as Record<string, unknown>;
  return (
    (typeof r.server_id === 'number' || typeof r.server_id === 'string') &&
    typeof r.score === 'number' &&
    r.analysis !== null &&
    typeof r.analysis === 'object' &&
    r.estimated_capacity !== null &&
    typeof r.estimated_capacity === 'object'
  );
}

/**
 * Validate recommendation request
 */
export function validateRecommendRequest(body: any, lang: string = 'en'): ValidationError | null {
  // Ensure lang is valid
  const validLang = ['en', 'zh', 'ja', 'ko'].includes(lang) ? lang : 'en';
  const messages = i18n[validLang];

  if (!body || typeof body !== 'object') {
    return {
      error: 'Request body must be a JSON object',
      missing_fields: ['tech_stack', 'expected_users', 'use_case'],
      schema: messages.schema,
      example: messages.example
    };
  }

  const missingFields: string[] = [];
  const invalidFields: { field: string; reason: string }[] = [];

  // Check required fields
  if (!body.tech_stack) {
    missingFields.push('tech_stack');
  } else if (!Array.isArray(body.tech_stack) || body.tech_stack.length === 0) {
    invalidFields.push({ field: 'tech_stack', reason: 'must be a non-empty array of strings' });
  } else if (body.tech_stack.length > LIMITS.MAX_TECH_STACK) {
    invalidFields.push({ field: 'tech_stack', reason: `must not exceed ${LIMITS.MAX_TECH_STACK} items` });
  } else if (!body.tech_stack.every((item: any) => typeof item === 'string')) {
    invalidFields.push({ field: 'tech_stack', reason: 'all items must be strings' });
  }

  if (body.expected_users === undefined) {
    missingFields.push('expected_users');
  } else if (typeof body.expected_users !== 'number' || body.expected_users < 1) {
    invalidFields.push({ field: 'expected_users', reason: 'must be a positive number' });
  } else if (body.expected_users > 10000000) {
    invalidFields.push({ field: 'expected_users', reason: 'must not exceed 10,000,000' });
  }

  if (!body.use_case) {
    missingFields.push('use_case');
  } else if (typeof body.use_case !== 'string' || body.use_case.trim().length === 0) {
    invalidFields.push({ field: 'use_case', reason: 'must be a non-empty string' });
  } else if (body.use_case.length > LIMITS.MAX_USE_CASE_LENGTH) {
    invalidFields.push({ field: 'use_case', reason: `must not exceed ${LIMITS.MAX_USE_CASE_LENGTH} characters` });
  }

  // Check optional fields if provided
  if (body.traffic_pattern !== undefined && !['steady', 'spiky', 'growing'].includes(body.traffic_pattern)) {
    invalidFields.push({ field: 'traffic_pattern', reason: "must be one of: 'steady', 'spiky', 'growing'" });
  }

  if (body.region_preference !== undefined) {
    if (!Array.isArray(body.region_preference)) {
      invalidFields.push({ field: 'region_preference', reason: 'must be an array' });
    } else if (body.region_preference.length > LIMITS.MAX_REGION_PREFERENCE) {
      invalidFields.push({ field: 'region_preference', reason: `must not exceed ${LIMITS.MAX_REGION_PREFERENCE} items` });
    } else if (!body.region_preference.every((item: any) => typeof item === 'string')) {
      invalidFields.push({ field: 'region_preference', reason: 'all items must be strings' });
    }
  }

  if (body.budget_limit !== undefined && (typeof body.budget_limit !== 'number' || body.budget_limit < 0)) {
    invalidFields.push({ field: 'budget_limit', reason: 'must be a non-negative number' });
  }

  if (body.provider_filter !== undefined) {
    if (!Array.isArray(body.provider_filter)) {
      invalidFields.push({ field: 'provider_filter', reason: 'must be an array' });
    } else if (body.provider_filter.length > 10) {
      invalidFields.push({ field: 'provider_filter', reason: 'must not exceed 10 items' });
    } else if (!body.provider_filter.every((item: any) => typeof item === 'string')) {
      invalidFields.push({ field: 'provider_filter', reason: 'all items must be strings' });
    }
  }

  // Validate lang field if provided
  if (body.lang !== undefined && !['en', 'zh', 'ja', 'ko'].includes(body.lang)) {
    invalidFields.push({ field: 'lang', reason: "must be one of: 'en', 'zh', 'ja', 'ko'" });
  }

  // Return error if any issues found
  if (missingFields.length > 0 || invalidFields.length > 0) {
    return {
      error: missingFields.length > 0 ? messages.missingFields : messages.invalidFields,
      ...(missingFields.length > 0 && { missing_fields: missingFields }),
      ...(invalidFields.length > 0 && { invalid_fields: invalidFields }),
      schema: messages.schema,
      example: messages.example
    };
  }

  return null;
}

/**
 * Helper function to get allowed CORS origin
 */
export function getAllowedOrigin(request: Request): string {
  const allowedOrigins = [
    'https://server-recommend.kappa-d8e.workers.dev',
  ];
  const origin = request.headers.get('Origin');

  // If Origin is provided and matches allowed list, return it
  if (origin && allowedOrigins.includes(origin)) {
    return origin;
  }

  // For requests without Origin (non-browser: curl, API clients, server-to-server)
  // Return empty string - CORS headers won't be sent but request is still processed
  // This is safe because CORS only affects browser requests
  if (!origin) {
    return '';
  }

  // Origin provided but not in allowed list - return first allowed origin
  // Browser will block the response due to CORS mismatch
  return allowedOrigins[0];
}

/**
 * Find use case configuration by matching patterns
 */
export function findUseCaseConfig(useCase: string): UseCaseConfig {
  const useCaseLower = useCase.toLowerCase();

  for (const config of USE_CASE_CONFIGS) {
    if (config.patterns.test(useCaseLower)) {
      return config;
    }
  }

  // Default configuration
  return {
    category: 'default',
    patterns: /.*/,
    dauMultiplier: { min: 10, max: 14 },
    activeRatio: 0.5
  };
}

/**
 * Get DAU multiplier based on use case (how many daily active users per concurrent user)
 */
export function getDauMultiplier(useCase: string): { min: number; max: number } {
  return findUseCaseConfig(useCase).dauMultiplier;
}

/**
 * Get active user ratio (what percentage of DAU actually performs the bandwidth-heavy action)
 */
export function getActiveUserRatio(useCase: string): number {
  return findUseCaseConfig(useCase).activeRatio;
}

/**
 * Estimate monthly bandwidth based on concurrent users and use case
 */
export function estimateBandwidth(concurrentUsers: number, useCase: string, trafficPattern?: string): BandwidthEstimate {
  const useCaseLower = useCase.toLowerCase();

  // Get use case configuration
  const config = findUseCaseConfig(useCase);
  const useCaseCategory = config.category;

  // Calculate DAU estimate from concurrent users with use-case-specific multipliers
  const dauMultiplier = config.dauMultiplier;
  const estimatedDauMin = Math.round(concurrentUsers * dauMultiplier.min);
  const estimatedDauMax = Math.round(concurrentUsers * dauMultiplier.max);
  const dailyUniqueVisitors = Math.round((estimatedDauMin + estimatedDauMax) / 2);
  const activeUserRatio = config.activeRatio;
  const activeDau = Math.round(dailyUniqueVisitors * activeUserRatio);

  // Traffic pattern adjustment
  let patternMultiplier = 1.0;
  if (trafficPattern === 'spiky') {
    patternMultiplier = 1.5; // Account for peak loads
  } else if (trafficPattern === 'growing') {
    patternMultiplier = 1.3; // Headroom for growth
  }

  let dailyBandwidthGB: number;
  let bandwidthModel: string;

  // ========== IMPROVED BANDWIDTH MODELS ==========
  // Each use case uses the most appropriate calculation method

  switch (useCaseCategory) {
    case 'video': {
      // VIDEO/STREAMING: Bitrate-based model
      const is4K = /4k|uhd|ultra/i.test(useCaseLower);
      const bitrateGBperHour = is4K ? 11.25 : 2.25; // 4K vs HD
      const avgWatchTimeHours = is4K ? 1.0 : 1.5;
      const gbPerActiveUser = bitrateGBperHour * avgWatchTimeHours;
      dailyBandwidthGB = activeDau * gbPerActiveUser * patternMultiplier;
      bandwidthModel = `bitrate-based: ${activeDau} active × ${bitrateGBperHour} GB/hr × ${avgWatchTimeHours}hr`;
      break;
    }

    case 'file': {
      // FILE DOWNLOAD: File-size based model
      const isLargeFiles = /iso|video|backup|대용량/.test(useCaseLower);
      const avgFileSizeGB = isLargeFiles ? 2.0 : 0.2;
      const downloadsPerUser = isLargeFiles ? 1 : 3;
      const gbPerActiveUser = avgFileSizeGB * downloadsPerUser;
      dailyBandwidthGB = activeDau * gbPerActiveUser * patternMultiplier;
      bandwidthModel = `file-based: ${activeDau} active × ${avgFileSizeGB} GB × ${downloadsPerUser} downloads`;
      break;
    }

    case 'gaming': {
      // GAMING: Session-duration based model
      const isMinecraft = /minecraft|마인크래프트/.test(useCaseLower);
      const mbPerHour = isMinecraft ? 150 : 80;
      const avgSessionHours = isMinecraft ? 3 : 2.5;
      const gbPerActiveUser = (mbPerHour * avgSessionHours) / 1024;
      dailyBandwidthGB = activeDau * gbPerActiveUser * patternMultiplier;
      bandwidthModel = `session-based: ${activeDau} active × ${mbPerHour} MB/hr × ${avgSessionHours}hr`;
      break;
    }

    case 'api': {
      // API/SAAS: Request-based model
      const avgRequestKB = 20;
      const requestsPerUserPerDay = 1000;
      const gbPerActiveUser = (avgRequestKB * requestsPerUserPerDay) / (1024 * 1024);
      dailyBandwidthGB = activeDau * gbPerActiveUser * patternMultiplier;
      bandwidthModel = `request-based: ${activeDau} active × ${avgRequestKB}KB × ${requestsPerUserPerDay} req`;
      break;
    }

    case 'ecommerce': {
      // E-COMMERCE: Page-based model (images heavy)
      const avgPageSizeMB = 2.5;
      const pagesPerSession = 20;
      const gbPerActiveUser = (avgPageSizeMB * pagesPerSession) / 1024;
      dailyBandwidthGB = activeDau * gbPerActiveUser * patternMultiplier;
      bandwidthModel = `page-based: ${activeDau} active × ${avgPageSizeMB}MB × ${pagesPerSession} pages`;
      break;
    }

    case 'forum': {
      // FORUM/COMMUNITY: Page-based model (text + some images)
      const avgPageSizeMB = 0.7;
      const pagesPerSession = 30;
      const gbPerActiveUser = (avgPageSizeMB * pagesPerSession) / 1024;
      dailyBandwidthGB = activeDau * gbPerActiveUser * patternMultiplier;
      bandwidthModel = `page-based: ${activeDau} active × ${avgPageSizeMB}MB × ${pagesPerSession} pages`;
      break;
    }

    case 'blog': {
      // STATIC/BLOG: Lightweight page-based model
      const avgPageSizeMB = 1.5;
      const pagesPerSession = 4;
      const gbPerActiveUser = (avgPageSizeMB * pagesPerSession) / 1024;
      dailyBandwidthGB = activeDau * gbPerActiveUser * patternMultiplier;
      bandwidthModel = `page-based: ${activeDau} active × ${avgPageSizeMB}MB × ${pagesPerSession} pages`;
      break;
    }

    case 'chat': {
      // CHAT/MESSAGING: Message-based model
      const textBandwidthMB = (3 * 200) / 1024; // 3KB × 200 messages
      const attachmentBandwidthMB = 20; // occasional images/files
      const gbPerActiveUser = (textBandwidthMB + attachmentBandwidthMB) / 1024;
      dailyBandwidthGB = activeDau * gbPerActiveUser * patternMultiplier;
      bandwidthModel = `message-based: ${activeDau} active × ~20MB/user (text+attachments)`;
      break;
    }

    default: {
      // DEFAULT: General web app (page-based)
      const avgPageSizeMB = 1.0;
      const pagesPerSession = 10;
      const gbPerActiveUser = (avgPageSizeMB * pagesPerSession) / 1024;
      dailyBandwidthGB = activeDau * gbPerActiveUser * patternMultiplier;
      bandwidthModel = `page-based (default): ${activeDau} active × ${avgPageSizeMB}MB × ${pagesPerSession} pages`;
      break;
    }
  }

  console.log(`[Bandwidth] Model: ${bandwidthModel}`);
  console.log(`[Bandwidth] DAU: ${dailyUniqueVisitors} (${dauMultiplier.min}-${dauMultiplier.max}x), Active: ${activeDau} (${(activeUserRatio * 100).toFixed(0)}%), Daily: ${dailyBandwidthGB.toFixed(1)} GB`);

  // Monthly bandwidth
  const monthlyGB = dailyBandwidthGB * 30;
  const monthlyTB = monthlyGB / 1024;

  // Categorize
  let category: 'light' | 'moderate' | 'heavy' | 'very_heavy';
  let description: string;

  if (monthlyTB < 0.5) {
    category = 'light';
    description = `~${Math.round(monthlyGB)} GB/month - Most VPS plans include sufficient bandwidth`;
  } else if (monthlyTB < 2) {
    category = 'moderate';
    description = `~${monthlyTB.toFixed(1)} TB/month - Check provider bandwidth limits`;
  } else if (monthlyTB < 6) {
    category = 'heavy';
    description = `~${monthlyTB.toFixed(1)} TB/month - Prefer providers with generous bandwidth (Linode: 1-6TB included)`;
  } else {
    category = 'very_heavy';
    description = `~${monthlyTB.toFixed(1)} TB/month - HIGH BANDWIDTH: Linode strongly recommended for cost savings`;
  }

  return {
    monthly_gb: Math.round(monthlyGB),
    monthly_tb: Math.round(monthlyTB * 10) / 10,
    daily_gb: Math.round(dailyBandwidthGB * 10) / 10,
    category,
    description,
    estimated_dau_min: estimatedDauMin,
    estimated_dau_max: estimatedDauMax,
    active_ratio: activeUserRatio
  };
}

/**
 * Get provider bandwidth allocation based on memory size
 * Returns included transfer in TB/month
 */
export function getProviderBandwidthAllocation(providerName: string, memoryGb: number): {
  included_tb: number;
  overage_per_gb: number;
  overage_per_tb: number;
} {
  const provider = providerName.toLowerCase();

  if (provider.includes('linode')) {
    // Linode: roughly 1TB per 1GB RAM (Nanode 1GB = 1TB, 2GB = 2TB, etc.)
    const includedTb = Math.min(Math.max(memoryGb, 1), 20);
    return {
      included_tb: includedTb,
      overage_per_gb: 0.005,  // $0.005/GB = $5/TB
      overage_per_tb: 5
    };
  } else if (provider.includes('vultr')) {
    // Vultr: varies by plan, roughly 1-2TB for small, up to 10TB for large
    let includedTb: number;
    if (memoryGb <= 2) includedTb = 1;
    else if (memoryGb <= 4) includedTb = 2;
    else if (memoryGb <= 8) includedTb = 3;
    else if (memoryGb <= 16) includedTb = 4;
    else if (memoryGb <= 32) includedTb = 5;
    else includedTb = Math.min(memoryGb / 4, 10);

    return {
      included_tb: includedTb,
      overage_per_gb: 0.01,   // $0.01/GB = $10/TB
      overage_per_tb: 10
    };
  } else {
    // Default/Other providers: conservative estimate
    return {
      included_tb: Math.min(memoryGb, 5),
      overage_per_gb: 0.01,
      overage_per_tb: 10
    };
  }
}

/**
 * Calculate bandwidth cost info for a server
 */
export function calculateBandwidthInfo(
  server: import('./types').Server,
  bandwidthEstimate: BandwidthEstimate
): BandwidthInfo {
  const allocation = getProviderBandwidthAllocation(server.provider_name, server.memory_gb);
  const estimatedTb = bandwidthEstimate.monthly_tb;
  const overageTb = Math.max(0, estimatedTb - allocation.included_tb);
  const overageCost = overageTb * allocation.overage_per_tb;

  // Convert server price to USD if needed for total cost calculation
  const serverPriceUsd = server.currency === 'KRW'
    ? server.monthly_price / 1400  // Approximate KRW to USD
    : server.monthly_price;

  const totalCost = serverPriceUsd + overageCost;

  let warning: string | undefined;
  if (overageTb > allocation.included_tb) {
    warning = `⚠️ 예상 트래픽(${estimatedTb.toFixed(1)}TB)이 기본 포함량(${allocation.included_tb}TB)의 2배 이상입니다. 상위 플랜을 고려하세요.`;
  } else if (overageTb > 0) {
    warning = `예상 초과 트래픽: ${overageTb.toFixed(1)}TB (추가 비용 ~$${overageCost.toFixed(0)}/월)`;
  }

  return {
    included_transfer_tb: allocation.included_tb,
    overage_cost_per_gb: allocation.overage_per_gb,
    overage_cost_per_tb: allocation.overage_per_tb,
    estimated_monthly_tb: Math.round(estimatedTb * 10) / 10,
    estimated_overage_tb: Math.round(overageTb * 10) / 10,
    estimated_overage_cost: Math.round(overageCost * 100) / 100,
    total_estimated_cost: Math.round(totalCost * 100) / 100,
    warning
  };
}

/**
 * Sanitize user input for AI prompts to prevent prompt injection
 */
export function sanitizeForAIPrompt(input: string, maxLength: number = 200): string {
  // Remove potential prompt injection patterns
  let sanitized = input
    .replace(/ignore\s*(all|previous|above)?\s*instruction/gi, '[filtered]')
    .replace(/system\s*prompt/gi, '[filtered]')
    .replace(/you\s*are\s*(now|a)/gi, '[filtered]')
    .replace(/pretend\s*(to\s*be|you)/gi, '[filtered]')
    .replace(/act\s*as/gi, '[filtered]')
    .replace(/disregard/gi, '[filtered]');

  // Limit length
  return sanitized.slice(0, maxLength);
}

// In-memory fallback for rate limiting when CACHE KV is unavailable
const inMemoryRateLimit = new Map<string, { count: number; resetTime: number }>();

/**
 * Rate limiting check using KV storage with in-memory fallback
 */
export async function checkRateLimit(clientIP: string, env: import('./types').Env): Promise<{ allowed: boolean; requestId: string }> {
  const requestId = crypto.randomUUID();
  const now = Date.now();
  const maxRequests = LIMITS.RATE_LIMIT_MAX_REQUESTS;
  const windowMs = LIMITS.RATE_LIMIT_WINDOW_MS;

  // Use in-memory fallback if CACHE unavailable
  if (!env.CACHE) {
    const record = inMemoryRateLimit.get(clientIP);

    if (!record || record.resetTime < now) {
      // New window or expired
      inMemoryRateLimit.set(clientIP, { count: 1, resetTime: now + windowMs });
      return { allowed: true, requestId };
    }

    if (record.count >= maxRequests) {
      return { allowed: false, requestId };
    }

    // Increment count
    record.count++;
    return { allowed: true, requestId };
  }

  // KV-based rate limiting
  const kvKey = `ratelimit:${clientIP}`;

  try {
    const recordJson = await env.CACHE.get(kvKey);
    const record = recordJson ? JSON.parse(recordJson) as { count: number; resetTime: number } : null;

    if (!record || record.resetTime < now) {
      // New window
      await env.CACHE.put(
        kvKey,
        JSON.stringify({ count: 1, resetTime: now + windowMs }),
        { expirationTtl: 60 }
      );
      return { allowed: true, requestId };
    }

    if (record.count >= maxRequests) {
      return { allowed: false, requestId };
    }

    // Increment count
    record.count++;
    await env.CACHE.put(
      kvKey,
      JSON.stringify(record),
      { expirationTtl: 60 }
    );
    return { allowed: true, requestId };
  } catch (error) {
    console.error('[RateLimit] KV error:', error);
    // On error, deny the request (fail closed) for security
    return { allowed: false, requestId };
  }
}
