/**
 * Cloudflare Worker - Server Recommendation System
 *
 * AI-powered server recommendation service using Workers AI, D1, and KV.
 */

interface Env {
  AI: Ai;  // Legacy - kept for fallback
  DB: D1Database;
  CACHE: KVNamespace;
  OPENAI_API_KEY: string;
  AI_GATEWAY_URL?: string;  // Cloudflare AI Gateway URL to bypass regional restrictions
}

interface ValidationError {
  error: string;
  missing_fields?: string[];
  invalid_fields?: { field: string; reason: string }[];
  schema: Record<string, string>;
  example: Record<string, any>;
}

interface RecommendRequest {
  tech_stack: string[];
  expected_users: number;
  use_case: string;
  traffic_pattern?: 'steady' | 'spiky' | 'growing';
  region_preference?: string[];
  budget_limit?: number;
  provider_filter?: string[]; // Filter by specific providers (e.g., ["Linode", "Vultr"])
  lang?: 'en' | 'zh' | 'ja' | 'ko'; // Response language
}

interface Server {
  id: number;
  provider_name: string;
  instance_id: string;
  instance_name: string;
  vcpu: number;
  memory_mb: number;
  memory_gb: number;
  storage_gb: number;
  network_speed_gbps: number | null;
  instance_family: string | null;
  gpu_count: number;
  gpu_type: string | null;
  monthly_price: number;
  currency: 'USD' | 'KRW';
  region_name: string;
  region_code: string;
}

interface BandwidthInfo {
  included_transfer_tb: number;      // 기본 포함 트래픽 (TB/월)
  overage_cost_per_gb: number;       // 초과 비용 ($/GB)
  overage_cost_per_tb: number;       // 초과 비용 ($/TB)
  estimated_monthly_tb: number;      // 예상 월간 사용량 (TB)
  estimated_overage_tb: number;      // 예상 초과량 (TB)
  estimated_overage_cost: number;    // 예상 초과 비용 ($)
  total_estimated_cost: number;      // 총 예상 비용 (서버 + 트래픽)
  warning?: string;                  // 트래픽 관련 경고
}

interface RecommendationResult {
  server: Server;
  score: number;
  analysis: {
    tech_fit: string;
    capacity: string;
    cost_efficiency: string;
    scalability: string;
  };
  estimated_capacity: {
    max_daily_users?: number;
    max_concurrent_users: number;
    requests_per_second: number;
  };
  bandwidth_info?: BandwidthInfo;
  benchmark_reference?: BenchmarkReference;
  vps_benchmark_reference?: {
    plan_name: string;
    geekbench_single: number;
    geekbench_multi: number;
    monthly_price_usd: number;
    performance_per_dollar: number;
  };
}

interface BenchmarkReference {
  processor_name: string;
  benchmarks: {
    name: string;
    category: string;
    score: number;
    percentile: number;
  }[];
}

interface BenchmarkData {
  id: number;
  processor_name: string;
  benchmark_name: string;
  category: string;
  score: number;
  percentile: number;
  cores: number | null;
}

interface VPSBenchmark {
  id: number;
  provider_name: string;
  plan_name: string;
  cpu_type: string;
  vcpu: number;
  memory_gb: number;
  country_code: string;
  geekbench_single: number;
  geekbench_multi: number;
  geekbench_total: number;
  monthly_price_usd: number;
  performance_per_dollar: number;
  geekbench_version: string;
  gb6_single_normalized: number;
  gb6_multi_normalized: number;
}

interface TechSpec {
  id: number;
  name: string;
  category: string;
  vcpu_per_users: number;
  vcpu_per_users_max: number | null;
  min_memory_mb: number;
  max_memory_mb: number | null;
  description: string | null;
  aliases: string | null;
  is_memory_intensive: boolean;
  is_cpu_intensive: boolean;
}

interface BandwidthEstimate {
  monthly_gb: number;
  monthly_tb: number;
  daily_gb: number;
  category: 'light' | 'moderate' | 'heavy' | 'very_heavy';
  description: string;
  estimated_dau_min: number;  // Daily Active Users estimate (min)
  estimated_dau_max: number;  // Daily Active Users estimate (max)
  active_ratio: number;       // Active user ratio (0.0-1.0)
}

/**
 * Get DAU multiplier based on use case (how many daily active users per concurrent user)
 */
function getDauMultiplier(useCase: string): { min: number; max: number } {
  const useCaseLower = useCase.toLowerCase();

  if (/game|gaming|minecraft|게임/.test(useCaseLower)) {
    // Gaming: users stay online longer, higher concurrent ratio
    return { min: 10, max: 20 };
  } else if (/blog|news|static|블로그|뉴스|포트폴리오/.test(useCaseLower)) {
    // Blog/Static: short visits, lower concurrent ratio
    return { min: 30, max: 50 };
  } else if (/api|saas|backend|서비스|백엔드/.test(useCaseLower)) {
    // SaaS/API: business hours concentration
    return { min: 5, max: 10 };
  } else if (/e-?commerce|shop|store|쇼핑|커머스|온라인몰/.test(useCaseLower)) {
    // E-commerce: moderate session lengths
    return { min: 20, max: 30 };
  } else if (/forum|community|board|게시판|커뮤니티|포럼/.test(useCaseLower)) {
    // Forum/Community: moderate engagement
    return { min: 15, max: 25 };
  } else if (/video|stream|media|youtube|netflix|vod|동영상|스트리밍|미디어/.test(useCaseLower)) {
    // Video/Streaming: medium-long sessions
    return { min: 8, max: 12 };
  } else {
    // Default: general web app
    return { min: 10, max: 14 };
  }
}

/**
 * Get active user ratio (what percentage of DAU actually performs the bandwidth-heavy action)
 */
function getActiveUserRatio(useCase: string): number {
  const useCaseLower = useCase.toLowerCase();

  if (/video|stream|media|youtube|netflix|vod|동영상|스트리밍|미디어/.test(useCaseLower)) {
    // Video/Streaming: only 30% of DAU actually stream
    return 0.3;
  } else if (/game|gaming|minecraft|게임/.test(useCaseLower)) {
    // Gaming: 50% of DAU are active players
    return 0.5;
  } else if (/e-?commerce|shop|store|쇼핑|커머스|온라인몰/.test(useCaseLower)) {
    // E-commerce: 40% browse products
    return 0.4;
  } else if (/api|saas|backend|서비스|백엔드/.test(useCaseLower)) {
    // API/SaaS: 60% active usage
    return 0.6;
  } else if (/forum|community|board|게시판|커뮤니티|포럼/.test(useCaseLower)) {
    // Forum/Community: 50% active posting/reading
    return 0.5;
  } else if (/blog|static|portfolio|블로그|포트폴리오/.test(useCaseLower)) {
    // Static/Blog: 30% active readers
    return 0.3;
  } else {
    // Default: 50% active
    return 0.5;
  }
}

/**
 * Estimate monthly bandwidth based on concurrent users and use case
 *
 * Formula: concurrent_users × dau_multiplier × active_ratio × avg_page_size_mb × requests_per_session × active_hours × 30
 *
 * Multipliers by use case:
 * - Static site/blog: 0.5 MB/request, 5 requests/session
 * - API/SaaS: 0.1 MB/request, 50 requests/session
 * - E-commerce: 1 MB/request, 20 requests/session
 * - Media/Video: 50 MB/request, 10 requests/session
 * - Gaming: 0.05 MB/request, 1000 requests/session
 * - Forum/Community: 0.3 MB/request, 30 requests/session
 */
function estimateBandwidth(concurrentUsers: number, useCase: string, trafficPattern?: string): BandwidthEstimate {
  const useCaseLower = useCase.toLowerCase();

  // Calculate DAU estimate from concurrent users with use-case-specific multipliers
  const dauMultiplier = getDauMultiplier(useCase);
  const estimatedDauMin = Math.round(concurrentUsers * dauMultiplier.min);
  const estimatedDauMax = Math.round(concurrentUsers * dauMultiplier.max);
  const dailyUniqueVisitors = Math.round((estimatedDauMin + estimatedDauMax) / 2);
  const activeUserRatio = getActiveUserRatio(useCase);
  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

  if (/video|stream|media|youtube|netflix|vod|동영상|스트리밍|미디어/.test(useCaseLower)) {
    // VIDEO/STREAMING: Bitrate-based model
    // - HD streaming: ~5 Mbps = 2.25 GB/hour
    // - Average watch time: 1.5 hours per session
    // - 4K streaming: ~25 Mbps = 11.25 GB/hour (detect if mentioned)
    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;   // 4K users watch less
    const gbPerActiveUser = bitrateGBperHour * avgWatchTimeHours;
    dailyBandwidthGB = activeDau * gbPerActiveUser * patternMultiplier;
    bandwidthModel = `bitrate-based: ${activeDau} active × ${bitrateGBperHour} GB/hr × ${avgWatchTimeHours}hr`;

  } else if (/download|file|storage|cdn|파일|다운로드|저장소/.test(useCaseLower)) {
    // FILE DOWNLOAD: File-size based model
    // - Average file size: 100-500 MB depending on type
    // - Downloads per active user: 2-5 per day
    const isLargeFiles = /iso|video|backup|대용량/.test(useCaseLower);
    const avgFileSizeGB = isLargeFiles ? 2.0 : 0.2; // 2GB for large, 200MB for normal
    const downloadsPerUser = isLargeFiles ? 1 : 3;
    const gbPerActiveUser = avgFileSizeGB * downloadsPerUser;
    dailyBandwidthGB = activeDau * gbPerActiveUser * patternMultiplier;
    bandwidthModel = `file-based: ${activeDau} active × ${avgFileSizeGB} GB × ${downloadsPerUser} downloads`;

  } else if (/game|gaming|minecraft|게임/.test(useCaseLower)) {
    // GAMING: Session-duration based model
    // - Multiplayer games: 50-150 MB/hour (small packets, frequent)
    // - Average session: 2-3 hours
    // - Minecraft specifically uses more due to chunk loading
    const isMinecraft = /minecraft|마인크래프트/.test(useCaseLower);
    const mbPerHour = isMinecraft ? 150 : 80; // Minecraft uses more
    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`;

  } else if (/api|saas|backend|서비스|백엔드/.test(useCaseLower)) {
    // API/SAAS: Request-based model
    // - Average request+response: 10-50 KB
    // - Requests per active user per day: 500-2000
    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`;

  } else if (/e-?commerce|shop|store|쇼핑|커머스|온라인몰/.test(useCaseLower)) {
    // E-COMMERCE: Page-based model (images heavy)
    // - Average page with images: 2-3 MB
    // - Pages per session: 15-25 (product browsing)
    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`;

  } else if (/forum|community|board|게시판|커뮤니티|포럼/.test(useCaseLower)) {
    // FORUM/COMMUNITY: Page-based model (text + some images)
    // - Average page: 0.5-1 MB
    // - Pages per session: 20-40 (thread reading)
    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`;

  } else if (/blog|static|portfolio|블로그|포트폴리오|landing/.test(useCaseLower)) {
    // STATIC/BLOG: Lightweight page-based model
    // - Average page: 1-2 MB (optimized images)
    // - Pages per session: 3-5 (bounce rate high)
    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`;

  } else if (/chat|messaging|slack|discord|채팅|메신저/.test(useCaseLower)) {
    // CHAT/MESSAGING: Message-based model
    // - Average message: 1-5 KB (text + small attachments)
    // - Messages per active user: 100-500 per day
    // - Some image/file sharing: adds 10-50 MB/user
    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)`;

  } else {
    // 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`;
  }

  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
 */
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.)
    // Max around 20TB for largest plans
    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
    // Generally less generous than Linode
    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
 */
function calculateBandwidthInfo(
  server: 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
  };
}

interface AIRecommendationResponse {
  recommendations: Array<{
    server_id: number;
    score: number;
    analysis: {
      tech_fit: string;
      capacity: string;
      cost_efficiency: string;
      scalability: string;
    };
    estimated_capacity: {
      max_daily_users?: number;
      max_concurrent_users: number;
      requests_per_second: number;
    };
  }>;
  infrastructure_tips?: string[];
}

/**
 * i18n Messages for multi-language support
 */
const i18n: Record<string, {
  missingFields: string;
  invalidFields: string;
  schema: Record<string, string>;
  example: Record<string, any>;
  aiLanguageInstruction: string;
}> = {
  en: {
    missingFields: 'Missing required fields',
    invalidFields: 'Invalid field values',
    schema: {
      tech_stack: "(required) string[] - e.g. ['nginx', 'nodejs']",
      expected_users: "(required) number - expected concurrent users, e.g. 1000",
      use_case: "(required) string - e.g. 'e-commerce website'",
      traffic_pattern: "(optional) 'steady' | 'spiky' | 'growing'",
      region_preference: "(optional) string[] - e.g. ['korea', 'japan']",
      budget_limit: "(optional) number - max monthly USD",
      provider_filter: "(optional) string[] - e.g. ['linode', 'vultr']",
      lang: "(optional) 'en' | 'zh' | 'ja' | 'ko' - response language"
    },
    example: {
      tech_stack: ["nginx", "nodejs", "postgresql"],
      expected_users: 5000,
      use_case: "SaaS application"
    },
    aiLanguageInstruction: 'Respond in English.'
  },
  zh: {
    missingFields: '缺少必填字段',
    invalidFields: '字段值无效',
    schema: {
      tech_stack: "(必填) string[] - 例如 ['nginx', 'nodejs']",
      expected_users: "(必填) number - 预计同时在线用户数，例如 1000",
      use_case: "(必填) string - 例如 '电商网站'",
      traffic_pattern: "(可选) 'steady' | 'spiky' | 'growing'",
      region_preference: "(可选) string[] - 例如 ['korea', 'japan']",
      budget_limit: "(可选) number - 每月最高预算(美元)",
      provider_filter: "(可选) string[] - 例如 ['linode', 'vultr']",
      lang: "(可选) 'en' | 'zh' | 'ja' | 'ko' - 响应语言"
    },
    example: {
      tech_stack: ["nginx", "nodejs", "postgresql"],
      expected_users: 5000,
      use_case: "SaaS应用程序"
    },
    aiLanguageInstruction: 'Respond in Chinese (Simplified). All analysis text must be in Chinese.'
  },
  ja: {
    missingFields: '必須フィールドがありません',
    invalidFields: 'フィールド値が無効です',
    schema: {
      tech_stack: "(必須) string[] - 例: ['nginx', 'nodejs']",
      expected_users: "(必須) number - 予想同時接続ユーザー数、例: 1000",
      use_case: "(必須) string - 例: 'ECサイト'",
      traffic_pattern: "(任意) 'steady' | 'spiky' | 'growing'",
      region_preference: "(任意) string[] - 例: ['korea', 'japan']",
      budget_limit: "(任意) number - 月額予算上限(USD)",
      provider_filter: "(任意) string[] - 例: ['linode', 'vultr']",
      lang: "(任意) 'en' | 'zh' | 'ja' | 'ko' - 応答言語"
    },
    example: {
      tech_stack: ["nginx", "nodejs", "postgresql"],
      expected_users: 5000,
      use_case: "SaaSアプリケーション"
    },
    aiLanguageInstruction: 'Respond in Japanese. All analysis text must be in Japanese.'
  },
  ko: {
    missingFields: '필수 필드가 누락되었습니다',
    invalidFields: '필드 값이 잘못되었습니다',
    schema: {
      tech_stack: "(필수) string[] - 예: ['nginx', 'nodejs']",
      expected_users: "(필수) number - 예상 동시 접속자 수, 예: 1000",
      use_case: "(필수) string - 예: '이커머스 웹사이트'",
      traffic_pattern: "(선택) 'steady' | 'spiky' | 'growing'",
      region_preference: "(선택) string[] - 예: ['korea', 'japan']",
      budget_limit: "(선택) number - 월 예산 한도(원화, KRW)",
      provider_filter: "(선택) string[] - 예: ['linode', 'vultr']",
      lang: "(선택) 'en' | 'zh' | 'ja' | 'ko' - 응답 언어"
    },
    example: {
      tech_stack: ["nginx", "nodejs", "postgresql"],
      expected_users: 5000,
      use_case: "SaaS 애플리케이션"
    },
    aiLanguageInstruction: 'Respond in Korean. All analysis text must be in Korean.'
  }
};

/**
 * Helper function to get allowed CORS origin
 */
function getAllowedOrigin(request: Request): string {
  const allowedOrigins = [
    'https://server-recommend.kappa-d8e.workers.dev',
  ];
  const origin = request.headers.get('Origin');
  if (origin && allowedOrigins.includes(origin)) {
    return origin;
  }
  // Allow requests without Origin header (non-browser, curl, etc.)
  if (!origin) {
    return '*';
  }
  return allowedOrigins[0];
}

/**
 * Rate limiting check using KV storage
 */
async function checkRateLimit(clientIP: string, env: Env): Promise<{ allowed: boolean; requestId: string }> {
  const requestId = crypto.randomUUID();

  // If CACHE is not configured, allow the request
  if (!env.CACHE) {
    return { allowed: true, requestId };
  }

  const now = Date.now();
  const maxRequests = 60;
  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 + 60000 }),
        { 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 };
  }
}

/**
 * Main request handler
 */
export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const requestId = crypto.randomUUID();

    try {
      const url = new URL(request.url);
      const path = url.pathname;

      // Rate limiting (except for health checks)
      if (path !== '/api/health') {
        const clientIP = request.headers.get('CF-Connecting-IP') || 'unknown';
        const rateCheck = await checkRateLimit(clientIP, env);

        if (!rateCheck.allowed) {
          const origin = getAllowedOrigin(request);
          return jsonResponse(
            { error: 'Too many requests', request_id: rateCheck.requestId },
            429,
            {
              'Access-Control-Allow-Origin': origin,
              'Vary': 'Origin',
            }
          );
        }
      }

      // CORS headers for all responses
      const origin = getAllowedOrigin(request);
      const corsHeaders = {
        'Access-Control-Allow-Origin': origin,
        'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
        'Access-Control-Allow-Headers': 'Content-Type',
        'Vary': 'Origin',
      };

      // Handle preflight requests
      if (request.method === 'OPTIONS') {
        return new Response(null, { headers: corsHeaders });
      }

      // Route handling
      if (path === '/api/health') {
        return handleHealth(corsHeaders);
      }

      if (path === '/api/servers' && request.method === 'GET') {
        return handleGetServers(request, env, corsHeaders);
      }

      if (path === '/api/recommend' && request.method === 'POST') {
        return handleRecommend(request, env, corsHeaders);
      }

      return jsonResponse(
        { error: 'Not found', request_id: requestId },
        404,
        corsHeaders
      );
    } catch (error) {
      console.error('[Worker] Unhandled error:', error);
      const origin = getAllowedOrigin(request);
      return jsonResponse(
        {
          error: 'Internal server error',
          request_id: requestId,
        },
        500,
        {
          'Access-Control-Allow-Origin': origin,
          'Vary': 'Origin',
        }
      );
    }
  },
};

/**
 * Health check endpoint
 */
function handleHealth(corsHeaders: Record<string, string>): Response {
  return jsonResponse(
    {
      status: 'ok',
      timestamp: new Date().toISOString(),
      service: 'server-recommend',
    },
    200,
    corsHeaders
  );
}

/**
 * GET /api/servers - Server list with filtering
 */
async function handleGetServers(
  request: Request,
  env: Env,
  corsHeaders: Record<string, string>
): Promise<Response> {
  try {
    const url = new URL(request.url);
    const provider = url.searchParams.get('provider');
    const minCpu = url.searchParams.get('minCpu');
    const minMemory = url.searchParams.get('minMemory');
    const region = url.searchParams.get('region');

    console.log('[GetServers] Query params:', {
      provider,
      minCpu,
      minMemory,
      region,
    });

    // Build SQL query dynamically
    let query = `
      SELECT
        it.id,
        p.display_name as provider_name,
        it.instance_id,
        it.instance_name,
        it.vcpu,
        it.memory_mb,
        ROUND(it.memory_mb / 1024.0, 1) as memory_gb,
        it.storage_gb,
        it.network_speed_gbps,
        it.instance_family,
        it.gpu_count,
        it.gpu_type,
        MIN(pr.monthly_price) as monthly_price,
        MIN(r.region_name) as region_name,
        MIN(r.region_code) as region_code
      FROM instance_types it
      JOIN providers p ON it.provider_id = p.id
      JOIN pricing pr ON pr.instance_type_id = it.id
      JOIN regions r ON pr.region_id = r.id
      WHERE p.id IN (1, 2)  -- Linode, Vultr only
        AND (
          -- 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%'
        )
    `;

    const params: (string | number)[] = [];

    if (provider) {
      query += ` AND p.name = ?`;
      params.push(provider);
    }

    if (minCpu) {
      const parsedCpu = parseInt(minCpu, 10);
      if (isNaN(parsedCpu)) {
        return jsonResponse({ error: 'Invalid minCpu parameter' }, 400, corsHeaders);
      }
      query += ` AND it.vcpu >= ?`;
      params.push(parsedCpu);
    }

    if (minMemory) {
      const parsedMemory = parseInt(minMemory, 10);
      if (isNaN(parsedMemory)) {
        return jsonResponse({ error: 'Invalid minMemory parameter' }, 400, corsHeaders);
      }
      query += ` AND it.memory_mb >= ?`;
      params.push(parsedMemory * 1024);
    }

    if (region) {
      query += ` AND r.region_code = ?`;
      params.push(region);
    }

    query += ` GROUP BY it.id ORDER BY MIN(pr.monthly_price) ASC LIMIT 100`;

    const result = await env.DB.prepare(query).bind(...params).all();

    if (!result.success) {
      throw new Error('Database query failed');
    }

    // Validate each result with type guard
    const servers = (result.results as unknown[]).filter(isValidServer);
    const invalidCount = result.results.length - servers.length;
    if (invalidCount > 0) {
      console.warn(`[GetServers] Filtered out ${invalidCount} invalid server records`);
    }

    console.log('[GetServers] Found servers:', servers.length);

    return jsonResponse(
      {
        servers,
        count: servers.length,
        filters: { provider, minCpu, minMemory, region },
      },
      200,
      corsHeaders
    );
  } catch (error) {
    console.error('[GetServers] Error:', error);
    const requestId = crypto.randomUUID();
    return jsonResponse(
      {
        error: 'Failed to retrieve servers',
        request_id: requestId,
      },
      500,
      corsHeaders
    );
  }
}

/**
 * POST /api/recommend - AI-powered server recommendation
 */
async function handleRecommend(
  request: Request,
  env: Env,
  corsHeaders: Record<string, string>
): Promise<Response> {
  const requestId = crypto.randomUUID();

  try {
    // Parse and validate request
    const body = await request.json() as RecommendRequest;
    const lang = body.lang || 'en';
    const validationError = validateRecommendRequest(body, lang);
    if (validationError) {
      return jsonResponse(validationError, 400, corsHeaders);
    }

    console.log('[Recommend] Request summary:', {
      tech_stack_count: body.tech_stack.length,
      expected_users: body.expected_users,
      use_case_length: body.use_case.length,
      traffic_pattern: body.traffic_pattern,
      has_region_pref: !!body.region_preference,
      has_budget: !!body.budget_limit,
      has_provider_filter: !!body.provider_filter,
      lang: lang,
    });

    // Generate cache key
    const cacheKey = generateCacheKey(body);
    console.log('[Recommend] Cache key:', cacheKey);

    // Check cache (optional - may not be configured)
    if (env.CACHE) {
      const cached = await env.CACHE.get(cacheKey);
      if (cached) {
        console.log('[Recommend] Cache hit');
        return jsonResponse(
          { ...JSON.parse(cached), cached: true },
          200,
          corsHeaders
        );
      }
    }

    console.log('[Recommend] Cache miss or disabled');

    // Phase 1: Execute independent queries in parallel
    const [techSpecs, benchmarkDataAll] = await Promise.all([
      queryTechSpecs(env.DB, body.tech_stack),
      queryBenchmarkData(env.DB, body.tech_stack).catch(err => {
        console.warn('[Recommend] Benchmark data unavailable:', err.message);
        return [] as BenchmarkData[];
      }),
    ]);
    console.log('[Recommend] Tech specs matched:', techSpecs.length);
    console.log('[Recommend] Benchmark data points (initial):', benchmarkDataAll.length);

    // Calculate minimum memory with proper aggregation
    // Memory-intensive services (Java, Elasticsearch, Redis): sum their memory requirements
    // Non-memory-intensive services: 256MB overhead each
    const memoryIntensiveSpecs = techSpecs.filter(s => s.is_memory_intensive);
    const otherSpecs = techSpecs.filter(s => !s.is_memory_intensive);

    let minMemoryMb: number | undefined;
    if (memoryIntensiveSpecs.length > 0 || otherSpecs.length > 0) {
      // Sum memory-intensive requirements
      const memoryIntensiveSum = memoryIntensiveSpecs.reduce((sum, s) => sum + s.min_memory_mb, 0);
      // Add 256MB overhead per non-memory-intensive service
      const otherOverhead = otherSpecs.length * 256;
      minMemoryMb = memoryIntensiveSum + otherOverhead;

      console.log(`[Recommend] Memory calculation: ${memoryIntensiveSpecs.length} memory-intensive (${(memoryIntensiveSum/1024).toFixed(1)}GB) + ${otherSpecs.length} other services (${(otherOverhead/1024).toFixed(1)}GB) = ${(minMemoryMb/1024).toFixed(1)}GB total`);
    }

    // Calculate minimum vCPU with category-based weighting
    // Different tech categories have different bottleneck characteristics
    let minVcpu: number | undefined;
    if (techSpecs.length > 0) {
      // Group specs by category
      const categoryWeights: Record<string, number> = {
        'web_server': 0.1,      // nginx, apache: reverse proxy uses minimal resources
        'runtime': 1.0,         // nodejs, php, python: actual computation
        'database': 1.0,        // mysql, postgresql, mongodb: major bottleneck
        'cache': 0.5,           // redis, memcached: supporting role
        'search': 0.8,          // elasticsearch: CPU-intensive but not always primary
        'container': 0.3,       // docker: orchestration overhead
        'messaging': 0.5,       // rabbitmq, kafka: I/O bound
        'default': 0.7          // unknown categories
      };

      // Calculate weighted vCPU requirements per category
      const categoryRequirements = new Map<string, number>();

      for (const spec of techSpecs) {
        const category = spec.category || 'default';
        const weight = categoryWeights[category] || categoryWeights['default'];
        const vcpuNeeded = Math.ceil(body.expected_users / spec.vcpu_per_users);
        const weightedVcpu = vcpuNeeded * weight;

        const existing = categoryRequirements.get(category) || 0;
        // Take max within same category (not additive)
        categoryRequirements.set(category, Math.max(existing, weightedVcpu));

        console.log(`[Recommend] ${spec.name} (${category}): ${vcpuNeeded} vCPU × ${weight} weight = ${weightedVcpu.toFixed(1)} weighted vCPU`);
      }

      // Find bottleneck: use MAX across categories, not SUM
      // Request flow (web_server → runtime → database) means the slowest component is the bottleneck
      // SUM would over-provision since components process the SAME requests sequentially
      let maxWeightedVcpu = 0;
      let bottleneckCategory = '';
      for (const [category, vcpu] of categoryRequirements) {
        console.log(`[Recommend] Category '${category}': ${vcpu.toFixed(1)} weighted vCPU`);
        if (vcpu > maxWeightedVcpu) {
          maxWeightedVcpu = vcpu;
          bottleneckCategory = category;
        }
      }

      minVcpu = Math.max(Math.ceil(maxWeightedVcpu), 1); // At least 1 vCPU
      console.log(`[Recommend] Bottleneck: '${bottleneckCategory}' with ${maxWeightedVcpu.toFixed(1)} weighted vCPU → ${minVcpu} vCPU (for ${body.expected_users} users)`);
    }

    // Calculate bandwidth estimate for provider filtering
    const bandwidthEstimate = estimateBandwidth(body.expected_users, body.use_case, body.traffic_pattern);
    console.log(`[Recommend] Bandwidth estimate: ${bandwidthEstimate.monthly_tb >= 1 ? bandwidthEstimate.monthly_tb + ' TB' : bandwidthEstimate.monthly_gb + ' GB'}/month (${bandwidthEstimate.category})`);

    // Estimate specs for VPS benchmark query (doesn't need exact candidates)
    const estimatedCores = minVcpu || 2;
    const estimatedMemory = minMemoryMb ? Math.ceil(minMemoryMb / 1024) : 4;
    const defaultProviders = bandwidthEstimate?.category === 'very_heavy' ? ['Linode'] : ['Linode', 'Vultr'];

    // Phase 2: Query candidate servers and VPS benchmarks in parallel
    const [candidates, vpsBenchmarks] = await Promise.all([
      queryCandidateServers(env.DB, body, minMemoryMb, minVcpu, bandwidthEstimate, lang),
      queryVPSBenchmarksBatch(env.DB, estimatedCores, estimatedMemory, defaultProviders).catch((err: unknown) => {
        const message = err instanceof Error ? err.message : String(err);
        console.warn('[Recommend] VPS benchmarks unavailable:', message);
        return [] as VPSBenchmark[];
      }),
    ]);

    console.log('[Recommend] Candidate servers:', candidates.length);
    console.log('[Recommend] VPS benchmark data points:', vpsBenchmarks.length);

    if (candidates.length === 0) {
      return jsonResponse(
        {
          error: 'No servers found matching your requirements',
          recommendations: [],
          request_id: requestId,
        },
        200,
        corsHeaders
      );
    }

    // Use initially fetched benchmark data (already filtered by tech stack)
    const benchmarkData = benchmarkDataAll;

    // Use OpenAI GPT-4o-mini to analyze and recommend (techSpecs already queried above)
    const aiResult = await getAIRecommendations(
      env,
      env.OPENAI_API_KEY,
      body,
      candidates,
      benchmarkData,
      vpsBenchmarks,
      techSpecs,
      bandwidthEstimate,
      lang
    );

    console.log('[Recommend] Generated recommendations:', aiResult.recommendations.length);

    const response = {
      recommendations: aiResult.recommendations,
      infrastructure_tips: aiResult.infrastructure_tips || [],
      bandwidth_estimate: {
        monthly_tb: bandwidthEstimate.monthly_tb,
        monthly_gb: bandwidthEstimate.monthly_gb,
        daily_gb: bandwidthEstimate.daily_gb,
        category: bandwidthEstimate.category,
        description: bandwidthEstimate.description,
        active_ratio: bandwidthEstimate.active_ratio,
        calculation_note: `Based on ${body.expected_users} concurrent users with ${Math.round(bandwidthEstimate.active_ratio * 100)}% active ratio`,
      },
      total_candidates: candidates.length,
      cached: false,
    };

    // Cache result for 1 hour (if cache is configured)
    if (env.CACHE) {
      await env.CACHE.put(cacheKey, JSON.stringify(response), {
        expirationTtl: 3600,
      });
    }

    return jsonResponse(response, 200, corsHeaders);
  } catch (error) {
    console.error('[Recommend] Error:', error);
    console.error('[Recommend] Error stack:', error instanceof Error ? error.stack : 'No stack');
    console.error('[Recommend] Error details:', error instanceof Error ? error.message : 'Unknown error');
    return jsonResponse(
      {
        error: 'Failed to generate recommendations',
        request_id: requestId,
      },
      500,
      corsHeaders
    );
  }
}

/**
 * Type guard to validate Server object structure
 */
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
 */
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
 */
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
 */
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
 */
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
 */
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 > 20) {
    invalidFields.push({ field: 'tech_stack', reason: 'must not exceed 20 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 > 500) {
    invalidFields.push({ field: 'use_case', reason: 'must not exceed 500 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 > 10) {
      invalidFields.push({ field: 'region_preference', reason: 'must not exceed 10 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;
}

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

/**
 * Query candidate servers from database
 * @param minMemoryMb - Minimum memory requirement from tech specs (optional)
 * @param minVcpu - Minimum vCPU requirement based on expected users (optional)
 * @param bandwidthEstimate - Bandwidth estimate for provider prioritization (optional)
 * @param lang - Language for currency selection: 'ko' → KRW, others → retail USD
 */
async function queryCandidateServers(
  db: D1Database,
  req: RecommendRequest,
  minMemoryMb?: number,
  minVcpu?: number,
  bandwidthEstimate?: BandwidthEstimate,
  lang: string = 'en'
): Promise<Server[]> {
  // Select price column based on language
  // Korean → monthly_price_krw (KRW), Others → monthly_price_retail (1.21x USD)
  const priceColumn = lang === 'ko' ? 'pr.monthly_price_krw' : 'pr.monthly_price_retail';
  const currency = lang === 'ko' ? 'KRW' : 'USD';

  // Check if region preference is specified
  const hasRegionPref = req.region_preference && req.region_preference.length > 0;

  let query = `
    SELECT
      it.id,
      p.display_name as provider_name,
      it.instance_id,
      it.instance_name,
      it.vcpu,
      it.memory_mb,
      ROUND(it.memory_mb / 1024.0, 1) as memory_gb,
      it.storage_gb,
      it.network_speed_gbps,
      it.instance_family,
      it.gpu_count,
      it.gpu_type,
      MIN(${priceColumn}) as monthly_price,
      r.region_name as region_name,
      r.region_code as region_code,
      r.country_code as country_code
    FROM instance_types it
    JOIN providers p ON it.provider_id = p.id
    JOIN pricing pr ON pr.instance_type_id = it.id
    JOIN regions r ON pr.region_id = r.id
    WHERE p.id IN (1, 2)  -- Linode, Vultr only
  `;

  const params: (string | number)[] = [];

  if (req.budget_limit) {
    // Use same price column as display for budget filtering
    query += ` AND ${priceColumn} <= ?`;
    params.push(req.budget_limit);
  }

  // Filter by minimum memory requirement (from tech specs)
  if (minMemoryMb && minMemoryMb > 0) {
    query += ` AND it.memory_mb >= ?`;
    params.push(minMemoryMb);
    console.log(`[Candidates] Filtering by minimum memory: ${minMemoryMb}MB (${(minMemoryMb/1024).toFixed(1)}GB)`);
  }

  // Filter by minimum vCPU requirement (from expected users + tech specs)
  if (minVcpu && minVcpu > 0) {
    query += ` AND it.vcpu >= ?`;
    params.push(minVcpu);
    console.log(`[Candidates] Filtering by minimum vCPU: ${minVcpu}`);
  }

  // Provider preference based on bandwidth requirements (no hard filtering to avoid empty results)
  // Heavy/Very heavy bandwidth → Prefer Linode (better bandwidth allowance), but allow all providers
  // AI prompt will warn about bandwidth costs for non-Linode providers
  if (bandwidthEstimate) {
    if (bandwidthEstimate.category === 'very_heavy') {
      // >6TB/month: Strongly prefer Linode, but don't exclude others (Linode may not be available in all regions)
      console.log(`[Candidates] Very heavy bandwidth (${bandwidthEstimate.monthly_tb}TB/month): Linode strongly preferred, all providers included`);
    } else if (bandwidthEstimate.category === 'heavy') {
      // 2-6TB/month: Prefer Linode
      console.log(`[Candidates] Heavy bandwidth (${bandwidthEstimate.monthly_tb}TB/month): Linode preferred`);
    }
  }

  // Country name to code mapping for common names
  // Note: Use specific city names to avoid LIKE pattern collisions (e.g., 'de' matches 'Delhi')
  const countryNameToCode: Record<string, string[]> = {
    'korea': ['seoul', 'ap-northeast-2'],
    'south korea': ['seoul', 'ap-northeast-2'],
    'japan': ['tokyo', 'osaka', 'ap-northeast-1', 'ap-northeast-3'],
    'singapore': ['singapore', 'ap-southeast-1'],
    'indonesia': ['jakarta', 'ap-southeast-3'],
    'india': ['mumbai', 'delhi', 'bangalore', 'hyderabad', 'ap-south-1'],
    'australia': ['sydney', 'melbourne', 'ap-southeast-2'],
    'germany': ['frankfurt', 'nuremberg', 'falkenstein', 'eu-central-1'],
    'usa': ['us-east', 'us-west', 'virginia', 'oregon', 'ohio'],
    'united states': ['us-east', 'us-west', 'virginia', 'oregon', 'ohio'],
    'uk': ['london', 'manchester', 'eu-west-2'],
    'united kingdom': ['london', 'manchester', 'eu-west-2'],
    'netherlands': ['amsterdam', 'eu-west-1'],
    'france': ['paris', 'eu-west-3'],
    'hong kong': ['hong kong', 'ap-east-1'],
    'taiwan': ['taipei', 'ap-northeast-1'],
    'brazil': ['sao paulo', 'sa-east-1'],
    'canada': ['montreal', 'toronto', 'ca-central-1'],
  };

  // Flexible region matching: region_code, region_name, or country_code
  if (req.region_preference && req.region_preference.length > 0) {
    // User specified region → filter to that region only
    const regionConditions: string[] = [];
    for (const region of req.region_preference) {
      const lowerRegion = region.toLowerCase();

      // Expand country names to their codes/cities
      const expandedRegions = countryNameToCode[lowerRegion] || [lowerRegion];
      const allRegions = [lowerRegion, ...expandedRegions];

      for (const r of allRegions) {
        const escapedRegion = escapeLikePattern(r);
        regionConditions.push(`(
          LOWER(r.region_code) = ? OR
          LOWER(r.region_code) LIKE ? ESCAPE '\\' OR
          LOWER(r.region_name) LIKE ? ESCAPE '\\' OR
          LOWER(r.country_code) = ?
        )`);
        params.push(r, `%${escapedRegion}%`, `%${escapedRegion}%`, r);
      }
    }
    query += ` AND (${regionConditions.join(' OR ')})`;
  } else {
    // No region specified → default to Seoul/Tokyo/Osaka/Singapore
    query += ` AND (
      -- 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%'
    )`;
  }

  // Filter by provider if specified
  if (req.provider_filter && req.provider_filter.length > 0) {
    const placeholders = req.provider_filter.map(() => '?').join(',');
    query += ` AND (p.name IN (${placeholders}) OR p.display_name IN (${placeholders}))`;
    params.push(...req.provider_filter, ...req.provider_filter);
  }

  // Group by instance + region to show each server per region
  // For heavy/very_heavy bandwidth, prioritize Linode (p.id=1) due to generous bandwidth allowance
  const isHighBandwidth = bandwidthEstimate?.category === 'heavy' || bandwidthEstimate?.category === 'very_heavy';
  const orderByClause = isHighBandwidth
    ? `ORDER BY CASE WHEN p.id = 1 THEN 0 ELSE 1 END, monthly_price ASC`
    : `ORDER BY monthly_price ASC`;
  query += ` GROUP BY it.id, r.id ${orderByClause} LIMIT 50`;

  const result = await db.prepare(query).bind(...params).all();

  if (!result.success) {
    throw new Error('Failed to query candidate servers');
  }

  // Add currency to each result and validate with type guard
  const serversWithCurrency = (result.results as unknown[]).map(server => {
    if (typeof server === 'object' && server !== null) {
      return { ...server, currency };
    }
    return server;
  });
  const validServers = serversWithCurrency.filter(isValidServer);
  const invalidCount = result.results.length - validServers.length;
  if (invalidCount > 0) {
    console.warn(`[Candidates] Filtered out ${invalidCount} invalid server records`);
  }
  return validServers;
}

/**
 * Query relevant benchmark data for tech stack
 */
async function queryBenchmarkData(
  db: D1Database,
  techStack: string[],
  coreCount?: number
): Promise<BenchmarkData[]> {
  // Map tech stack to relevant benchmark types
  const techToBenchmark: Record<string, string[]> = {
    'node.js': ['pts-node-octane', 'pts-node-express-loadtest'],
    'nodejs': ['pts-node-octane', 'pts-node-express-loadtest'],
    'express': ['pts-node-express-loadtest'],
    'nginx': ['pts-nginx'],
    'apache': ['pts-apache'],
    'php': ['pts-phpbench'],
    'redis': ['pts-redis'],
    'mysql': ['pts-mysqlslap'],
    'postgresql': ['pts-mysqlslap'], // Use MySQL benchmark as proxy
    'docker': ['pts-compress-7zip', 'pts-postmark'], // CPU + I/O for containers
    'mongodb': ['pts-postmark'], // I/O intensive
    'python': ['pts-coremark', 'pts-compress-7zip'],
    'java': ['pts-coremark', 'pts-compress-7zip'],
    'go': ['pts-coremark', 'pts-compress-7zip'],
    'rust': ['pts-coremark', 'pts-compress-7zip'],
  };

  // Find relevant benchmark types
  const relevantBenchmarks = new Set<string>();
  for (const tech of techStack) {
    const benchmarks = techToBenchmark[tech.toLowerCase()] || [];
    benchmarks.forEach(b => relevantBenchmarks.add(b));
  }

  // Always include general CPU benchmark
  relevantBenchmarks.add('pts-compress-7zip');

  if (relevantBenchmarks.size === 0) {
    return [];
  }

  const benchmarkNames = Array.from(relevantBenchmarks);
  const placeholders = benchmarkNames.map(() => '?').join(',');

  // Query benchmark data, optionally filtering by core count
  let query = `
    SELECT
      p.id,
      p.name as processor_name,
      bt.name as benchmark_name,
      bt.category,
      br.score,
      br.percentile,
      p.cores
    FROM benchmark_results br
    JOIN processors p ON br.processor_id = p.id
    JOIN benchmark_types bt ON br.benchmark_type_id = bt.id
    WHERE bt.name IN (${placeholders})
  `;

  const params: (string | number)[] = [...benchmarkNames];

  // If we know core count, filter to similar processors
  if (coreCount && coreCount > 0) {
    query += ` AND (p.cores IS NULL OR (p.cores >= ? AND p.cores <= ?))`;
    params.push(Math.max(1, coreCount - 2), coreCount + 4);
  }

  query += ` ORDER BY br.percentile DESC, br.score DESC LIMIT 50`;

  const result = await db.prepare(query).bind(...params).all();

  if (!result.success) {
    console.warn('[Benchmark] Query failed');
    return [];
  }

  // Validate each result with type guard
  return (result.results as unknown[]).filter(isValidBenchmarkData);
}

/**
 * Get benchmark reference for a server
 */
function getBenchmarkReference(
  benchmarks: BenchmarkData[],
  vcpu: number
): BenchmarkReference | undefined {
  // Find benchmarks from processors with similar core count
  const similarBenchmarks = benchmarks.filter(b =>
    b.cores === null || (b.cores >= vcpu - 2 && b.cores <= vcpu + 4)
  );

  if (similarBenchmarks.length === 0) {
    return undefined;
  }

  // Group by processor and get the best match
  const byProcessor = new Map<string, BenchmarkData[]>();
  for (const b of similarBenchmarks) {
    const existing = byProcessor.get(b.processor_name) || [];
    existing.push(b);
    byProcessor.set(b.processor_name, existing);
  }

  // Find processor with most benchmark data
  let bestProcessor = '';
  let maxBenchmarks = 0;
  for (const [name, data] of byProcessor) {
    if (data.length > maxBenchmarks) {
      maxBenchmarks = data.length;
      bestProcessor = name;
    }
  }

  if (!bestProcessor) {
    return undefined;
  }

  const processorBenchmarks = byProcessor.get(bestProcessor)!;
  return {
    processor_name: bestProcessor,
    benchmarks: processorBenchmarks.map(b => ({
      name: b.benchmark_name,
      category: b.category,
      score: b.score,
      percentile: b.percentile,
    })),
  };
}

/**
 * Query VPS benchmarks - prioritize matching provider
 */
async function queryVPSBenchmarks(
  db: D1Database,
  vcpu: number,
  memoryGb: number,
  providerHint?: string
): Promise<VPSBenchmark[]> {
  const vcpuMin = Math.max(1, vcpu - 1);
  const vcpuMax = vcpu + 2;
  const memMin = Math.max(1, memoryGb - 2);
  const memMax = memoryGb + 4;

  // First try to find benchmarks from the same provider
  if (providerHint) {
    const providerQuery = `
      SELECT *
      FROM vps_benchmarks
      WHERE (LOWER(provider_name) LIKE ? ESCAPE '\\' OR LOWER(plan_name) LIKE ? ESCAPE '\\')
      ORDER BY gb6_single_normalized DESC
      LIMIT 20
    `;
    const escapedHint = escapeLikePattern(providerHint.toLowerCase());
    const providerPattern = `%${escapedHint}%`;
    const providerResult = await db.prepare(providerQuery).bind(providerPattern, providerPattern).all();

    if (providerResult.success && providerResult.results.length > 0) {
      // Validate each result with type guard
      return (providerResult.results as unknown[]).filter(isValidVPSBenchmark);
    }
  }

  // Fallback: Find VPS with similar specs
  const query = `
    SELECT *
    FROM vps_benchmarks
    WHERE vcpu >= ? AND vcpu <= ?
      AND memory_gb >= ? AND memory_gb <= ?
    ORDER BY gb6_single_normalized DESC
    LIMIT 10
  `;

  const result = await db.prepare(query).bind(vcpuMin, vcpuMax, memMin, memMax).all();

  if (!result.success) {
    return [];
  }

  // Validate each result with type guard
  return (result.results as unknown[]).filter(isValidVPSBenchmark);
}

/**
 * Query VPS benchmarks in a single batched query
 * Consolidates multiple provider-specific queries into one for better performance
 */
async function queryVPSBenchmarksBatch(
  db: D1Database,
  vcpu: number,
  memoryGb: number,
  providers: string[]
): Promise<VPSBenchmark[]> {
  const vcpuMin = Math.max(1, vcpu - 1);
  const vcpuMax = vcpu + 2;
  const memMin = Math.max(1, memoryGb - 2);
  const memMax = memoryGb + 4;

  // Build provider conditions for up to 3 providers
  const providerConditions: string[] = [];
  const params: (string | number)[] = [];

  const limitedProviders = providers.slice(0, 3);
  for (const provider of limitedProviders) {
    const pattern = `%${escapeLikePattern(provider.toLowerCase())}%`;
    providerConditions.push(`(LOWER(provider_name) LIKE ? ESCAPE '\\' OR LOWER(plan_name) LIKE ? ESCAPE '\\')`);
    params.push(pattern, pattern);
  }

  // Build query with provider matching OR spec matching
  const query = `
    SELECT * FROM vps_benchmarks
    WHERE ${providerConditions.length > 0 ? `(${providerConditions.join(' OR ')})` : '1=0'}
       OR (vcpu >= ? AND vcpu <= ? AND memory_gb >= ? AND memory_gb <= ?)
    ORDER BY gb6_single_normalized DESC
    LIMIT 30
  `;

  params.push(vcpuMin, vcpuMax, memMin, memMax);

  const result = await db.prepare(query).bind(...params).all();

  if (!result.success) {
    console.warn('[VPSBenchmarksBatch] Query failed');
    return [];
  }

  // Validate each result with type guard
  return (result.results as unknown[]).filter(isValidVPSBenchmark);
}

/**
 * Format VPS benchmark data for AI prompt
 * Uses GB6-normalized scores (GB5 scores converted with ×1.45 factor)
 */
function formatVPSBenchmarkSummary(benchmarks: VPSBenchmark[]): string {
  if (benchmarks.length === 0) {
    return '';
  }

  const lines = ['Real VPS performance data (Geekbench 6 normalized):'];
  for (const b of benchmarks.slice(0, 5)) {
    const versionNote = b.geekbench_version?.startsWith('5.') ? ' [GB5→6]' : '';
    lines.push(
      `- ${b.plan_name} (${b.country_code}): Single=${b.gb6_single_normalized}, Multi=${b.gb6_multi_normalized}${versionNote}, $${b.monthly_price_usd}/mo, Perf/$=${b.performance_per_dollar.toFixed(1)}`
    );
  }

  return lines.join('\n');
}

/**
 * Format benchmark data for AI prompt
 */
function formatBenchmarkSummary(benchmarks: BenchmarkData[]): string {
  if (benchmarks.length === 0) {
    return '';
  }

  // Group by benchmark type
  const byType = new Map<string, BenchmarkData[]>();
  for (const b of benchmarks) {
    const existing = byType.get(b.benchmark_name) || [];
    existing.push(b);
    byType.set(b.benchmark_name, existing);
  }

  const lines: string[] = [];
  for (const [type, data] of byType) {
    // Get top 3 performers for this benchmark
    const top3 = data.slice(0, 3);
    const scores = top3.map(d =>
      `${d.processor_name}${d.cores ? ` (${d.cores} cores)` : ''}: ${d.score} (${d.percentile}th percentile)`
    );
    lines.push(`### ${type} (${data[0].category})`);
    lines.push(scores.join('\n'));
    lines.push('');
  }

  return lines.join('\n');
}

/**
 * Query tech stack specifications from database
 * Matches user's tech_stack against canonical names and aliases
 */
async function queryTechSpecs(
  db: D1Database,
  techStack: string[]
): Promise<TechSpec[]> {
  if (!techStack || techStack.length === 0) {
    return [];
  }

  // Normalize user input
  const normalizedStack = techStack.map(t => t.toLowerCase().trim());

  // Build query that matches both name and aliases (case-insensitive)
  // Using LOWER() for alias matching since aliases are stored as JSON array strings
  const conditions: string[] = [];
  const params: string[] = [];

  for (const tech of normalizedStack) {
    conditions.push(`(LOWER(name) = ? OR LOWER(aliases) LIKE ?)`);
    params.push(tech, `%"${tech}"%`);
  }

  const query = `
    SELECT
      id, name, category,
      vcpu_per_users, vcpu_per_users_max,
      min_memory_mb, max_memory_mb,
      description, aliases,
      is_memory_intensive, is_cpu_intensive
    FROM tech_specs
    WHERE ${conditions.join(' OR ')}
    ORDER BY category, name
  `;

  try {
    const result = await db.prepare(query).bind(...params).all();

    if (!result.success) {
      console.warn('[TechSpecs] Query failed');
      return [];
    }

    // Validate each result with type guard
    const validSpecs = (result.results as unknown[]).filter(isValidTechSpec);
    console.log(`[TechSpecs] Found ${validSpecs.length} specs for: ${normalizedStack.join(', ')}`);
    return validSpecs;
  } catch (error) {
    console.error('[TechSpecs] Error:', error);
    return [];
  }
}

/**
 * Format tech specs for AI prompt
 */
function formatTechSpecsForPrompt(techSpecs: TechSpec[]): string {
  if (!techSpecs || techSpecs.length === 0) {
    return `Tech stack resource guidelines:
- Default: 1 vCPU per 100-300 users, 1-2GB RAM`;
  }

  const lines = ['Tech stack resource guidelines (MUST follow minimum RAM requirements):'];

  for (const spec of techSpecs) {
    const vcpuRange = spec.vcpu_per_users_max
      ? `${spec.vcpu_per_users}-${spec.vcpu_per_users_max}`
      : `${spec.vcpu_per_users}`;

    // Convert MB to GB for readability
    const minMemoryGB = (spec.min_memory_mb / 1024).toFixed(1).replace('.0', '');
    const maxMemoryGB = spec.max_memory_mb ? (spec.max_memory_mb / 1024).toFixed(1).replace('.0', '') : null;
    const memoryRange = maxMemoryGB ? `${minMemoryGB}-${maxMemoryGB}GB` : `${minMemoryGB}GB+`;

    let line = `- ${spec.name}: 1 vCPU per ${vcpuRange} users, MINIMUM ${minMemoryGB}GB RAM`;

    // Add warnings for special requirements
    const warnings: string[] = [];
    if (spec.is_memory_intensive) warnings.push('⚠️ MEMORY-INTENSIVE: must have at least ' + minMemoryGB + 'GB RAM');
    if (spec.is_cpu_intensive) warnings.push('⚠️ CPU-INTENSIVE');
    if (warnings.length > 0) {
      line += ` [${warnings.join(', ')}]`;
    }

    lines.push(line);
  }

  // Add explicit warning for memory-intensive apps
  const memoryIntensive = techSpecs.filter(s => s.is_memory_intensive);
  if (memoryIntensive.length > 0) {
    const maxMinMemory = Math.max(...memoryIntensive.map(s => s.min_memory_mb));
    lines.push('');
    lines.push(`⚠️ CRITICAL: This tech stack includes memory-intensive apps. Servers with less than ${(maxMinMemory / 1024).toFixed(0)}GB RAM will NOT work properly!`);
  }

  return lines.join('\n');
}

/**
 * Get AI-powered recommendations using OpenAI GPT-4o-mini
 */
async function getAIRecommendations(
  env: Env,
  apiKey: string,
  req: RecommendRequest,
  candidates: Server[],
  benchmarkData: BenchmarkData[],
  vpsBenchmarks: VPSBenchmark[],
  techSpecs: TechSpec[],
  bandwidthEstimate: BandwidthEstimate,
  lang: string = 'en'
): Promise<{ recommendations: RecommendationResult[]; infrastructure_tips?: string[] }> {
  // Validate API key before making any API calls
  if (!apiKey || !apiKey.trim()) {
    console.error('[AI] OPENAI_API_KEY is not configured or empty');
    throw new Error('OPENAI_API_KEY not configured. Please set the secret via: wrangler secret put OPENAI_API_KEY');
  }
  if (!apiKey.startsWith('sk-')) {
    console.error('[AI] OPENAI_API_KEY has invalid format (should start with sk-)');
    throw new Error('Invalid OPENAI_API_KEY format');
  }
  console.log('[AI] API key validated (format: sk-***)');

  // Build dynamic tech specs prompt from database
  const techSpecsPrompt = formatTechSpecsForPrompt(techSpecs);

  // Ensure lang is valid
  const validLang = ['en', 'zh', 'ja', 'ko'].includes(lang) ? lang : 'en';
  const languageInstruction = i18n[validLang].aiLanguageInstruction;

  // Build system prompt with benchmark awareness
  const systemPrompt = `You are a cloud infrastructure expert focused on COST-EFFECTIVE solutions. Your goal is to recommend the SMALLEST and CHEAPEST server that can handle the user's requirements.

CRITICAL RULES:
1. NEVER over-provision. Recommend the minimum specs needed.
2. Cost efficiency is the PRIMARY factor - cheaper is better if it meets requirements.
3. A 1-2 vCPU server can handle 100-500 concurrent users for most web workloads.
4. Nginx/reverse proxy needs very little resources - 1 vCPU can handle 1000+ req/sec.
5. Provide 3 options: Budget (cheapest viable), Balanced (some headroom), Premium (growth ready).

BANDWIDTH CONSIDERATIONS (VERY IMPORTANT):
- Estimated monthly bandwidth is provided based on concurrent users and use case.
- TOTAL COST = Base server price + Bandwidth overage charges
- Provider bandwidth allowances:
  * Linode: 1TB (1GB plan) to 20TB (192GB plan) included free, $0.005/GB overage
  * Vultr: 1TB-10TB depending on plan, $0.01/GB overage (2x Linode rate)
  * DigitalOcean: 1TB-12TB depending on plan, $0.01/GB overage
- For bandwidth >1TB/month: Linode is often cheaper despite higher base price
- For bandwidth >3TB/month: Linode is STRONGLY preferred (overage savings significant)
- Always mention bandwidth implications in cost_efficiency analysis

${techSpecsPrompt}

Use REAL BENCHMARK DATA to validate capacity estimates.

${languageInstruction}`;

  // Build user prompt with requirements and candidates
  console.log('[AI] Bandwidth estimate:', bandwidthEstimate);

  // Detect high-traffic based on bandwidth estimate (more accurate than keyword matching)
  const isHighTraffic = bandwidthEstimate.category === 'heavy' || bandwidthEstimate.category === 'very_heavy';

  // Format benchmark data for the prompt
  const benchmarkSummary = formatBenchmarkSummary(benchmarkData);
  const vpsBenchmarkSummary = formatVPSBenchmarkSummary(vpsBenchmarks);

  const userPrompt = `Analyze these server options and recommend the top 3 best matches.

## User Requirements
- Tech Stack: ${req.tech_stack.join(', ')}
- Expected Concurrent Users: ${req.expected_users} ${req.traffic_pattern === 'spiky' ? '(with traffic spikes)' : req.traffic_pattern === 'growing' ? '(growing user base)' : '(steady traffic)'}
- **Estimated DAU (Daily Active Users)**: ${bandwidthEstimate.estimated_dau_min.toLocaleString()}-${bandwidthEstimate.estimated_dau_max.toLocaleString()}명 (동시 접속 ${req.expected_users}명 기준)
- Use Case: ${req.use_case}
- Traffic Pattern: ${req.traffic_pattern || 'steady'}
- **Estimated Monthly Bandwidth**: ${bandwidthEstimate.monthly_tb >= 1 ? `${bandwidthEstimate.monthly_tb} TB` : `${bandwidthEstimate.monthly_gb} GB`} (${bandwidthEstimate.category})
${isHighTraffic ? `- ⚠️ HIGH BANDWIDTH WORKLOAD (${bandwidthEstimate.monthly_tb} TB/month): MUST recommend Linode over Vultr. Linode includes 1-6TB/month transfer vs Vultr overage charges ($0.01/GB). Bandwidth cost savings > base price difference.` : ''}
${req.region_preference ? `- Region Preference: ${req.region_preference.join(', ')}` : ''}
${req.budget_limit ? `- Budget Limit: $${req.budget_limit}/month` : ''}

## Real VPS Benchmark Data (Geekbench 6 normalized - actual VPS tests)
${vpsBenchmarkSummary || 'No similar VPS benchmark data available.'}

## CPU Benchmark Reference (from Phoronix Test Suite)
${benchmarkSummary || 'No relevant CPU benchmark data available.'}

## Available Servers
${candidates.map((s, idx) => `
${idx + 1}. ${s.provider_name} - ${s.instance_name}${s.instance_family ? ` (${s.instance_family})` : ''}
   ID: ${s.id}
   Instance: ${s.instance_id}
   vCPU: ${s.vcpu} | Memory: ${s.memory_gb} GB | Storage: ${s.storage_gb} GB
   Network: ${s.network_speed_gbps ? `${s.network_speed_gbps} Gbps` : 'N/A'}${s.gpu_count > 0 ? ` | GPU: ${s.gpu_count}x ${s.gpu_type || 'Unknown'}` : ' | GPU: None'}
   Price: ${s.currency === 'KRW' ? '₩' : '$'}${s.currency === 'KRW' ? Math.round(s.monthly_price).toLocaleString() : s.monthly_price.toFixed(2)}/month (${s.currency}) | Region: ${s.region_name} (${s.region_code})
`).join('\n')}

Return ONLY a valid JSON object (no markdown, no code blocks) with this exact structure:
{
  "recommendations": [
    {
      "server_id": 123,
      "score": 95,
      "analysis": {
        "tech_fit": "Why this server fits the tech stack",
        "capacity": "MUST mention: '동시 접속 X명 요청 (DAU A-B명), 최대 동시 Y명까지 처리 가능' format",
        "cost_efficiency": "MUST include: base price + bandwidth cost estimate. Example: '$5/month + ~$X bandwidth = ~$Y total'",
        "scalability": "Scalability potential including bandwidth headroom"
      },
      "estimated_capacity": {
        "max_concurrent_users": 7500,
        "requests_per_second": 1000
      }
    }
  ],
  "infrastructure_tips": [
    "Practical tip 1",
    "Practical tip 2"
  ]
}

Provide exactly 3 recommendations:
1. BUDGET option: Cheapest TOTAL cost (base + bandwidth) that can handle the load (highest score if viable)
2. BALANCED option: Some headroom for traffic spikes
3. PREMIUM option: Ready for 2-3x growth

SCORING (100 points total):
- Total Cost Efficiency (40%): Base price + estimated bandwidth overage. Lower total = higher score.
- Capacity Fit (30%): Can it handle the concurrent users and bandwidth?
- Scalability (30%): Room for growth in CPU, memory, AND bandwidth allowance.

The option with the LOWEST TOTAL MONTHLY COST (including bandwidth) should have the HIGHEST score.`;

  // Use AI Gateway if configured (bypasses regional restrictions like HKG)
  // AI Gateway URL format: https://gateway.ai.cloudflare.com/v1/{account_id}/{gateway_name}/openai
  const useAIGateway = !!env.AI_GATEWAY_URL;
  const apiEndpoint = useAIGateway
    ? `${env.AI_GATEWAY_URL}/chat/completions`
    : 'https://api.openai.com/v1/chat/completions';

  console.log(`[AI] Sending request to ${useAIGateway ? 'AI Gateway → ' : ''}OpenAI GPT-4o-mini`);
  if (useAIGateway) {
    console.log('[AI] Using Cloudflare AI Gateway to bypass regional restrictions');
  }

  // Create AbortController with 30 second timeout
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), 30000);

  try {
    const openaiResponse = await fetch(apiEndpoint, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${apiKey}`,
      },
      body: JSON.stringify({
        model: 'gpt-4o-mini',
        messages: [
          { role: 'system', content: systemPrompt },
          { role: 'user', content: userPrompt },
        ],
        max_tokens: 2000,
        temperature: 0.3,
      }),
      signal: controller.signal,
    });

    clearTimeout(timeoutId);

    if (!openaiResponse.ok) {
      const errorText = await openaiResponse.text();

      // Parse error details for better debugging
      let errorDetails = '';
      try {
        const errorObj = JSON.parse(errorText);
        errorDetails = errorObj?.error?.message || errorObj?.error?.type || '';
      } catch {
        errorDetails = errorText.slice(0, 200);
      }

      // Sanitize API keys from error messages
      const sanitized = errorDetails.replace(/sk-[a-zA-Z0-9-_]+/g, 'sk-***');

      // Enhanced logging for specific error codes
      if (openaiResponse.status === 403) {
        const isRegionalBlock = errorDetails.includes('Country') || errorDetails.includes('region') || errorDetails.includes('territory');
        if (isRegionalBlock && !useAIGateway) {
          console.error('[AI] ❌ REGIONAL BLOCK (403) - OpenAI blocked this region');
          console.error('[AI]   Worker is running in a blocked region (e.g., HKG)');
          console.error('[AI]   FIX: Set AI_GATEWAY_URL secret to use Cloudflare AI Gateway');
          console.error('[AI]   1. Create AI Gateway: https://dash.cloudflare.com → AI → AI Gateway');
          console.error('[AI]   2. Run: wrangler secret put AI_GATEWAY_URL');
          console.error('[AI]   3. Enter: https://gateway.ai.cloudflare.com/v1/{account_id}/{gateway_name}/openai');
        } else {
          console.error('[AI] ❌ AUTH FAILED (403) - Possible causes:');
          console.error('[AI]   1. Invalid or expired OPENAI_API_KEY');
          console.error('[AI]   2. API key not properly set in Cloudflare secrets');
          console.error('[AI]   3. Account billing issue or quota exceeded');
        }
        console.error('[AI] Error details:', sanitized);
      } else if (openaiResponse.status === 429) {
        console.error('[AI] ⚠️ RATE LIMITED (429) - Too many requests');
        console.error('[AI] Error details:', sanitized);
      } else if (openaiResponse.status === 401) {
        console.error('[AI] ❌ UNAUTHORIZED (401) - API key invalid');
        console.error('[AI] Error details:', sanitized);
      } else {
        console.error('[AI] OpenAI API error:', openaiResponse.status, sanitized);
      }

      throw new Error(`OpenAI API error: ${openaiResponse.status}`);
    }

    const openaiResult = await openaiResponse.json() as {
      choices: Array<{ message: { content: string } }>;
    };

    const response = openaiResult.choices[0]?.message?.content || '';

    console.log('[AI] Response received from OpenAI, length:', response.length);
    console.log('[AI] Raw response preview:', response.substring(0, 500));

    // Parse AI response
    const aiResult = parseAIResponse(response);
    console.log('[AI] Parsed recommendations count:', aiResult.recommendations.length);

    // Pre-index VPS benchmarks by provider for O(1) lookups
    const vpsByProvider = new Map<string, VPSBenchmark[]>();
    for (const vps of vpsBenchmarks) {
      const providerKey = vps.provider_name.toLowerCase();
      const existing = vpsByProvider.get(providerKey) || [];
      existing.push(vps);
      vpsByProvider.set(providerKey, existing);
    }

    // Map AI recommendations to full results
    const results: RecommendationResult[] = [];
    for (const aiRec of aiResult.recommendations) {
      // Handle both string and number server_id from AI
      const serverId = Number(aiRec.server_id);
      const server = candidates.find((s) => s.id === serverId);
      if (!server) {
        console.warn('[AI] Server not found:', aiRec.server_id);
        continue;
      }

      // Get benchmark reference for this server's CPU count
      const benchmarkRef = getBenchmarkReference(benchmarkData, server.vcpu);

      // Find matching VPS benchmark using pre-indexed data
      const providerName = server.provider_name.toLowerCase();
      let matchingVPS: VPSBenchmark | undefined;

      // Try to find from indexed provider benchmarks
      for (const [providerKey, benchmarks] of vpsByProvider.entries()) {
        if (providerKey.includes(providerName) || providerName.includes(providerKey)) {
          // First try exact or close vCPU match
          matchingVPS = benchmarks.find(
            (v) => v.vcpu === server.vcpu || (v.vcpu >= server.vcpu - 1 && v.vcpu <= server.vcpu + 1)
          );
          // Fallback to any from this provider
          if (!matchingVPS && benchmarks.length > 0) {
            matchingVPS = benchmarks[0];
          }
          if (matchingVPS) break;
        }
      }

      // Final fallback: similar specs from any provider
      if (!matchingVPS) {
        matchingVPS = vpsBenchmarks.find(
          (v) => v.vcpu === server.vcpu || (v.vcpu >= server.vcpu - 1 && v.vcpu <= server.vcpu + 1)
        );
      }

      // Calculate bandwidth info for this server
      const bandwidthInfo = calculateBandwidthInfo(server, bandwidthEstimate);

      results.push({
        server: server,
        score: aiRec.score,
        analysis: aiRec.analysis,
        estimated_capacity: aiRec.estimated_capacity,
        bandwidth_info: bandwidthInfo,
        benchmark_reference: benchmarkRef,
        vps_benchmark_reference: matchingVPS
          ? {
              plan_name: matchingVPS.plan_name,
              geekbench_single: matchingVPS.geekbench_single,
              geekbench_multi: matchingVPS.geekbench_multi,
              monthly_price_usd: matchingVPS.monthly_price_usd,
              performance_per_dollar: matchingVPS.performance_per_dollar,
            }
          : undefined,
      });

      if (results.length >= 3) break;
    }

    return {
      recommendations: results,
      infrastructure_tips: aiResult.infrastructure_tips,
    };
  } catch (error) {
    clearTimeout(timeoutId);
    // Handle timeout specifically
    if (error instanceof Error && error.name === 'AbortError') {
      console.error('[AI] Request timed out after 30 seconds');
      throw new Error('AI request timed out - please try again');
    }
    console.error('[AI] Error:', error);
    throw new Error(`AI processing failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
  }
}

/**
 * Parse AI response and extract JSON
 */
function parseAIResponse(response: any): AIRecommendationResponse {
  try {
    // Handle different response formats
    let content: string;

    if (typeof response === 'string') {
      content = response;
    } else if (response.response) {
      content = response.response;
    } else if (response.result && response.result.response) {
      content = response.result.response;
    } else if (response.choices && response.choices[0]?.message?.content) {
      content = response.choices[0].message.content;
    } else {
      console.error('[AI] Unexpected response format:', response);
      throw new Error('Unexpected AI response format');
    }

    // Remove markdown code blocks if present
    content = content.replace(/```json\s*/g, '').replace(/```\s*/g, '').trim();

    // Find JSON object in response
    const jsonMatch = content.match(/\{[\s\S]*\}/);
    if (!jsonMatch) {
      throw new Error('No JSON found in AI response');
    }

    const parsed = JSON.parse(jsonMatch[0]);

    if (!parsed.recommendations || !Array.isArray(parsed.recommendations)) {
      throw new Error('Invalid recommendations structure');
    }

    // Validate each recommendation with type guard
    const validRecommendations = parsed.recommendations.filter(isValidAIRecommendation);
    if (validRecommendations.length === 0 && parsed.recommendations.length > 0) {
      console.warn('[AI] All recommendations failed validation, raw:', JSON.stringify(parsed.recommendations[0]).slice(0, 200));
      throw new Error('AI recommendations failed validation');
    }

    return {
      recommendations: validRecommendations,
      infrastructure_tips: Array.isArray(parsed.infrastructure_tips) ? parsed.infrastructure_tips : [],
    } as AIRecommendationResponse;
  } catch (error) {
    console.error('[AI] Parse error:', error);
    console.error('[AI] Response was:', response);
    throw new Error(`Failed to parse AI response: ${error instanceof Error ? error.message : 'Unknown error'}`);
  }
}

/**
 * Sanitize special characters for cache key
 */
function sanitizeCacheValue(value: string): string {
  return value.replace(/[|:,]/g, '_');
}

/**
 * Generate cache key from request parameters
 */
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('|')}`;
}

/**
 * Simple hash function for strings
 */
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
  }
  return Math.abs(hash).toString(36);
}

/**
 * JSON response helper
 */
function jsonResponse(
  data: any,
  status: number,
  headers: Record<string, string> = {}
): Response {
  return new Response(JSON.stringify(data), {
    status,
    headers: {
      'Content-Type': 'application/json',
      'X-Content-Type-Options': 'nosniff',
      'X-Frame-Options': 'DENY',
      'Cache-Control': 'no-store',
      ...headers,
    },
  });
}
