// Simple replacement server using the same functionality as the previous chat server
// This file is a fresh, cleanized version that includes robust model discovery

const http = require('http');
const fs = require('fs/promises');
const fsSync = require('fs');
const path = require('path');
const os = require('os');
const { spawn } = require('child_process');
const { randomUUID, randomBytes } = require('crypto');
const archiver = require('archiver');
const AdmZip = require('adm-zip');
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const nodemailer = null;
const PDFDocument = require('pdfkit');
const security = require('./security');
const { createExternalWpTester, getExternalTestingConfig } = require('./external-wp-testing');
const blogSystem = require('./blog-system');
const versionManager = require('./src/utils/versionManager');

let sharp = null;
try {
  // Optional dependency (native). If missing, server will store originals.
  sharp = require('sharp');
} catch (_) {
  sharp = null;
}

const KIB = 1024;
const MIB = KIB * 1024;
const GIB = MIB * 1024;
const TIB = GIB * 1024;

function parseMemoryValue(raw) {
  if (raw === undefined || raw === null) return 0;
  const str = String(raw).trim();
  const match = str.match(/^([0-9.]+)\s*([kKmMgGtT]i?)?(?:[bB])?$/);
  if (!match) return Number(str) || 0;
  const value = parseFloat(match[1]);
  const unit = (match[2] || '').toLowerCase();
  // Binary unit multipliers (KiB, MiB, GiB, TiB)
  const multipliers = { '': 1, k: KIB, ki: KIB, m: MIB, mi: MIB, g: GIB, gi: GIB, t: TIB, ti: TIB };
  if (!Number.isFinite(value) || value <= 0) return 0;
  return Math.round(value * (multipliers[unit] || 1));
}

/**
 * Normalize interval values so misconfigured env vars can't create tight loops.
 * @param {string|number|undefined} raw
 * @param {number} fallback
 * @param {number} min
 * @returns {number}
 */
function resolveIntervalMs(raw, fallback, min) {
  const parsed = Number(raw);
  if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
  return Math.max(parsed, min);
}

function resolvePositiveInt(raw, fallback) {
  const parsed = Number(raw);
  if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
  return Math.round(parsed);
}

function formatDuration(ms) {
  if (ms < 1000) return Math.round(ms) + 'ms';
  if (ms < 60000) return (ms / 1000).toFixed(1) + 's';
  if (ms < 3600000) return (ms / 60000).toFixed(1) + 'm';
  if (ms < 86400000) return (ms / 3600000).toFixed(1) + 'h';
  return (ms / 86400000).toFixed(1) + 'd';
}

function detectDockerLimits(baseMemoryBytes, baseCpuCores, repoRoot) {
  let memoryBytes = baseMemoryBytes;
  let cpuCores = baseCpuCores;
  const root = repoRoot || process.cwd();
  // Best-effort parse of docker compose files without adding a YAML dependency; unexpected formats fall back to provided defaults.
  const candidates = ['stack-portainer.yml', 'docker-compose.yml'];
  for (const candidate of candidates) {
    try {
      const content = fsSync.readFileSync(path.join(root, candidate), 'utf8');
      const memMatch = content.match(/memory:\s*['"]?([0-9.]+\s*[kKmMgGtT]i?[bB]?)/);
      if (memMatch) {
        const parsed = parseMemoryValue(memMatch[1]);
        if (parsed > 0) memoryBytes = parsed;
      }
      const cpuMatch = content.match(/cpus:\s*['"]?([0-9.]+)/);
      if (cpuMatch) {
        const parsedCpu = Number(cpuMatch[1]);
        if (Number.isFinite(parsedCpu) && parsedCpu > 0) cpuCores = parsedCpu;
      }
    } catch (_) {
      // Ignore missing or unreadable docker config files and fall back to defaults
    }
  }
  return { memoryBytes, cpuCores };
}

const PORT = Number(process.env.CHAT_PORT || 4000);
const HOST = process.env.CHAT_HOST || '0.0.0.0';
const DATA_ROOT = process.env.CHAT_DATA_ROOT || path.join(process.cwd(), '.data');
const STATE_DIR = path.join(DATA_ROOT, '.opencode-chat');
const STATE_FILE = path.join(STATE_DIR, 'sessions.json');
const WORKSPACES_ROOT = path.join(DATA_ROOT, 'apps');
const STATIC_ROOT = path.join(__dirname, 'public');
const UPLOADS_DIR = path.join(STATE_DIR, 'uploads');
const REPO_ROOT = process.env.CHAT_REPO_ROOT || process.cwd();
const OPENCODE_REPO_ROOT = path.join(REPO_ROOT, 'opencode');
const OPENCODE_REPO_CLI = path.join(OPENCODE_REPO_ROOT, 'packages', 'opencode', 'bin', 'opencode');
const OPENCODE_PROMPT_DIR = path.join(OPENCODE_REPO_ROOT, 'packages', 'opencode', 'src', 'session', 'prompt');
const OPENCODE_REQUIRE_REPO = process.env.OPENCODE_REQUIRE_REPO !== 'false';
const DEFAULT_OPENROUTER_API_URL = 'https://openrouter.ai/api/v1/chat/completions';
const OPENROUTER_API_URL = process.env.OPENROUTER_API_URL || DEFAULT_OPENROUTER_API_URL;
const OPENROUTER_API_KEY = process.env.OPENROUTER_API_KEY || process.env.OPENROUTER_API_TOKEN || '';
// Separate paid API key for premium models (optional)
const OPENROUTER_PAID_API_KEY = process.env.OPENROUTER_PAID_API_KEY || process.env.OPENROUTER_PAID_API_TOKEN || OPENROUTER_API_KEY;
const OPENCODE_OLLAMA_API_KEY = process.env.OPENCODE_OLLAMA_API_KEY || '';
const OPENCODE_OLLAMA_BASE_URL = process.env.OPENCODE_OLLAMA_BASE_URL || 'https://ollama.plugincompass.com';
const OPENCODE_OLLAMA_MODEL = process.env.OPENCODE_OLLAMA_MODEL || 'qwen3:0.6b';
const OPENCODE_OLLAMA_PROVIDER = process.env.OPENCODE_OLLAMA_PROVIDER || 'openai';

// Ollama self-hosted (planning provider) - can be set via OLLAMA_* env vars or legacy OPENCODE_OLLAMA_* vars
const OLLAMA_API_URL = process.env.OLLAMA_API_URL || OPENCODE_OLLAMA_BASE_URL || '';
const OLLAMA_API_KEY = process.env.OLLAMA_API_KEY || process.env.OLLAMA_API_TOKEN || OPENCODE_OLLAMA_API_KEY || '';
const OLLAMA_DEFAULT_MODEL = process.env.OLLAMA_DEFAULT_MODEL || OPENCODE_OLLAMA_MODEL || ''; 
const OPENROUTER_MODEL_PRIMARY = process.env.OPENROUTER_MODEL_PRIMARY || process.env.OPENROUTER_MODEL || '';
const OPENROUTER_MODEL_BACKUP_1 = process.env.OPENROUTER_MODEL_BACKUP_1 || process.env.OPENROUTER_MODEL_BACKUP1 || '';
const OPENROUTER_MODEL_BACKUP_2 = process.env.OPENROUTER_MODEL_BACKUP_2 || process.env.OPENROUTER_MODEL_BACKUP2 || '';
const OPENROUTER_MODEL_BACKUP_3 = process.env.OPENROUTER_MODEL_BACKUP_3 || process.env.OPENROUTER_MODEL_BACKUP3 || '';
const OPENROUTER_FALLBACK_MODELS = process.env.OPENROUTER_FALLBACK_MODELS
  ? process.env.OPENROUTER_FALLBACK_MODELS.split(',').map((m) => m.trim()).filter(Boolean)
  : []; 
const OPENROUTER_STATIC_FALLBACK_MODELS = [
  'anthropic/claude-3.5-sonnet',
  'openai/gpt-4o-mini',
  'mistralai/mistral-large-latest',
  'google/gemini-flash-1.5',
];
const OPENROUTER_DEFAULT_MODEL = process.env.OPENROUTER_DEFAULT_MODEL || 'openai/gpt-4o-mini';
const OPENROUTER_PLAN_PROMPT_PATH = process.env.OPENROUTER_PLAN_PROMPT_PATH || path.join(STATIC_ROOT, 'openrouter-plan-prompt.txt');
const OPENROUTER_APP_NAME = process.env.OPENROUTER_APP_NAME || 'Shopify AI App Builder';
const OPENROUTER_SITE_URL = process.env.OPENROUTER_SITE_URL || process.env.OPENROUTER_SITE || '';
const OPENROUTER_ERROR_DETAIL_LIMIT = 400;

// External directory restriction for OpenCode (auto-deny based on app ID)
const ENABLE_EXTERNAL_DIR_RESTRICTION = process.env.ENABLE_EXTERNAL_DIR_RESTRICTION !== 'false';
let warnedOpenRouterApiUrl = false;

// Mistral configuration
const DEFAULT_MISTRAL_API_URL = 'https://api.mistral.ai/v1/chat/completions';
const MISTRAL_API_URL = process.env.MISTRAL_API_URL || DEFAULT_MISTRAL_API_URL;
const MISTRAL_API_KEY = process.env.MISTRAL_API_KEY || '';
const MISTRAL_MODEL_PRIMARY = process.env.MISTRAL_MODEL_PRIMARY || process.env.MISTRAL_MODEL || '';
const MISTRAL_MODEL_BACKUP_1 = process.env.MISTRAL_MODEL_BACKUP_1 || process.env.MISTRAL_MODEL_BACKUP1 || '';
const MISTRAL_MODEL_BACKUP_2 = process.env.MISTRAL_MODEL_BACKUP_2 || process.env.MISTRAL_MODEL_BACKUP2 || '';
const MISTRAL_MODEL_BACKUP_3 = process.env.MISTRAL_MODEL_BACKUP_3 || process.env.MISTRAL_MODEL_BACKUP3 || '';
const MISTRAL_DEFAULT_MODEL = process.env.MISTRAL_DEFAULT_MODEL || 'mistral-large-latest';
const MISTRAL_ERROR_DETAIL_LIMIT = 400;

// Optional direct provider credentials (set via environment variables)
const GOOGLE_API_KEY = process.env.GOOGLE_API_KEY || process.env.GOOGLE_API_TOKEN || '';
const GOOGLE_API_URL = process.env.GOOGLE_API_URL || 'https://generativelanguage.googleapis.com/v1beta2';
const GROQ_API_KEY = process.env.GROQ_API_KEY || process.env.GROQ_API_TOKEN || '';
const GROQ_API_URL = process.env.GROQ_API_URL || 'https://api.groq.com/openai/v1/chat/completions';
const NVIDIA_API_KEY = process.env.NVIDIA_API_KEY || process.env.NVIDIA_API_TOKEN || '';
const NVIDIA_API_URL = process.env.NVIDIA_API_URL || 'https://api.nvidia.com/v1';
const CHUTES_API_KEY = process.env.PLUGIN_COMPASS_CHUTES_API_KEY || process.env.CHUTES_API_KEY || process.env.CHUTES_API_TOKEN || '';
const CHUTES_API_URL = process.env.CHUTES_API_URL || 'https://api.chutes.ai/v1';

const PROVIDER_LIMITS_FILE = path.join(STATE_DIR, 'provider-limits.json');
const PROVIDER_USAGE_FILE = path.join(STATE_DIR, 'provider-usage.json');
const TOKEN_USAGE_FILE = path.join(STATE_DIR, 'token-usage.json');
const EXTERNAL_TEST_USAGE_FILE = path.join(STATE_DIR, 'external-test-usage.json');
const TOPUP_SESSIONS_FILE = path.join(STATE_DIR, 'topup-sessions.json');
const TOPUP_PENDING_FILE = path.join(STATE_DIR, 'topup-pending.json');
const PAYG_SESSIONS_FILE = path.join(STATE_DIR, 'payg-sessions.json');
const PAYG_PENDING_FILE = path.join(STATE_DIR, 'payg-pending.json');
const SUBSCRIPTION_SESSIONS_FILE = path.join(STATE_DIR, 'subscription-sessions.json');
const SUBSCRIPTION_PENDING_FILE = path.join(STATE_DIR, 'subscription-pending.json');
const PLAN_TOKENS_FILE = path.join(STATE_DIR, 'plan-tokens.json');
const TOKEN_RATES_FILE = path.join(STATE_DIR, 'token-rates.json');
const FEATURE_REQUESTS_FILE = path.join(STATE_DIR, 'feature-requests.json');
const CONTACT_MESSAGES_FILE = path.join(STATE_DIR, 'contact-messages.json');
const INVOICES_FILE = path.join(STATE_DIR, 'invoices.json');
const INVOICES_DIR = path.join(STATE_DIR, 'invoices');
const PROCESSED_WEBHOOKS_FILE = path.join(STATE_DIR, 'processed-webhooks.json');
const PROCESSED_WEBHOOKS_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
// One-off top-up discounts (Business 2.5%, Enterprise 5%; boost add-ons keep higher 10%/25% rates)
const BUSINESS_TOPUP_DISCOUNT = 0.025;
const ENTERPRISE_TOPUP_DISCOUNT = 0.05;
const MIN_PAYMENT_AMOUNT = (() => {
  const parsed = Number(process.env.DODO_MIN_AMOUNT || 50);
  return Number.isFinite(parsed) && parsed > 0 ? parsed : 50;
})();
const DODO_PAYMENTS_API_KEY = process.env.DODO_PAYMENTS_API_KEY || process.env.DODO_API_KEY || '';
const DODO_ENVIRONMENT = (process.env.DODO_PAYMENTS_ENV || process.env.DODO_ENVIRONMENT || process.env.DODO_ENV || 'test').toLowerCase();
const DODO_BASE_URL = DODO_ENVIRONMENT.includes('live') ? 'https://live.dodopayments.com' : 'https://test.dodopayments.com';
const DODO_ENABLED = Boolean(DODO_PAYMENTS_API_KEY);
// Token top-up product IDs (4 options × 3 currencies)
const TOPUP_PRODUCT_IDS = {
  // Top-up Option 1 (100,000 tokens)
  topup_1_usd: process.env.DODO_TOPUP_1_USD || '',
  topup_1_gbp: process.env.DODO_TOPUP_1_GBP || '',
  topup_1_eur: process.env.DODO_TOPUP_1_EUR || '',

  // Top-up Option 2 (5,000,000 tokens)
  topup_2_usd: process.env.DODO_TOPUP_2_USD || '',
  topup_2_gbp: process.env.DODO_TOPUP_2_GBP || '',
  topup_2_eur: process.env.DODO_TOPUP_2_EUR || '',

  // Top-up Option 3 (20,000,000 tokens)
  topup_3_usd: process.env.DODO_TOPUP_3_USD || '',
  topup_3_gbp: process.env.DODO_TOPUP_3_GBP || '',
  topup_3_eur: process.env.DODO_TOPUP_3_EUR || '',

  // Top-up Option 4 (50,000,000 tokens)
  topup_4_usd: process.env.DODO_TOPUP_4_USD || '',
  topup_4_gbp: process.env.DODO_TOPUP_4_GBP || '',
  topup_4_eur: process.env.DODO_TOPUP_4_EUR || '',
};
const TOPUP_TOKENS = {
  topup_1: resolvePositiveInt(process.env.DODO_TOPUP_TOKENS_1, 100_000),
  topup_2: resolvePositiveInt(process.env.DODO_TOPUP_TOKENS_2, 5_000_000),
  topup_3: resolvePositiveInt(process.env.DODO_TOPUP_TOKENS_3, 20_000_000),
  topup_4: resolvePositiveInt(process.env.DODO_TOPUP_TOKENS_4, 50_000_000),
};

// Pay-as-you-go (overage) product IDs (1 option × 3 currencies)
const PAYG_PRODUCT_IDS = {
  usd: process.env.DODO_PAYG_USD || '',
  gbp: process.env.DODO_PAYG_GBP || '',
  eur: process.env.DODO_PAYG_EUR || '',
};

// Pay-as-you-go pricing in minor units per PAYG unit (defaults: $2.50 / €2.50 / £2.00 per 1M tokens)
const PAYG_UNIT_TOKENS = Math.max(1, Number(process.env.DODO_PAYG_UNIT_TOKENS || 1_000_000));
const PAYG_PRICES = {
  usd: Number(process.env.DODO_PAYG_USD_AMOUNT || 250),
  gbp: Number(process.env.DODO_PAYG_GBP_AMOUNT || 200),
  eur: Number(process.env.DODO_PAYG_EUR_AMOUNT || 250),
};
const PAYG_MIN_TOKENS = Math.max(0, Number(process.env.DODO_PAYG_MIN_TOKENS || 0));
const PAYG_ENABLED = (process.env.DODO_PAYG_ENABLED || '1') !== '0';

// Usage-based billing events (Dodo meters)
const DODO_USAGE_EVENTS_ENABLED = (process.env.DODO_USAGE_EVENTS_ENABLED || '1') !== '0';
const DODO_USAGE_EVENT_NAME = process.env.DODO_USAGE_EVENT_NAME || 'token.usage';
const DODO_USAGE_EVENT_COST_FIELD = process.env.DODO_USAGE_EVENT_COST_FIELD || 'costCents';
const DODO_USAGE_EVENT_TOKENS_FIELD = process.env.DODO_USAGE_EVENT_TOKENS_FIELD || 'billableTokens';

// Subscription product IDs for plans (plan_billing_currency format)
const SUBSCRIPTION_PRODUCT_IDS = {
  // Starter Plan Products
  starter_monthly_usd: process.env.DODO_STARTER_MONTHLY_USD || 'prod_starter_monthly_usd',
  starter_yearly_usd: process.env.DODO_STARTER_YEARLY_USD || 'prod_starter_yearly_usd',
  starter_monthly_gbp: process.env.DODO_STARTER_MONTHLY_GBP || 'prod_starter_monthly_gbp',
  starter_yearly_gbp: process.env.DODO_STARTER_YEARLY_GBP || 'prod_starter_yearly_gbp',
  starter_monthly_eur: process.env.DODO_STARTER_MONTHLY_EUR || 'prod_starter_monthly_eur',
  starter_yearly_eur: process.env.DODO_STARTER_YEARLY_EUR || 'prod_starter_yearly_eur',
  
  // Professional Plan Products
  professional_monthly_usd: process.env.DODO_PROFESSIONAL_MONTHLY_USD || 'prod_professional_monthly_usd',
  professional_yearly_usd: process.env.DODO_PROFESSIONAL_YEARLY_USD || 'prod_professional_yearly_usd',
  professional_monthly_gbp: process.env.DODO_PROFESSIONAL_MONTHLY_GBP || 'prod_professional_monthly_gbp',
  professional_yearly_gbp: process.env.DODO_PROFESSIONAL_YEARLY_GBP || 'prod_professional_yearly_gbp',
  professional_monthly_eur: process.env.DODO_PROFESSIONAL_MONTHLY_EUR || 'prod_professional_monthly_eur',
  professional_yearly_eur: process.env.DODO_PROFESSIONAL_YEARLY_EUR || 'prod_professional_yearly_eur',
  
  // Enterprise Plan Products
  enterprise_monthly_usd: process.env.DODO_ENTERPRISE_MONTHLY_USD || 'prod_enterprise_monthly_usd',
  enterprise_yearly_usd: process.env.DODO_ENTERPRISE_YEARLY_USD || 'prod_enterprise_yearly_usd',
  enterprise_monthly_gbp: process.env.DODO_ENTERPRISE_MONTHLY_GBP || 'prod_enterprise_monthly_gbp',
  enterprise_yearly_gbp: process.env.DODO_ENTERPRISE_YEARLY_GBP || 'prod_enterprise_yearly_gbp',
  enterprise_monthly_eur: process.env.DODO_ENTERPRISE_MONTHLY_EUR || 'prod_enterprise_monthly_eur',
  enterprise_yearly_eur: process.env.DODO_ENTERPRISE_YEARLY_EUR || 'prod_enterprise_yearly_eur',
};

// Subscription pricing (in cents/minor units)
const SUBSCRIPTION_PRICES = {
  // Starter Plan Prices
  starter_monthly_usd: Number(process.env.DODO_STARTER_MONTHLY_USD_AMOUNT || 750),
  starter_yearly_usd: Number(process.env.DODO_STARTER_YEARLY_USD_AMOUNT || 7500),
  starter_monthly_gbp: Number(process.env.DODO_STARTER_MONTHLY_GBP_AMOUNT || 625),
  starter_yearly_gbp: Number(process.env.DODO_STARTER_YEARLY_GBP_AMOUNT || 6250),
  starter_monthly_eur: Number(process.env.DODO_STARTER_MONTHLY_EUR_AMOUNT || 750),
  starter_yearly_eur: Number(process.env.DODO_STARTER_YEARLY_EUR_AMOUNT || 7500),
  
  // Professional Plan Prices
  professional_monthly_usd: Number(process.env.DODO_PROFESSIONAL_MONTHLY_USD_AMOUNT || 2500),
  professional_yearly_usd: Number(process.env.DODO_PROFESSIONAL_YEARLY_USD_AMOUNT || 25000),
  professional_monthly_gbp: Number(process.env.DODO_PROFESSIONAL_MONTHLY_GBP_AMOUNT || 2100),
  professional_yearly_gbp: Number(process.env.DODO_PROFESSIONAL_YEARLY_GBP_AMOUNT || 21000),
  professional_monthly_eur: Number(process.env.DODO_PROFESSIONAL_MONTHLY_EUR_AMOUNT || 2500),
  professional_yearly_eur: Number(process.env.DODO_PROFESSIONAL_YEARLY_EUR_AMOUNT || 25000),
  
  // Enterprise Plan Prices
  enterprise_monthly_usd: Number(process.env.DODO_ENTERPRISE_MONTHLY_USD_AMOUNT || 7500),
  enterprise_yearly_usd: Number(process.env.DODO_ENTERPRISE_YEARLY_USD_AMOUNT || 75000),
  enterprise_monthly_gbp: Number(process.env.DODO_ENTERPRISE_MONTHLY_GBP_AMOUNT || 6250),
  enterprise_yearly_gbp: Number(process.env.DODO_ENTERPRISE_YEARLY_GBP_AMOUNT || 62500),
  enterprise_monthly_eur: Number(process.env.DODO_ENTERPRISE_MONTHLY_EUR_AMOUNT || 7500),
  enterprise_yearly_eur: Number(process.env.DODO_ENTERPRISE_YEARLY_EUR_AMOUNT || 75000),
};

// Token top-up pricing (in cents/minor units) - 4 options × 3 currencies
// Option 1: 100,000 tokens, Option 2: 5,000,000 tokens, Option 3: 20,000,000 tokens, Option 4: 50,000,000 tokens
const TOPUP_PRICES = {
  // Top-up Option 1 (100,000 tokens)
  topup_1_usd: resolvePositiveInt(process.env.DODO_TOPUP_1_USD_AMOUNT, 750),
  topup_1_gbp: resolvePositiveInt(process.env.DODO_TOPUP_1_GBP_AMOUNT, 500),
  topup_1_eur: resolvePositiveInt(process.env.DODO_TOPUP_1_EUR_AMOUNT, 750),

  // Top-up Option 2 (5,000,000 tokens)
  topup_2_usd: resolvePositiveInt(process.env.DODO_TOPUP_2_USD_AMOUNT, 2500),
  topup_2_gbp: resolvePositiveInt(process.env.DODO_TOPUP_2_GBP_AMOUNT, 2000),
  topup_2_eur: resolvePositiveInt(process.env.DODO_TOPUP_2_EUR_AMOUNT, 2500),

  // Top-up Option 3 (20,000,000 tokens)
  topup_3_usd: resolvePositiveInt(process.env.DODO_TOPUP_3_USD_AMOUNT, 7500),
  topup_3_gbp: resolvePositiveInt(process.env.DODO_TOPUP_3_GBP_AMOUNT, 6000),
  topup_3_eur: resolvePositiveInt(process.env.DODO_TOPUP_3_EUR_AMOUNT, 7500),

  // Top-up Option 4 (50,000,000 tokens)
  topup_4_usd: resolvePositiveInt(process.env.DODO_TOPUP_4_USD_AMOUNT, 12500),
  topup_4_gbp: resolvePositiveInt(process.env.DODO_TOPUP_4_GBP_AMOUNT, 10000),
  topup_4_eur: resolvePositiveInt(process.env.DODO_TOPUP_4_EUR_AMOUNT, 12500),
};

// Supported billing cycles and currencies
const BILLING_CYCLES = ['monthly', 'yearly'];
const SUPPORTED_CURRENCIES = ['usd', 'gbp', 'eur'];
const MINUTE_MS = 60_000;
const HOUR_MS = 3_600_000;
const DAY_MS = 86_400_000;
const DODO_PRODUCTS_CACHE_TTL_MS = Math.max(30_000, Number(process.env.DODO_PRODUCTS_CACHE_TTL_MS || 5 * MINUTE_MS));
const FORTY_EIGHT_HOURS_MS = 48 * DAY_MS;
const AVG_CHARS_PER_TOKEN = 4; // rough heuristic
const MAX_JSON_BODY_SIZE = Number(process.env.MAX_JSON_BODY_SIZE || 6_000_000); // 6 MB default for JSON payloads (attachments)
const MAX_ATTACHMENT_SIZE = Number(process.env.MAX_ATTACHMENT_SIZE || 5_000_000); // 5 MB limit per attachment
const AFFILIATES_FILE = path.join(STATE_DIR, 'affiliates.json');
const WITHDRAWALS_FILE = path.join(STATE_DIR, 'withdrawals.json');
const AFFILIATE_COOKIE_NAME = 'affiliate_session';
const AFFILIATE_REF_COOKIE = 'affiliate_ref';
const AFFILIATE_SESSION_TTL_MS = Number(process.env.AFFILIATE_SESSION_TTL_MS || 30 * DAY_MS);
const AFFILIATE_COMMISSION_RATE = 0.075;
const AFFILIATE_REF_COOKIE_TTL_MS = 30 * DAY_MS;
const AFFILIATE_REF_COOKIE_TTL_SECONDS = Math.floor(AFFILIATE_REF_COOKIE_TTL_MS / 1000);
const ADMIN_USER = process.env.ADMIN_USER || process.env.ADMIN_EMAIL || '';
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || process.env.ADMIN_PASS || '';
const ADMIN_SESSION_TTL_MS = Number(process.env.ADMIN_SESSION_TTL_MS || 86_400_000); // default 24h
const ADMIN_MODELS_FILE = path.join(STATE_DIR, 'admin-models.json');
const OPENROUTER_SETTINGS_FILE = path.join(STATE_DIR, 'openrouter-settings.json');
const MISTRAL_SETTINGS_FILE = path.join(STATE_DIR, 'mistral-settings.json');
const ADMIN_COOKIE_NAME = 'admin_session';
const TRACKING_FILE = path.join(STATE_DIR, 'tracking.json');
const TRACKING_PERSIST_INTERVAL_MS = 60000; // Persist every minute
const ASSETS_DIR = path.join(STATIC_ROOT, 'assets');
const DEFAULT_MEMORY_LIMIT_BYTES = (() => {
  const parsed = parseMemoryValue(process.env.CONTAINER_MEMORY_LIMIT || process.env.MAX_CONTAINER_MEMORY || '512M');
  return parsed > 0 ? parsed : parseMemoryValue('512M');
})();
const DEFAULT_CPU_LIMIT_CORES = (() => {
  const parsed = Number(process.env.CONTAINER_CPU_LIMIT || process.env.MAX_CONTAINER_CPU || 0.5);
  return Number.isFinite(parsed) && parsed > 0 ? parsed : 0.5;
})();
const RESOURCE_LIMITS = detectDockerLimits(DEFAULT_MEMORY_LIMIT_BYTES, DEFAULT_CPU_LIMIT_CORES, REPO_ROOT);
const RESOURCE_MEMORY_SOFT_RATIO = Number(process.env.RESOURCE_MEMORY_SOFT_RATIO || 0.9);
const RESOURCE_CPU_SOFT_RATIO = Number(process.env.RESOURCE_CPU_SOFT_RATIO || 0.9);
const RESOURCE_CHECK_INTERVAL_MS = resolveIntervalMs(process.env.RESOURCE_CHECK_INTERVAL_MS, 750, 250);
const RESOURCE_MIN_LOAD_FLOOR = Number(process.env.RESOURCE_MIN_LOAD_FLOOR || 0.5);
const RESOURCE_WAIT_LOG_INTERVAL_MS = resolveIntervalMs(process.env.RESOURCE_WAIT_LOG_INTERVAL_MS, 4000, 500);
const OPENCODE_MAX_CONCURRENCY = Number(process.env.OPENCODE_MAX_CONCURRENCY || 0);

// User authentication configuration
const USERS_DB_FILE = path.join(STATE_DIR, 'users.json');
const USER_SESSIONS_FILE = path.join(STATE_DIR, 'user-sessions.json');
const USER_SESSION_SECRET = process.env.USER_SESSION_SECRET || process.env.SESSION_SECRET || (() => {
  // Generate a secure random session secret for development
  // In production, this should be set via environment variable
  const generatedSecret = randomBytes(32).toString('hex');
  console.warn('⚠️  WARNING: No USER_SESSION_SECRET or SESSION_SECRET found. Generated a random secret for this session.');
  console.warn('⚠️  For production use, set USER_SESSION_SECRET environment variable to a secure random value.');
  console.warn('⚠️  Generate one with: openssl rand -hex 32');
  return generatedSecret;
})();
const USER_COOKIE_NAME = 'user_session';
const USER_SESSION_TTL_MS = Number(process.env.USER_SESSION_TTL_MS || 30 * 24 * 60 * 60 * 1000); // default 30 days
const USER_SESSION_SHORT_TTL_MS = Number(process.env.USER_SESSION_SHORT_TTL_MS || 3 * 24 * 60 * 60 * 1000); // default 3 days
const PASSWORD_SALT_ROUNDS = 12; // bcrypt salt rounds
const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID || '';
const GOOGLE_CLIENT_SECRET = process.env.GOOGLE_CLIENT_SECRET || '';
const GITHUB_CLIENT_ID = process.env.GITHUB_CLIENT_ID || '';
const GITHUB_CLIENT_SECRET = process.env.GITHUB_CLIENT_SECRET || '';
const PUBLIC_BASE_URL = process.env.PUBLIC_BASE_URL || '';
const OAUTH_STATE_TTL_MS = Number(process.env.OAUTH_STATE_TTL_MS || 10 * 60 * 1000); // 10 minutes
const oauthStateStore = new Map();
const OAUTH_USER_AGENT = process.env.OAUTH_USER_AGENT || 'shopify-ai-app-builder';
const EMAIL_VERIFICATION_TTL_MS = Number(process.env.EMAIL_VERIFICATION_TTL_MS || 24 * 60 * 60 * 1000); // 24h
const PASSWORD_RESET_TTL_MS = Number(process.env.PASSWORD_RESET_TTL_MS || 60 * 60 * 1000); // 1h
const MAILPILOT_URL = process.env.MAILPILOT_URL || 'https://emailmarketing.modelrailway3d.co.uk';
const MAILPILOT_TOKEN = process.env.MAILPILOT_TOKEN || '';
const POSTHOG_API_KEY = process.env.POSTHOG_API_KEY || '';
const POSTHOG_API_HOST = process.env.POSTHOG_API_HOST || 'https://app.posthog.com';
const DODO_WEBHOOK_KEY = process.env.DODO_PAYMENTS_WEBHOOK_KEY || process.env.DODO_WEBHOOK_KEY || '';
const USER_PLANS = ['hobby', 'starter', 'professional', 'enterprise'];
const DEFAULT_PLAN = 'hobby';
const DEFAULT_BILLING_STATUS = 'active';
const PAID_PLANS = new Set(['starter', 'professional', 'enterprise']);
const MAX_UPLOAD_ZIP_SIZE = Number(process.env.MAX_UPLOAD_ZIP_SIZE || 25_000_000); // 25 MB default for uploaded zip apps
const MAX_EXPORT_ZIP_SIZE = Number(process.env.MAX_EXPORT_ZIP_SIZE || 100_000_000); // 100 MB default for exported zip apps
const MAX_EXPORT_FILE_COUNT = Number(process.env.MAX_EXPORT_FILE_COUNT || 10000); // Max 10,000 files per export
const BASE64_OVERHEAD_MULTIPLIER = 1.34; // ~33% overhead with small buffer
const ZIP_LOCAL_HEADER_SIG = [0x50, 0x4b, 0x03, 0x04];
const ZIP_EOCD_EMPTY_SIG = [0x50, 0x4b, 0x05, 0x06];
const BLOCKED_PATH_PATTERN = /^(?:[a-zA-Z]:|\\\\|\/\/|con:|prn:|aux:|nul:|com[1-9]:|lpt[1-9]:)/i;
const PLAN_APP_LIMITS = {
  hobby: 3,
  starter: 10,
  professional: 20,
  enterprise: Infinity,
};
const PLAN_TOKEN_LIMITS = {
  hobby: 50_000,
  starter: 100_000,
  professional: 5_000_000,
  enterprise: 20_000_000,
};
const EXTERNAL_TEST_LIMITS = {
  hobby: 3,
  starter: 50,
  professional: Infinity,
  enterprise: Infinity,
};

// Default token rates (price per 1M tokens in minor units/cents)
const DEFAULT_TOKEN_RATES = {
  usd: 250,
  gbp: 200,
  eur: 250,
};

// Runtime-editable copy persisted to disk
let planTokenLimits = JSON.parse(JSON.stringify(PLAN_TOKEN_LIMITS));
let tokenRates = JSON.parse(JSON.stringify(DEFAULT_TOKEN_RATES));

async function loadPlanTokenLimits() {
  try {
    await ensureStateFile();
    const raw = await fs.readFile(PLAN_TOKENS_FILE, 'utf8').catch(() => null);
    if (raw) {
      const parsed = JSON.parse(raw);
      if (parsed && typeof parsed === 'object') {
        const clean = {};
        Object.keys(PLAN_TOKEN_LIMITS).forEach((plan) => { clean[plan] = 0; });
        for (const [plan, value] of Object.entries(parsed)) {
          if (!clean.hasOwnProperty(plan)) continue;
          clean[plan] = Math.max(0, Number(value || 0));
        }
        planTokenLimits = clean;
      }
    }
  } catch (error) {
    log('Failed to load plan token limits, using defaults', { error: String(error) });
  }
}

async function persistPlanTokenLimits() {
  await ensureStateFile();
  const payload = JSON.stringify(planTokenLimits, null, 2);
  try {
    await safeWriteFile(PLAN_TOKENS_FILE, payload);
  } catch (err) {
    log('Failed to persist plan token limits', { error: String(err) });
  }
}

async function loadTokenRates() {
  try {
    await ensureStateFile();
    const raw = await fs.readFile(TOKEN_RATES_FILE, 'utf8').catch(() => null);
    if (raw) {
      const parsed = JSON.parse(raw);
      if (parsed && typeof parsed === 'object') {
        const clean = {};
        Object.keys(DEFAULT_TOKEN_RATES).forEach((currency) => { clean[currency] = 0; });
        for (const [currency, value] of Object.entries(parsed)) {
          if (!clean.hasOwnProperty(currency)) continue;
          clean[currency] = Math.max(0, Number(value || 0));
        }
        tokenRates = clean;
      }
    }
  } catch (error) {
    log('Failed to load token rates, using defaults', { error: String(error) });
  }
}

async function persistTokenRates() {
  await ensureStateFile();
  const payload = JSON.stringify(tokenRates, null, 2);
  try {
    await safeWriteFile(TOKEN_RATES_FILE, payload);
  } catch (err) {
    log('Failed to persist token rates', { error: String(err) });
  }
}

const PLAN_PRICES = {
  starter: Number(process.env.PRICE_STARTER || 7.5),
  professional: Number(process.env.PRICE_BUSINESS || 25),
  enterprise: Number(process.env.PRICE_ENTERPRISE || 75),
};
const AUTO_MODEL_TOKEN = 'auto';
const DEFAULT_PROVIDER_FALLBACK = 'opencode';
const DEFAULT_PROVIDER_SEEDS = ['openrouter', 'mistral', 'google', 'groq', 'nvidia', 'chutes', 'cerebras', 'ollama', DEFAULT_PROVIDER_FALLBACK, 'cohere', 'kilo'];
const PROVIDER_PERSIST_DEBOUNCE_MS = 200;
const TOKEN_ESTIMATION_BUFFER = 400;
const BOOST_PACK_SIZE = 500_000;
const BOOST_BASE_PRICE = 15;
const TOKEN_GRACE_RATIO = 0.05;
const TOKEN_GRACE_MIN = 500;
const HOBBY_PRIORITY_DELAY_MS = (() => {
  const raw = Number(process.env.HOBBY_PRIORITY_DELAY_MS || process.env.STARTER_PRIORITY_DELAY_MS);
  return Number.isFinite(raw) && raw >= 0 ? raw : 280;
})();
const STARTER_PRIORITY_DELAY_MS = (() => {
  const raw = Number(process.env.STARTER_PRIORITY_DELAY_MS);
  return Number.isFinite(raw) && raw >= 0 ? raw : 220;
})();
const BUSINESS_PRIORITY_DELAY_MS = (() => {
  const raw = Number(process.env.BUSINESS_PRIORITY_DELAY_MS);
  return Number.isFinite(raw) && raw >= 0 ? raw : 120;
})();
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

const IMAGE_COMPRESSION_ENABLED = (process.env.IMAGE_COMPRESSION_ENABLED || '1') !== '0';
const IMAGE_MAX_DIMENSION = Number(process.env.IMAGE_MAX_DIMENSION || 1600);
const IMAGE_WEBP_QUALITY = Number(process.env.IMAGE_WEBP_QUALITY || 78);

function isPaidPlan(plan) {
  const normalized = String(plan || '').trim().toLowerCase();
  return PAID_PLANS.has(normalized);
}

function isImageMime(mimeType) {
  const mt = String(mimeType || '').toLowerCase();
  return mt.startsWith('image/');
}

function getBytesPrefix(buf, length) {
  if (!Buffer.isBuffer(buf)) return Buffer.alloc(0);
  return buf.slice(0, Math.max(0, length || 0));
}

function isLikelyPng(buf) {
  const b = getBytesPrefix(buf, 8);
  return b.length >= 8 && b[0] === 0x89 && b[1] === 0x50 && b[2] === 0x4e && b[3] === 0x47 && b[4] === 0x0d && b[5] === 0x0a && b[6] === 0x1a && b[7] === 0x0a;
}

function isLikelyJpeg(buf) {
  const b = getBytesPrefix(buf, 3);
  return b.length >= 3 && b[0] === 0xff && b[1] === 0xd8 && b[2] === 0xff;
}

function isLikelyGif(buf) {
  const b = getBytesPrefix(buf, 6);
  return b.length >= 6 && b[0] === 0x47 && b[1] === 0x49 && b[2] === 0x46 && b[3] === 0x38 && (b[4] === 0x39 || b[4] === 0x37) && b[5] === 0x61;
}

function isLikelyWebp(buf) {
  const b = getBytesPrefix(buf, 12);
  return b.length >= 12 && b[0] === 0x52 && b[1] === 0x49 && b[2] === 0x46 && b[3] === 0x46 && b[8] === 0x57 && b[9] === 0x45 && b[10] === 0x42 && b[11] === 0x50;
}

function isLikelySvg(buf) {
  const head = getBytesPrefix(buf, 200).toString('utf8').trim().toLowerCase();
  return head.startsWith('<svg') || head.startsWith('<?xml') || head.includes('<svg');
}

function validateImageSignature(clientMimeType, buffer) {
  const mt = String(clientMimeType || '').toLowerCase();
  if (!mt.startsWith('image/')) return true;
  if (!Buffer.isBuffer(buffer) || buffer.length < 8) return false;
  if (mt === 'image/png') return isLikelyPng(buffer);
  if (mt === 'image/jpeg') return isLikelyJpeg(buffer);
  if (mt === 'image/gif') return isLikelyGif(buffer);
  if (mt === 'image/webp') return isLikelyWebp(buffer);
  if (mt === 'image/svg+xml') return isLikelySvg(buffer);
  return true;
}

async function compressImageBuffer(buffer, mimeType) {
  if (!IMAGE_COMPRESSION_ENABLED) return { buffer, mimeType, ext: null };
  if (!sharp) return { buffer, mimeType, ext: null };
  const mt = String(mimeType || '').toLowerCase();
  if (!mt.startsWith('image/')) return { buffer, mimeType, ext: null };
  if (mt === 'image/svg+xml' || mt === 'image/gif') return { buffer, mimeType, ext: null };
  try {
    const pipeline = sharp(buffer, { failOn: 'none' }).rotate();
    const resized = pipeline.resize({ width: IMAGE_MAX_DIMENSION, height: IMAGE_MAX_DIMENSION, fit: 'inside', withoutEnlargement: true });
    const out = await resized.webp({ quality: IMAGE_WEBP_QUALITY }).toBuffer();
    if (out && out.length && out.length < buffer.length) {
      return { buffer: out, mimeType: 'image/webp', ext: 'webp' };
    }
  } catch (err) {
    log('image compression failed; storing original', { mimeType: mt, err: String(err) });
  }
  return { buffer, mimeType, ext: null };
}

function safeFileNamePart(raw) {
  return String(raw || 'upload').replace(/[^a-zA-Z0-9\-_\.]/g, '_').slice(0, 140) || 'upload';
}

function extensionForMime(mimeType) {
  const mt = String(mimeType || '').toLowerCase();
  if (mt === 'image/png') return 'png';
  if (mt === 'image/jpeg') return 'jpg';
  if (mt === 'image/gif') return 'gif';
  if (mt === 'image/webp') return 'webp';
  if (mt === 'image/svg+xml') return 'svg';
  if (mt === 'application/pdf') return 'pdf';
  if (mt === 'text/plain') return 'txt';
  if (mt === 'text/css') return 'css';
  if (mt === 'application/javascript') return 'js';
  if (mt === 'text/html') return 'html';
  if (mt === 'text/markdown') return 'md';
  if (mt === 'text/csv') return 'csv';
  if (mt === 'application/json') return 'json';
  if (mt === 'application/xml' || mt === 'text/xml') return 'xml';
  return 'bin';
}

function toDataUrl(mimeType, base64Data) {
  const mt = String(mimeType || 'application/octet-stream');
  return `data:${mt};base64,${base64Data || ''}`;
}

async function buildOpenRouterMessagesForSession(session, currentMessage) {
  const history = Array.isArray(session?.messages) ? session.messages : [];
  const prior = history.filter((m) => m && m.id !== currentMessage?.id && m.status === 'done').slice(-8);
  const mapped = prior.map((m) => {
    if (m.role === 'assistant') return { role: 'assistant', content: String(m.reply || m.partialOutput || '') };
    return { role: 'user', content: String(m.content || '') };
  }).filter((m) => (m.content || '').trim().length);

  const parts = [];
  const text = String(currentMessage?.content || '').trim();
  if (text) parts.push({ type: 'text', text });
  const images = (Array.isArray(currentMessage?.attachments) ? currentMessage.attachments : []).filter((a) => a && isImageMime(a.type) && a.url);
  for (const img of images.slice(0, 6)) {
    try {
      const filename = String(img.url || '').split('/').pop();
      if (!filename) continue;
      const candidate = path.join(session.uploadsDir, filename);
      const resolved = path.resolve(candidate);
      const uploadsRoot = path.resolve(session.uploadsDir);
      if (!resolved.startsWith(uploadsRoot + path.sep) && resolved !== uploadsRoot) continue;
      const buf = await fs.readFile(resolved);
      parts.push({ type: 'image_url', image_url: { url: toDataUrl(img.type, buf.toString('base64')) } });
    } catch (err) {
      log('failed to embed image for OpenRouter', { err: String(err), url: img.url });
    }
  }
  const userMsg = parts.length ? { role: 'user', content: parts } : { role: 'user', content: text };
  return mapped.concat([userMsg]);
}

const state = { sessions: [] };
const sessionQueues = new Map();
const activeStreams = new Map(); // Track active SSE streams per message
const runningProcesses = new Map(); // Track running opencode processes
let resourceReservations = 0; // Tracks reserved slots; Node event loop serializes updates.
const SUPPORTED_CLIS = ['opencode'];
let server = null;
let isShuttingDown = false;
const AUTO_SAVE_INTERVAL_MS = 120000; // Auto-save every 2 minutes (reduced from 30s)
let autoSaveTimer = null;

// ============================================================================
// Memory Management Constants and State
// ============================================================================
const MEMORY_CLEANUP_INTERVAL_MS = 300000; // Run cleanup every 5 minutes (reduced from 60s)
const SESSION_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
const MESSAGE_HISTORY_LIMIT = 50; // Max messages to keep per session (reduced from 100)
const PARTIAL_OUTPUT_MAX_LENGTH = 50000; // Truncate large outputs in completed messages
const STALE_STREAM_TIMEOUT_MS = 600000; // 10 minutes - clean up stale SSE streams
const STALE_PROCESS_TIMEOUT_MS = 900000; // 15 minutes - clean up potentially stuck processes
const MAX_SESSION_QUEUES = 1000; // Limit concurrent session queues
const MAX_OAUTH_STATES = 10000; // Limit OAuth state entries
const MAX_LOGIN_ATTEMPTS_ENTRIES = 50000; // Limit login attempt tracking
let lastMemoryCleanup = 0;
let memoryCleanupTimer = null;
const recentlyCleanedSessions = new Set(); // Track recently cleaned sessions to avoid redundant work

// Processed webhooks for idempotency protection (prevents duplicate payment processing)
const processedWebhookEvents = new Map(); // eventId -> { processedAt, type }
let processedWebhooksPersistTimer = null;

// Track spawned child processes for proper cleanup
const childProcesses = new Map(); // processId -> { pid, startTime, sessionId, messageId }

// ============================================================================
// Memory Cleanup Functions
// ============================================================================

/**
 * Triggers garbage collection hint and runs memory cleanup
 * @param {string} reason - Reason for triggering cleanup (for logging)
 */
function triggerMemoryCleanup(reason = 'manual') {
  const now = Date.now();
  // Debounce - don't run more than once every 60 seconds (increased from 10s)
  if (now - lastMemoryCleanup < 60000) return;
  lastMemoryCleanup = now;
  
  const beforeMem = process.memoryUsage();
  
  try {
    // Clean up completed session data (only if needed)
    const activeMessages = state.sessions.reduce((count, s) => count + (s.pending || 0), 0);
    if (activeMessages === 0 || state.sessions.length > 20) {
      cleanupCompletedSessions();
    }
    
    // Clean up stale entries from Maps
    cleanupStaleMaps();
    
    // Clean up orphaned processes
    cleanupOrphanedProcesses();
    
    // Truncate large message outputs (less frequently)
    if (now % 300000 < 60000) { // Every 5 minutes
      truncateLargeOutputs();
    }
    
    // Hint to V8 to run GC if available (--expose-gc flag)
    if (global.gc) {
      global.gc();
    }
    
    const afterMem = process.memoryUsage();
    const freedMb = ((beforeMem.heapUsed - afterMem.heapUsed) / (1024 * 1024)).toFixed(2);
    
    if (freedMb > 1) {
      log('Memory cleanup completed', {
        reason,
        freedMb: `${freedMb}MB`,
        beforeHeap: `${(beforeMem.heapUsed / 1024 / 1024).toFixed(2)}MB`,
        afterHeap: `${(afterMem.heapUsed / 1024 / 1024).toFixed(2)}MB`,
        rss: `${(afterMem.rss / 1024 / 1024).toFixed(2)}MB`
      });
    }
  } catch (err) {
    log('Memory cleanup error', { error: String(err), reason });
  }
}

/**
 * Cleans up old and completed session data
 */
function cleanupCompletedSessions() {
  const now = Date.now();
  let cleanedSessions = 0;
  let cleanedMessages = 0;
  
  for (const session of state.sessions) {
    // Skip sessions cleaned in the last hour to avoid redundant work
    const lastCleaned = session.lastCleanedAt ? now - new Date(session.lastCleanedAt).getTime() : Infinity;
    if (lastCleaned < 3600000) continue; // Skip if cleaned within last hour
    
    let sessionChanged = false;
    
    // Clean up old messages within sessions
    if (Array.isArray(session.messages) && session.messages.length > MESSAGE_HISTORY_LIMIT) {
      const excess = session.messages.length - MESSAGE_HISTORY_LIMIT;
      // Remove oldest completed messages first
      const toRemove = session.messages
        .filter(m => m.status === 'done' || m.status === 'error' || m.status === 'cancelled')
        .slice(0, excess);
      
      for (const msg of toRemove) {
        const idx = session.messages.indexOf(msg);
        if (idx !== -1) {
          session.messages.splice(idx, 1);
          cleanedMessages++;
          sessionChanged = true;
        }
      }
    }
    
    // Clear partialOutput from completed messages (keep reply only)
    if (Array.isArray(session.messages)) {
      for (const msg of session.messages) {
        if ((msg.status === 'done' || msg.status === 'error') && msg.partialOutput && msg.reply) {
          // Keep only the final reply, not the streaming partial output
          delete msg.partialOutput;
          cleanedMessages++;
          sessionChanged = true;
        }
      }
    }
    
    // Check session age
    const sessionAge = now - new Date(session.createdAt).getTime();
    if (sessionAge > SESSION_MAX_AGE_MS && session.pending === 0) {
      // Mark for cleanup but don't delete - just clean up heavy data
      if (session.messages && session.messages.length > 20) {
        const oldLength = session.messages.length;
        session.messages = session.messages.slice(-20); // Keep only last 20 messages
        if (session.messages.length < oldLength) {
          cleanedSessions++;
          sessionChanged = true;
        }
      }
    }
    
    // Mark session as cleaned
    if (sessionChanged) {
      session.lastCleanedAt = new Date().toISOString();
    }
  }
  
  if (cleanedSessions > 0 || cleanedMessages > 0) {
    log('Cleaned session data', { cleanedSessions, cleanedMessages });
  }
}

/**
 * Cleans up stale entries from various Maps to prevent memory leaks
 */
function cleanupStaleMaps() {
  const now = Date.now();
  let cleaned = { streams: 0, queues: 0, oauth: 0, processes: 0, loginAttempts: 0, apiRate: 0 };
  
  // Clean up stale active streams (SSE connections that didn't close properly)
  for (const [messageId, streams] of activeStreams.entries()) {
    // If message is no longer running/queued, cleanup its streams
    let messageFound = false;
    for (const session of state.sessions) {
      const msg = session.messages?.find(m => m.id === messageId);
      if (msg) {
        messageFound = true;
        if (msg.status !== 'running' && msg.status !== 'queued') {
          // Message completed - close and remove streams
          if (streams instanceof Set) {
            for (const stream of streams) {
              try { stream.end(); } catch (_) {}
            }
          }
          activeStreams.delete(messageId);
          cleaned.streams++;
        }
        break;
      }
    }
    if (!messageFound) {
      // Message not found at all - cleanup streams
      if (streams instanceof Set) {
        for (const stream of streams) {
          try { stream.end(); } catch (_) {}
        }
      }
      activeStreams.delete(messageId);
      cleaned.streams++;
    }
  }
  
  // Clean up old session queues for deleted/inactive sessions
  for (const [sessionId, _queue] of sessionQueues.entries()) {
    const session = state.sessions.find(s => s.id === sessionId);
    if (!session || session.pending === 0) {
      // Check if there are any running messages
      const hasRunning = session?.messages?.some(m => m.status === 'running' || m.status === 'queued');
      if (!hasRunning) {
        sessionQueues.delete(sessionId);
        cleaned.queues++;
      }
    }
  }
  
  // Limit session queues size
  if (sessionQueues.size > MAX_SESSION_QUEUES) {
    const toDelete = sessionQueues.size - MAX_SESSION_QUEUES;
    let deleted = 0;
    for (const [sessionId] of sessionQueues.entries()) {
      if (deleted >= toDelete) break;
      const session = state.sessions.find(s => s.id === sessionId);
      if (!session || session.pending === 0) {
        sessionQueues.delete(sessionId);
        deleted++;
        cleaned.queues++;
      }
    }
  }
  
  // Clean up running processes map for processes that finished
  for (const [messageId, processInfo] of runningProcesses.entries()) {
    // Check if message is still running
    let stillRunning = false;
    for (const session of state.sessions) {
      const msg = session.messages?.find(m => m.id === messageId);
      if (msg && (msg.status === 'running' || msg.status === 'queued')) {
        stillRunning = true;
        break;
      }
    }
    
    if (!stillRunning) {
      runningProcesses.delete(messageId);
      cleaned.processes++;
    }
    
    // Also check for stale processes (running too long)
    if (processInfo.started && (now - processInfo.started) > STALE_PROCESS_TIMEOUT_MS) {
      log('Cleaning up stale process entry', { messageId, age: now - processInfo.started });
      runningProcesses.delete(messageId);
      cleaned.processes++;
    }
  }
  
  // Clean up expired OAuth state entries (deferred cleanup - handled here for Maps that grow unbounded)
  // Note: oauthStateStore is defined later, so we access it via typeof check
  if (typeof oauthStateStore !== 'undefined' && oauthStateStore instanceof Map) {
    for (const [key, entry] of oauthStateStore.entries()) {
      if (entry.expiresAt && entry.expiresAt < now) {
        oauthStateStore.delete(key);
        cleaned.oauth++;
      }
    }
    // Hard limit on OAuth states
    if (oauthStateStore.size > MAX_OAUTH_STATES) {
      const toDelete = oauthStateStore.size - MAX_OAUTH_STATES;
      let deleted = 0;
      for (const [key] of oauthStateStore.entries()) {
        if (deleted >= toDelete) break;
        oauthStateStore.delete(key);
        deleted++;
        cleaned.oauth++;
      }
    }
  }
  
  // Clean up old login attempt entries (older than 1 hour)
  const LOGIN_ATTEMPT_MAX_AGE = 60 * 60 * 1000; // 1 hour
  if (typeof loginAttempts !== 'undefined' && loginAttempts instanceof Map) {
    for (const [key, record] of loginAttempts.entries()) {
      const age = now - (record.windowStart || 0);
      if (age > LOGIN_ATTEMPT_MAX_AGE && (!record.lockedUntil || record.lockedUntil < now)) {
        loginAttempts.delete(key);
        cleaned.loginAttempts++;
      }
    }
    // Hard limit
    if (loginAttempts.size > MAX_LOGIN_ATTEMPTS_ENTRIES) {
      const toDelete = loginAttempts.size - MAX_LOGIN_ATTEMPTS_ENTRIES;
      let deleted = 0;
      for (const [key, record] of loginAttempts.entries()) {
        if (deleted >= toDelete) break;
        if (!record.lockedUntil || record.lockedUntil < now) {
          loginAttempts.delete(key);
          deleted++;
          cleaned.loginAttempts++;
        }
      }
    }
  }
  
  // Clean up old admin login attempt entries
  if (typeof adminLoginAttempts !== 'undefined' && adminLoginAttempts instanceof Map) {
    for (const [key, record] of adminLoginAttempts.entries()) {
      const age = now - (record.windowStart || 0);
      if (age > LOGIN_ATTEMPT_MAX_AGE && (!record.lockedUntil || record.lockedUntil < now)) {
        adminLoginAttempts.delete(key);
        cleaned.loginAttempts++;
      }
    }
  }
  
  // Clean up old API rate limit entries (older than rate limit window)
  const API_RATE_LIMIT_MAX_AGE = 60 * 60 * 1000; // 1 hour
  if (typeof apiRateLimit !== 'undefined' && apiRateLimit instanceof Map) {
    for (const [key, record] of apiRateLimit.entries()) {
      const age = now - (record.windowStart || 0);
      if (age > API_RATE_LIMIT_MAX_AGE) {
        apiRateLimit.delete(key);
        cleaned.apiRate++;
      }
    }
  }
  
  const totalCleaned = Object.values(cleaned).reduce((a, b) => a + b, 0);
  if (totalCleaned > 0) {
    log('Cleaned stale map entries', cleaned);
  }
}

/**
 * Cleans up orphaned child processes
 */
function cleanupOrphanedProcesses() {
  const now = Date.now();
  let killed = 0;
  
  for (const [processId, info] of childProcesses.entries()) {
    const age = now - info.startTime;
    
    // Kill processes running longer than timeout
    if (age > STALE_PROCESS_TIMEOUT_MS) {
      try {
        process.kill(info.pid, 'SIGTERM');
        setTimeout(() => {
          try { process.kill(info.pid, 'SIGKILL'); } catch (_) {}
        }, 5000);
        killed++;
        log('Killed orphaned process', { processId, pid: info.pid, age, sessionId: info.sessionId });
      } catch (err) {
        // Process may already be dead
        if (err.code !== 'ESRCH') {
          log('Failed to kill orphaned process', { processId, error: String(err) });
        }
      }
      childProcesses.delete(processId);
    }
  }
  
  if (killed > 0) {
    log('Cleaned up orphaned processes', { killed });
  }
}

/**
 * Truncates large outputs in completed messages to save memory
 */
function truncateLargeOutputs() {
  let truncated = 0;
  
  for (const session of state.sessions) {
    if (!Array.isArray(session.messages)) continue;
    
    for (const msg of session.messages) {
      // Only truncate completed messages
      if (msg.status !== 'done' && msg.status !== 'error') continue;
      
      // Truncate large reply
      if (msg.reply && msg.reply.length > PARTIAL_OUTPUT_MAX_LENGTH) {
        msg.reply = msg.reply.substring(0, PARTIAL_OUTPUT_MAX_LENGTH) + '\n\n[Output truncated due to length...]';
        truncated++;
      }
      
      // Remove partialOutput if we have reply
      if (msg.partialOutput && msg.reply) {
        delete msg.partialOutput;
        truncated++;
      }
      
      // Truncate large content
      if (msg.content && msg.content.length > PARTIAL_OUTPUT_MAX_LENGTH) {
        msg.content = msg.content.substring(0, PARTIAL_OUTPUT_MAX_LENGTH) + '\n\n[Content truncated...]';
        truncated++;
      }
    }
  }
  
  if (truncated > 0) {
    log('Truncated large message fields', { truncated });
  }
}

/**
 * Starts periodic memory cleanup
 */
function startMemoryCleanup() {
  if (memoryCleanupTimer) return;
  
  memoryCleanupTimer = setInterval(() => {
    if (isShuttingDown) return;
    triggerMemoryCleanup('periodic');
  }, MEMORY_CLEANUP_INTERVAL_MS);
  
  log('Memory cleanup scheduler started', { intervalMs: MEMORY_CLEANUP_INTERVAL_MS });
}

/**
 * Stops periodic memory cleanup
 */
function stopMemoryCleanup() {
  if (memoryCleanupTimer) {
    clearInterval(memoryCleanupTimer);
    memoryCleanupTimer = null;
  }
}

// ============================================================================
// Webhook Idempotency Protection
// ============================================================================

async function loadProcessedWebhooks() {
  try {
    await ensureStateFile();
    const raw = await fs.readFile(PROCESSED_WEBHOOKS_FILE, 'utf8').catch(() => null);
    if (raw) {
      const parsed = JSON.parse(raw);
      if (parsed && typeof parsed === 'object') {
        const now = Date.now();
        // Clean up old entries on load
        for (const [eventId, data] of Object.entries(parsed)) {
          if (data && data.processedAt && (now - data.processedAt) < PROCESSED_WEBHOOKS_MAX_AGE_MS) {
            processedWebhookEvents.set(eventId, data);
          }
        }
        log('Loaded processed webhooks', { count: processedWebhookEvents.size });
      }
    }
  } catch (error) {
    log('Failed to load processed webhooks', { error: String(error) });
  }
}

async function persistProcessedWebhooks() {
  try {
    await ensureStateFile();
    const now = Date.now();
    const obj = {};
    // Only persist recent entries
    for (const [eventId, data] of processedWebhookEvents.entries()) {
      if (data && data.processedAt && (now - data.processedAt) < PROCESSED_WEBHOOKS_MAX_AGE_MS) {
        obj[eventId] = data;
      }
    }
    await safeWriteFile(PROCESSED_WEBHOOKS_FILE, JSON.stringify(obj, null, 2));
  } catch (error) {
    log('Failed to persist processed webhooks', { error: String(error) });
  }
}

function isWebhookProcessed(eventId) {
  if (!eventId) return false;
  return processedWebhookEvents.has(eventId);
}

async function markWebhookProcessed(eventId, eventType) {
  if (!eventId) return;
  processedWebhookEvents.set(eventId, {
    processedAt: Date.now(),
    type: eventType || 'unknown'
  });
  
  // Debounce persistence
  if (processedWebhooksPersistTimer) {
    clearTimeout(processedWebhooksPersistTimer);
  }
  processedWebhooksPersistTimer = setTimeout(() => {
    persistProcessedWebhooks().catch(err => {
      log('Failed to persist processed webhooks (debounced)', { error: String(err) });
    });
  }, 1000);
}

/**
 * Registers a child process for tracking
 * @param {string} processId - Unique process identifier
 * @param {number} pid - Process ID
 * @param {string} sessionId - Associated session ID
 * @param {string} messageId - Associated message ID
 */
function registerChildProcess(processId, pid, sessionId, messageId) {
  childProcesses.set(processId, {
    pid,
    startTime: Date.now(),
    sessionId,
    messageId
  });
}

/**
 * Unregisters a child process
 * @param {string} processId - Process identifier
 */
function unregisterChildProcess(processId) {
  childProcesses.delete(processId);
}

// ============================================================================
// OpenCode Process Manager - Singleton for managing a single OpenCode instance
// ============================================================================
class OpencodeProcessManager {
  constructor() {
    this.process = null;
    this.isReady = false;
    this.pendingRequests = new Map(); // messageId -> { resolve, reject, timeout }
    this.heartbeatInterval = null;
    this.lastActivity = Date.now();
    this.sessionWorkspaces = new Map(); // sessionId -> workspaceDir
  }

  async start() {
    if (this.process) {
      log('OpenCode process manager already running');
      return;
    }

    log('Starting OpenCode process manager singleton...');
    
    try {
      const cliCommand = resolveCliCommand('opencode');
      
      // Verify CLI exists
      try {
        fsSync.accessSync(cliCommand, fsSync.constants.X_OK);
      } catch (err) {
        throw new Error(`OpenCode CLI not found: ${cliCommand}`);
      }

      // Start OpenCode in server/daemon mode if supported
      // Otherwise, we'll continue using per-message spawning
      // Check if OpenCode supports a server/daemon mode
      const serverArgs = this.getServerModeArgs();
      
      if (serverArgs) {
        log('Starting OpenCode in server mode', { args: serverArgs });
        this.process = spawn(cliCommand, serverArgs, {
          stdio: ['pipe', 'pipe', 'pipe'],
          env: { ...process.env }
        });

        this.process.stdout.on('data', (data) => this.handleStdout(data));
        this.process.stderr.on('data', (data) => this.handleStderr(data));
        this.process.on('error', (err) => this.handleError(err));
        this.process.on('exit', (code) => this.handleExit(code));

        // Wait for ready signal
        await this.waitForReady();
        
        // Start heartbeat to keep process alive
        this.startHeartbeat();
        
        log('OpenCode process manager started successfully');
      } else {
        log('OpenCode does not support server mode, will use per-session approach');
        // We'll track sessions instead of using a single process
        this.isReady = true;
      }
    } catch (err) {
      log('Failed to start OpenCode process manager', { error: String(err) });
      this.process = null;
      this.isReady = false;
      throw err;
    }
  }

  getServerModeArgs() {
    // Check if OpenCode supports server/daemon mode
    // This would need to be customized based on actual OpenCode CLI capabilities
    // For now, we return null to indicate no server mode support
    // If OpenCode adds 'serve' or 'daemon' command, update this
    return null;
  }

  async waitForReady(timeout = 10000) {
    return new Promise((resolve, reject) => {
      const timer = setTimeout(() => {
        reject(new Error('OpenCode process failed to become ready'));
      }, timeout);

      const checkReady = () => {
        if (this.isReady) {
          clearTimeout(timer);
          resolve();
        } else {
          setTimeout(checkReady, 100);
        }
      };
      checkReady();
    });
  }

  handleStdout(data) {
    const text = data.toString();
    log('OpenCode process stdout', { text: text.slice(0, 200) });
    
    // Check for ready signal
    if (text.includes('ready') || text.includes('listening')) {
      this.isReady = true;
    }
    
    // Process responses for pending requests
    this.processOutput(text);
  }

  handleStderr(data) {
    const text = data.toString();
    log('OpenCode process stderr', { text: text.slice(0, 200) });
  }

  handleError(err) {
    log('OpenCode process error', { error: String(err) });
    this.isReady = false;
    this.rejectAllPending(err);
  }

  handleExit(code) {
    log('OpenCode process exited', { code });
    this.isReady = false;
    this.process = null;
    this.rejectAllPending(new Error(`OpenCode process exited with code ${code}`));
    
    // Auto-restart if not shutting down
    if (!isShuttingDown) {
      log('Auto-restarting OpenCode process...');
      setTimeout(() => this.start().catch(err => 
        log('Failed to restart OpenCode', { error: String(err) })
      ), 5000);
    }
  }

  startHeartbeat() {
    this.heartbeatInterval = setInterval(() => {
      if (!this.process || !this.isReady) return;
      
      // Send a lightweight heartbeat command
      // This keeps the process alive and checks responsiveness
      const idleTime = Date.now() - this.lastActivity;
      
      // If idle for more than 5 minutes, send a ping
      if (idleTime > 300000) {
        log('Sending heartbeat to OpenCode process', { idleTime });
        // Could send a simple command like 'version' or 'status'
      }
    }, 60000); // Every minute
  }

  stopHeartbeat() {
    if (this.heartbeatInterval) {
      clearInterval(this.heartbeatInterval);
      this.heartbeatInterval = null;
    }
  }

  processOutput(text) {
    // Parse output and route to appropriate pending request
    // This would need to be customized based on OpenCode's output format
    this.lastActivity = Date.now();
  }

  rejectAllPending(error) {
    for (const [messageId, request] of this.pendingRequests.entries()) {
      if (request.timeout) clearTimeout(request.timeout);
      request.reject(error);
    }
    this.pendingRequests.clear();
  }

  async executeInSession(sessionId, workspaceDir, command, args, options = {}) {
    if (!this.isReady && !this.process) {
      // Fall back to per-message spawning if server mode not available
      return this.executeStandalone(workspaceDir, command, args, options);
    }

    // Track workspace for this session
    this.sessionWorkspaces.set(sessionId, workspaceDir);
    this.lastActivity = Date.now();

    // Send command to persistent process
    // This would need protocol implementation based on OpenCode's capabilities
    // For now, fall back to standalone execution
    return this.executeStandalone(workspaceDir, command, args, options);
  }

  async executeStandalone(workspaceDir, command, args, options) {
    // Execute as a standalone process (current behavior)
    // But track it so we can see when multiple processes are running
    const processId = randomUUID();
    const startTime = Date.now();
    
    log('Executing OpenCode command (standalone)', { 
      processId, 
      command, 
      args,
      workspaceDir,
      activeProcesses: runningProcesses.size 
    });

    try {
      const result = await runCommand(command, args, {
        ...options,
        cwd: workspaceDir
      });
      
      const duration = Date.now() - startTime;
      log('OpenCode command completed', { processId, duration, activeProcesses: runningProcesses.size });
      
      return result;
    } catch (err) {
      const duration = Date.now() - startTime;
      log('OpenCode command failed', { processId, duration, error: String(err) });
      throw err;
    }
  }

  async stop() {
    log('Stopping OpenCode process manager...');
    this.stopHeartbeat();
    this.isReady = false;

    if (this.process) {
      this.process.kill('SIGTERM');
      
      // Wait for graceful shutdown
      await new Promise((resolve) => {
        const timeout = setTimeout(() => {
          if (this.process) {
            log('Force killing OpenCode process');
            this.process.kill('SIGKILL');
          }
          resolve();
        }, 5000);
        
        if (this.process) {
          this.process.once('exit', () => {
            clearTimeout(timeout);
            resolve();
          });
        } else {
          clearTimeout(timeout);
          resolve();
        }
      });
      
      this.process = null;
    }
    
    this.sessionWorkspaces.clear();
    log('OpenCode process manager stopped');
  }

  getStats() {
    return {
      isRunning: !!this.process,
      isReady: this.isReady,
      pendingRequests: this.pendingRequests.size,
      activeSessions: this.sessionWorkspaces.size,
      lastActivity: this.lastActivity,
      idleTime: Date.now() - this.lastActivity
    };
  }
}

// Global singleton instance
const opencodeManager = new OpencodeProcessManager();

// ============================================================================
// End OpenCode Process Manager
// ============================================================================

async function safeWriteFile(filePath, data, maxRetries = 3) {
  const tempPath = filePath + '.tmp';
  const parentDir = path.dirname(filePath);
  
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      await fs.mkdir(parentDir, { recursive: true });
      await fs.writeFile(tempPath, data, 'utf8');
      await fs.rename(tempPath, filePath);
      return;
    } catch (error) {
      if (error.code === 'ENOENT' && attempt < maxRetries) {
        log(`safeWriteFile retry ${attempt}/${maxRetries} for ${path.basename(filePath)}`, { error: String(error) });
        await delay(50 * attempt);
        continue;
      }
      log(`safeWriteFile failed for ${path.basename(filePath)}`, { error: String(error), attempt });
      throw error;
    }
  }
}

async function persistAllState() {
  const persistFunctions = [
    persistState,
    persistAdminModels,
    persistOpenRouterSettings,
    persistMistralSettings,
    persistPlanSettings,
    persistPlanTokenLimits,
    persistProviderLimits,
    persistProviderUsage,
    persistTokenUsage,
    persistTopupSessions,
    persistPendingTopups,
    persistPaygSessions,
    persistPendingPayg,
    persistUsersDb,
    persistAffiliatesDb,
    persistFeatureRequestsDb,
    persistContactMessagesDb,
  ];
  for (const persistFn of persistFunctions) {
    try {
      await persistFn();
    } catch (error) {
      log(`Failed to persist ${persistFn.name}`, { error: String(error) });
    }
  }
}

async function gracefulShutdown(signal) {
  if (isShuttingDown) return;
  isShuttingDown = true;
  log(`Received ${signal}, starting graceful shutdown with session preservation...`);
  
  // Stop periodic tasks first
  stopAutoSave();
  stopMemoryCleanup();
  
  // Notify all active SSE connections about the restart
  for (const [messageId, streams] of activeStreams.entries()) {
    try {
      if (streams instanceof Set) {
        for (const stream of streams) {
          try {
            stream.write(`data: ${JSON.stringify({ type: 'server-restart', message: 'Server is restarting, your session will be restored automatically...' })}\n\n`);
            stream.end();
          } catch (_) {}
        }
      } else if (streams && streams.write) {
        streams.write(`data: ${JSON.stringify({ type: 'server-restart', message: 'Server is restarting, your session will be restored automatically...' })}\n\n`);
      }
    } catch (e) {
      // Stream may already be closed, ignore
    }
  }
  activeStreams.clear();
  
  // Kill all tracked child processes
  log('Terminating child processes...');
  for (const [processId, info] of childProcesses.entries()) {
    try {
      process.kill(info.pid, 'SIGTERM');
    } catch (_) {
      // Process may already be dead
    }
  }
  // Give them a moment to terminate gracefully
  await delay(1000);
  // Force kill any remaining
  for (const [processId, info] of childProcesses.entries()) {
    try {
      process.kill(info.pid, 'SIGKILL');
    } catch (_) {}
  }
  childProcesses.clear();
  runningProcesses.clear();
  sessionQueues.clear();
  
  log('Stopping OpenCode process manager...');
  await opencodeManager.stop();
  
  log('Persisting all state with session continuity info...');
  await persistAllState();
  
  if (server) {
    log('Closing HTTP server...');
    await new Promise((resolve) => server.close(resolve));
  }
  
  log('Graceful shutdown complete, sessions preserved for restoration');
  process.exit(0);
}

function startAutoSave() {
  autoSaveTimer = setInterval(async () => {
    if (isShuttingDown) return;
    try {
      await persistAllState();
    } catch (error) {
      log('Auto-save failed', { error: String(error) });
    }
  }, AUTO_SAVE_INTERVAL_MS);
}

function stopAutoSave() {
  if (autoSaveTimer) {
    clearInterval(autoSaveTimer);
    autoSaveTimer = null;
  }
}
let cachedModels = new Map();
let cachedModelsAt = new Map();
// Cache for models.dev provider data
let cachedModelsDevProviders = new Map();
let cachedModelsDevProvidersAt = new Map();
const MODELS_DEV_CACHE_TTL = 3600000; // 1 hour
const adminSessions = new Map();
let adminModels = [];
let adminModelIndex = new Map();
// Simple model structure - just two lists
let opencodeModels = []; // Models from OpenCode - order determines fallback chain
let publicModels = []; // Public-facing models (completely separate)
let openrouterSettings = {
  primaryModel: OPENROUTER_MODEL_PRIMARY,
  backupModel1: OPENROUTER_MODEL_BACKUP_1,
  backupModel2: OPENROUTER_MODEL_BACKUP_2,
  backupModel3: OPENROUTER_MODEL_BACKUP_3,
};
let mistralSettings = {
  primaryModel: MISTRAL_MODEL_PRIMARY,
  backupModel1: MISTRAL_MODEL_BACKUP_1,
  backupModel2: MISTRAL_MODEL_BACKUP_2,
  backupModel3: MISTRAL_MODEL_BACKUP_3,
};
const PLANNING_PROVIDERS = ['openrouter', 'mistral', 'google', 'groq', 'nvidia', 'ollama', 'cohere', 'kilo'];
let planSettings = {
  provider: 'openrouter', // legacy field, retained for backwards compatibility
  freePlanModel: '',
  planningChain: [], // [{ provider, model }]
};
let providerLimits = {
  limits: {},
  modelProviders: {},
  opencodeBackupModel: '',
};
let pendingProviderPersistTimer = null;
let providerUsage = {};
let tokenUsage = {};
let processedTopups = {};
let pendingTopups = {};
let externalTestUsage = {};

const externalWpTester = createExternalWpTester({ logger: log });
let processedPayg = {};
let pendingPayg = {};
let processedSubscriptions = {};
let pendingSubscriptions = {};
let dodoProductCache = {
  fetchedAt: 0,
  items: [],
  byId: new Map(),
};
const userSessions = new Map(); // Track user sessions
const loginAttempts = new Map(); // { email:ip: { count, windowStart, lockedUntil } }
const adminLoginAttempts = new Map(); // { ip: { count, windowStart, lockedUntil } }
const apiRateLimit = new Map(); // { userId: { requests, windowStart } }
const csrfTokens = new Map(); // { token: { userId, expiresAt } }
let usersDb = []; // In-memory user database cache
let invoicesDb = []; // In-memory invoice database cache
let mailTransport = null;

function summarizeMailConfig() {
  return {
    configured: !!MAILPILOT_TOKEN,
    url: MAILPILOT_URL,
    tokenSet: !!MAILPILOT_TOKEN,
  };
}

function ensureMailTransport() {
  if (mailTransport) return mailTransport;

  if (!MAILPILOT_TOKEN) {
    console.log('');
    console.log('┌─────────────────────────────────────────────────────────────┐');
    console.log('│  📧  MAILPILOT NOT CONFIGURED                              │');
    console.log('├─────────────────────────────────────────────────────────────┤');
    console.log('│  Password reset and verification emails will NOT be sent.   │');
    console.log('│  To enable real emails, configure MailPilot in .env file:   │');
    console.log('│                                                             │');
    console.log('│    MAILPILOT_URL=https://emailmarketing.modelrailway3d.co.uk│');
    console.log('│    MAILPILOT_TOKEN=tx_abc123def456...                       │');
    console.log('│                                                             │');
    console.log('│  💡 Tip: Emails will be logged below when triggered          │');
    console.log('└─────────────────────────────────────────────────────────────┘');
    console.log('');
    mailTransport = {
      sendMail: async (payload) => {
        console.log('');
        console.log('┌─────────────────────────────────────────────────────────────┐');
        console.log('│  📧  [MOCK EMAIL - Not Sent]                                │');
        console.log('├─────────────────────────────────────────────────────────────┤');
        console.log(`│  To:    ${payload.to}`);
        console.log(`│  Subject: ${payload.subject}`);
        console.log('│  Body preview:');
        const preview = (payload.text || payload.html || '').slice(0, 200).replace(/\n/g, ' ');
        console.log(`│    ${preview}...`);
        console.log('│                                                             │');
        console.log('│  Configure MAILPILOT_TOKEN in .env to send real emails      │');
        console.log('└─────────────────────────────────────────────────────────────┘');
        console.log('');
        return { messageId: 'mock-' + Date.now() };
      },
      verify: (cb) => cb(null, true)
    };
    return mailTransport;
  }

  log('MailPilot configured', summarizeMailConfig());
  mailTransport = { url: MAILPILOT_URL, token: MAILPILOT_TOKEN };
  return mailTransport;
}

async function sendEmail({ to, subject, text, html }) {
  try {
    const transport = ensureMailTransport();
    
    if (!MAILPILOT_TOKEN) {
      await transport.sendMail({ to, subject, text, html });
      return { messageId: 'mock-' + Date.now() };
    }

    const payload = {
      to,
      subject,
      html: html || text || '',
    };

    log('sending email via MailPilot', { to, subject });
    
    const response = await fetch(`${MAILPILOT_URL}/api/transactional-emails/trigger/${MAILPILOT_TOKEN}`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(payload),
    });

    if (!response.ok) {
      const errorText = await response.text();
      throw new Error(`MailPilot API error: ${response.status} - ${errorText}`);
    }

    const result = await response.json();
    log('email sent successfully via MailPilot', { to, status: result.status });
    return { messageId: result.status || 'sent' };
  } catch (err) {
    log('FAILED TO SEND EMAIL', {
      to,
      subject,
      error: String(err),
      hint: 'Check MAILPILOT_URL and MAILPILOT_TOKEN configuration in .env file'
    });
    throw err;
  }
}
let trackingPersistTimer = null;
let featureRequestsDb = [];
let contactMessagesDb = [];

// Security Configuration with Sensible Defaults
const ADMIN_LOGIN_RATE_LIMIT = Number(process.env.ADMIN_LOGIN_RATE_LIMIT || 5); // attempts per minute
const USER_LOGIN_RATE_LIMIT = Number(process.env.USER_LOGIN_RATE_LIMIT || 10);
const API_RATE_LIMIT = Number(process.env.API_RATE_LIMIT || 100); // requests per minute
const MAX_PROMPT_LENGTH = Number(process.env.MAX_PROMPT_LENGTH || 10000);
const LOGIN_LOCKOUT_MS = Number(process.env.LOGIN_LOCKOUT_MS || 900000); // 15 minutes
const RATE_LIMIT_WINDOW_MS = 60000; // 1 minute window

// Admin password hashing
let adminPasswordHash = null;

function log(message, extra) {
  const payload = extra ? `${message} ${JSON.stringify(extra)}` : message;
  console.log(`[${new Date().toISOString()}] ${payload}`);
}

// Lowercase identifiers and collapse unsafe characters into hyphens for safe path segments
function sanitizeSegment(value, fallback = '') {
  if (!value || typeof value !== 'string') return fallback || '';
  const clean = value.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/-+/g, '-').replace(/^-+|-+$/g, '');
  const safe = clean || fallback || '';
  return safe.slice(0, 120);
}

function sanitizeRedirectPath(rawPath, fallback = '/apps') {
  const value = (rawPath || '').trim();
  if (value && value.startsWith('/') && !value.startsWith('//')) return value;
  return fallback;
}

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

function getResourceUsageSnapshot() {
  const mem = process.memoryUsage();
  const load = os.loadavg();
  return {
    rss: mem.rss,
    heapUsed: mem.heapUsed,
    load1: load[0] || 0,
    running: runningProcesses.size,
  };
}

async function waitForResources(reasonId = '') {
  const softMemory = RESOURCE_LIMITS.memoryBytes ? RESOURCE_LIMITS.memoryBytes * RESOURCE_MEMORY_SOFT_RATIO : 0;
  const concurrencyLimit = OPENCODE_MAX_CONCURRENCY > 0
    ? OPENCODE_MAX_CONCURRENCY
    : (RESOURCE_LIMITS.cpuCores > 0 ? Math.max(1, Math.ceil(RESOURCE_LIMITS.cpuCores)) : 2);
  const loadLimit = RESOURCE_LIMITS.cpuCores > 0 ? Math.max(RESOURCE_LIMITS.cpuCores * RESOURCE_CPU_SOFT_RATIO, RESOURCE_MIN_LOAD_FLOOR) : Infinity;
  let lastLog = 0;
  let waitMs = RESOURCE_CHECK_INTERVAL_MS;
  const maxWaitMs = RESOURCE_CHECK_INTERVAL_MS * 8;
  const startTime = Date.now();

  // Messages wait indefinitely for resources - never skip/timeout
  // This ensures no messages are lost due to temporary resource constraints
  while (true) {
    // Attempt memory cleanup if we're under pressure
    const waitTime = Date.now() - startTime;
    if (waitTime > 5000 && waitTime % 10000 < waitMs) {
      // Every ~10 seconds of waiting, try to free memory
      triggerMemoryCleanup('resource_wait');
    }
    
    const usage = getResourceUsageSnapshot();
    const memoryOk = !softMemory || usage.rss < softMemory;
    // Combine active processes and pending reservations so we don't schedule beyond the allowed concurrency.
    const currentActive = usage.running + resourceReservations;
    const projectedConcurrency = currentActive + 1;
    const concurrencyOk = projectedConcurrency <= concurrencyLimit;
    const loadOk = loadLimit === Infinity || usage.load1 <= loadLimit;

    if (memoryOk && concurrencyOk && loadOk) {
      let released = false;
      resourceReservations += 1;
      if (waitTime > 1000) {
        log('resource guard acquired after wait', { reasonId, waitTime, rss: usage.rss });
      }
      // Caller must invoke release; processMessage wraps usage in a finally block to avoid leaks.
      return () => {
        if (released) return;
        released = true;
        resourceReservations = Math.max(0, resourceReservations - 1);
      };
    }

    const now = Date.now();
    if (now - lastLog > RESOURCE_WAIT_LOG_INTERVAL_MS) {
      lastLog = now;
      const reason = !memoryOk ? 'memory_pressure' : (!concurrencyOk ? 'concurrency_limit' : 'cpu_load');
      log('resource guard waiting (message queued)', {
        reasonId,
        reason,
        rss: usage.rss,
        heapUsed: usage.heapUsed,
        load1: usage.load1,
        running: usage.running,
        memoryLimit: RESOURCE_LIMITS.memoryBytes,
        cpuLimit: RESOURCE_LIMITS.cpuCores,
        concurrencyLimit,
        softMemory,
        reservations: resourceReservations,
        waitTime,
        queuedMessages: getQueuedMessageCount()
      });
    }
    await delay(waitMs);
    // Gradually increase wait time to reduce CPU spinning, but cap it
    waitMs = Math.min(maxWaitMs, Math.max(RESOURCE_CHECK_INTERVAL_MS, waitMs * 1.2));
  }
}

// Helper to count queued messages for logging
function getQueuedMessageCount() {
  let count = 0;
  for (const session of state.sessions) {
    if (session.messages) {
      count += session.messages.filter(m => m.status === 'queued' || m.status === 'running').length;
    }
  }
  return count;
}

function resolveBaseUrl(req) {
  if (PUBLIC_BASE_URL) return PUBLIC_BASE_URL.replace(/\/+$/, '');
  const hostHeader = (req && req.headers && req.headers.host) ? req.headers.host : `${HOST}:${PORT}`;
  const proto = (req && req.headers && req.headers['x-forwarded-proto'] === 'https') ? 'https' : 'http';
  return `${proto}://${hostHeader}`;
}

function buildRedirectUri(req, provider) {
  const base = resolveBaseUrl(req);
  const safeProvider = (provider || '').toLowerCase();
  return `${base}/auth/${safeProvider}/callback`;
}

function createOAuthState(next, provider, remember = false) {
  // Clean up expired entries opportunistically
  const now = Date.now();
  for (const [key, entry] of oauthStateStore.entries()) {
    if (entry.expiresAt && entry.expiresAt < now) oauthStateStore.delete(key);
  }
  const state = randomUUID();
  oauthStateStore.set(state, {
    provider: (provider || '').toLowerCase(),
    next: sanitizeRedirectPath(next),
    remember: Boolean(remember),
    expiresAt: now + OAUTH_STATE_TTL_MS,
  });
  return state;
}

function consumeOAuthState(state, provider) {
  if (!state) return null;
  const entry = oauthStateStore.get(state);
  oauthStateStore.delete(state);
  if (!entry) return null;
  if (entry.provider !== (provider || '').toLowerCase()) return null;
  if (entry.expiresAt && entry.expiresAt < Date.now()) return null;
  return entry;
}

function decodeJwtPayload(token) {
  try {
    const parts = token.split('.');
    if (parts.length < 2) return {};
    const payload = Buffer.from(parts[1], 'base64').toString('utf8');
    return JSON.parse(payload);
  } catch (_) {
    return {};
  }
}

function escapeHtml(str) {
  return String(str || '')
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#39;')
    .replace(/`/g, '&#96;')
    .replace(/\//g, '&#47;');
}

// Security: Rate Limiting Functions
function checkLoginRateLimit(key, maxAttempts, attemptsMap) {
  const now = Date.now();
  const record = attemptsMap.get(key) || { count: 0, windowStart: now, lockedUntil: null };

  // Check if locked out
  if (record.lockedUntil && record.lockedUntil > now) {
    return { blocked: true, locked: true, retryAfter: Math.ceil((record.lockedUntil - now) / 1000) };
  }

  // Reset window if expired
  if (now - record.windowStart > RATE_LIMIT_WINDOW_MS) {
    record.count = 0;
    record.windowStart = now;
  }

  record.count++;

  // Lock out if too many attempts
  if (record.count > maxAttempts) {
    record.lockedUntil = now + LOGIN_LOCKOUT_MS;
    log('account locked due to failed attempts', { key, attempts: record.count });
  }

  attemptsMap.set(key, record);

  return {
    blocked: record.count > maxAttempts,
    locked: false,
    attempts: record.count,
    remaining: Math.max(0, maxAttempts - record.count)
  };
}

function checkApiRateLimit(userId, maxRequests = API_RATE_LIMIT) {
  const now = Date.now();
  const record = apiRateLimit.get(userId) || { requests: 0, windowStart: now };

  if (now - record.windowStart > RATE_LIMIT_WINDOW_MS) {
    record.requests = 0;
    record.windowStart = now;
  }

  record.requests++;
  apiRateLimit.set(userId, record);

  const remaining = Math.max(0, maxRequests - record.requests);
  const resetIn = Math.ceil((record.windowStart + RATE_LIMIT_WINDOW_MS - now) / 1000);

  return {
    limited: record.requests > maxRequests,
    remaining,
    resetIn,
    limit: maxRequests
  };
}

function sendRateLimitExceeded(res, retryAfter = 60, limit = API_RATE_LIMIT) {
  res.writeHead(429, {
    'Content-Type': 'application/json',
    'Retry-After': retryAfter,
    'X-RateLimit-Limit': limit,
    'X-RateLimit-Remaining': '0',
    'X-RateLimit-Reset': retryAfter
  });
  res.end(JSON.stringify({
    error: 'Rate limit exceeded',
    message: 'Too many requests. Please try again later.',
    retryAfter,
    limit
  }));
}

// Security: CSRF Token Functions
function generateCsrfToken(userId) {
  const token = randomUUID();
  csrfTokens.set(token, { userId, expiresAt: Date.now() + 3600000 }); // 1 hour
  return token;
}

function validateCsrfToken(token, userId) {
  const record = csrfTokens.get(token);
  if (!record) return false;
  if (record.expiresAt < Date.now()) return false;
  if (record.userId !== userId) return false;
  return true;
}

// Security: Honeypot Detection
function checkHoneypot(body) {
  return !!(body.website && body.website.length > 0);
}

// Security: Prompt Injection Protection - Comprehensive
function sanitizePromptInput(input, options = {}) {
  if (!input || typeof input !== 'string') return '';

  // Use the comprehensive security module
  const result = security.sanitizeUserInput(input, {
    strictMode: options.strictMode || false,
    maxLength: options.maxLength || MAX_PROMPT_LENGTH,
    allowMarkup: options.allowMarkup || false,
    logViolations: options.logViolations !== false // default true
  });

  // If blocked, return empty string (will be handled by caller)
  if (result.blocked) {
    return '[BLOCKED]';
  }

  return result.sanitized;
}

// Security: Check if input should be blocked (for pre-validation)
function shouldBlockUserInput(input) {
  if (!input || typeof input !== 'string') return { blocked: false };
  return security.shouldBlockInput(input);
}

// Security: Output Validation
function sanitizeAiOutput(output) {
  if (!output || typeof output !== 'string') return '';

  const patterns = [
    /api[_-]?key["']?\s*[:=]\s*["']?[a-zA-Z0-9_-]{20,}["']?/gi,
    /password["']?\s*[:=]\s*["']?[^"'\s]{8,}["']?/gi,
    /Bearer\s+[a-zA-Z0-9\-\._~\+\/]+=*/gi,
    /AWS_ACCESS_KEY_ID[^\s]*/gi,
    /AWS_SECRET_ACCESS_KEY[^\s]*/gi,
  ];

  let result = output;
  for (const pattern of patterns) {
    result = result.replace(pattern, '[REDACTED]');
  }

  return result;
}

// Security: Password Validation
function validatePassword(password) {
  const errors = [];
  if (!password || password.length < 12) errors.push('Minimum 12 characters');
  if (!/[A-Z]/.test(password)) errors.push('Uppercase letter required');
  if (!/[a-z]/.test(password)) errors.push('Lowercase letter required');
  if (!/[0-9]/.test(password)) errors.push('Number required');
  if (!/[!@#$%^&*(),.?":{}|<>]/.test(password)) errors.push('Special character required');
  return { valid: errors.length === 0, errors };
}

// Security: Git Action Validation
const VALID_GIT_ACTIONS = new Set(['pull', 'push', 'sync', 'status', 'log', 'fetch', 'commit', 'checkout', 'branch', 'init', 'clone', 'add', 'reset', 'restore']);

function validateGitAction(action) {
  return VALID_GIT_ACTIONS.has(action?.toLowerCase());
}

// Security: Model Validation
const ALLOWED_MODELS = new Set();

function isModelAllowed(model) {
  if (!model) return false;
  if (ALLOWED_MODELS.has(model)) return true;
  if (getAdminModelByIdOrName(model)) return true;
  return false;
}

// Security: Host Header Validation
function validateHostHeader(host) {
  if (!host || typeof host !== 'string') return false;
  if (host.length > 256) return false;
  const validPattern = /^([a-zA-Z0-9][a-zA-Z0-9.-]*[a-zA-Z0-9]|\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})(:\d+)?$/;
  return validPattern.test(host);
}

// Security: User Agent Validation
const SUSPICIOUS_USER_AGENTS = [
  /bot/i, /crawler/i, /spider/i, /curl/i, /wget/i, /python/i,
  /httpclient/i, /java\//i, /libwww/i, /lwp/i, /fetch/i
];

function isSuspiciousUserAgent(ua) {
  if (!ua) return true;
  return SUSPICIOUS_USER_AGENTS.some(pattern => pattern.test(ua));
}

// Security: Git Commit Message Sanitization
function sanitizeGitMessage(message) {
  if (!message || typeof message !== 'string') return '';
  return message
    .replace(/[\r\n]/g, ' ')
    .replace(/[^a-zA-Z0-9\s\-_.,!?'"()[\]{}@#$%^&*+=\\/]/g, '')
    .slice(0, 500)
    .trim();
}

function summarizeMailConfig() {
  return {
    configured: !!MAILPILOT_TOKEN,
    url: MAILPILOT_URL,
    tokenSet: !!MAILPILOT_TOKEN,
  };
}

function readUserIdFromCookie(req) {
  try {
    const cookieHeader = req?.headers?.cookie || '';
    if (!cookieHeader) return '';
    const parts = cookieHeader.split(';').map((p) => p.trim());
    const match = parts.find((p) => p.startsWith('chat_user='));
    if (!match) return '';
    // slice/join preserves cookie values that legitimately contain '=' characters
    const raw = match.split('=').slice(1).join('=') || '';
    return decodeURIComponent(raw);
  } catch (_) {
    return '';
  }
}

function resolveUserId(req, url) {
  // First try the new user session system
  const userSession = getUserSession(req);
  if (userSession && userSession.userId) {
    return userSession.userId;
  }

  // Fall back to the old system for backwards compatibility
  const cookieUser = readUserIdFromCookie(req);
  const headerUser = (req?.headers?.['x-user-id'] || req?.headers?.['x-user'] || req?.headers?.['x-user-email'] || '').toString();
  const resolved = cookieUser || headerUser;
  const sanitized = sanitizeSegment(resolved, '');
  return sanitized;
}

function requireUserId(req, res, url) {
  const userId = resolveUserId(req, url);
  if (!userId) {
    sendJson(res, 401, { error: 'User identity required' });
    return null;
  }
  return userId;
}

function readAdminSessionToken(req) {
  try {
    const cookieHeader = req?.headers?.cookie || '';
    if (!cookieHeader) return '';
    const parts = cookieHeader.split(';').map((p) => p.trim());
    const match = parts.find((p) => p.startsWith(`${ADMIN_COOKIE_NAME}=`));
    if (!match) return '';
    return decodeURIComponent(match.split('=').slice(1).join('=') || '');
  } catch (_) {
    return '';
  }
}

function getAdminSession(req) {
  const token = readAdminSessionToken(req);
  if (!token) return null;
  const session = adminSessions.get(token);
  if (!session) return null;
  if (session.expiresAt && session.expiresAt < Date.now()) {
    adminSessions.delete(token);
    return null;
  }
  return { token, expiresAt: session.expiresAt };
}

function startAdminSession(res) {
  const token = randomUUID();
  const expiresAt = Date.now() + ADMIN_SESSION_TTL_MS;
  adminSessions.set(token, { expiresAt });
  const parts = [
    `${ADMIN_COOKIE_NAME}=${encodeURIComponent(token)}`,
    'Path=/',
    'HttpOnly',
    'SameSite=Lax',
    `Max-Age=${Math.floor(ADMIN_SESSION_TTL_MS / 1000)}`,
  ];
  if (process.env.COOKIE_SECURE === '0') parts.push('Secure');
  res.setHeader('Set-Cookie', parts.join('; '));
  return token;
}

function clearAdminSession(res) {
  res.setHeader('Set-Cookie', `${ADMIN_COOKIE_NAME}=deleted; Path=/; Max-Age=0; SameSite=Lax`);
}

function requireAdminAuth(req, res) {
  const session = getAdminSession(req);
  if (!session) {
    sendJson(res, 401, { error: 'Admin login required' });
    return null;
  }
  return session;
}

// User authentication functions
async function loadUsersDb() {
  let shouldPersist = false;
  try {
    await ensureStateFile();
    await fs.access(USERS_DB_FILE);
    const raw = await fs.readFile(USERS_DB_FILE, 'utf8');
    const parsed = JSON.parse(raw || '[]');
    const rawUsers = Array.isArray(parsed) ? parsed : [];
    usersDb = rawUsers.map((user) => {
      const verification = normalizeVerificationState(user);
      if (verification.shouldPersist) shouldPersist = true;
      const normalizedPlan = normalizePlanSelection(user?.plan) || DEFAULT_PLAN;
      return {
        ...user,
        providers: Array.isArray(user?.providers) ? user.providers : [],
        emailVerified: verification.verified,
        verificationToken: verification.verificationToken,
        verificationExpiresAt: verification.verificationExpiresAt,
        resetToken: user?.resetToken || '',
        resetExpiresAt: user?.resetExpiresAt || null,
        plan: normalizedPlan,
        billingStatus: user?.billingStatus || DEFAULT_BILLING_STATUS,
        billingEmail: user?.billingEmail || user?.email || '',
        paymentMethodLast4: user?.paymentMethodLast4 || '',
        subscriptionRenewsAt: user?.subscriptionRenewsAt || null,
        referredByAffiliateCode: sanitizeAffiliateCode(user?.referredByAffiliateCode),
        affiliateAttributionAt: user?.affiliateAttributionAt || null,
        affiliatePayouts: Array.isArray(user?.affiliatePayouts)
          ? user.affiliatePayouts.map((p) => normalizePlanSelection(p)).filter(Boolean)
          : [],
      };
    });
    if (shouldPersist) {
      await persistUsersDb();
    }
    log('Loaded users database', { count: usersDb.length });
  } catch (error) {
    usersDb = [];
    log('Failed to load users database, starting fresh', { error: String(error) });
  }
}

async function persistUsersDb() {
  await ensureStateFile();
  const payload = JSON.stringify(usersDb, null, 2);
  await safeWriteFile(USERS_DB_FILE, payload);
}

async function loadUserSessions() {
  try {
    await ensureStateFile();
    const raw = await fs.readFile(USER_SESSIONS_FILE, 'utf8').catch(() => null);
    if (raw) {
      const parsed = JSON.parse(raw || '{}');
      const now = Date.now();
      for (const [token, session] of Object.entries(parsed)) {
        if (session.expiresAt && session.expiresAt > now) {
          userSessions.set(token, session);
        }
      }
      log('Loaded user sessions', { count: userSessions.size });
    }
  } catch (error) {
    log('Failed to load user sessions, starting fresh', { error: String(error) });
  }
}

async function persistUserSessions() {
  await ensureStateFile();
  const now = Date.now();
  const sessions = {};
  for (const [token, session] of userSessions.entries()) {
    if (session.expiresAt && session.expiresAt > now) {
      sessions[token] = session;
    }
  }
  const payload = JSON.stringify(sessions, null, 2);
  await safeWriteFile(USER_SESSIONS_FILE, payload);
}

function sanitizeAffiliateCode(code) {
  return (code || '').toString().trim().toLowerCase().replace(/[^a-z0-9\-]/g, '').slice(0, 32);
}

async function loadAffiliatesDb() {
  try {
    await ensureStateFile();
    let raw = '[]';
    try {
      raw = await fs.readFile(AFFILIATES_FILE, 'utf8');
    } catch (_) { /* new file */ }
    const parsed = JSON.parse(raw || '[]');
    affiliatesDb = Array.isArray(parsed) ? parsed.map((a) => ({
      ...a,
      codes: Array.isArray(a?.codes) && a.codes.length ? a.codes.map((c) => ({
        code: sanitizeAffiliateCode(c.code || c.id || c.slug || ''),
        label: c.label || 'Tracking link',
        createdAt: c.createdAt || a.createdAt || new Date().toISOString(),
      })).filter((c) => c.code) : [],
      earnings: Array.isArray(a?.earnings) ? a.earnings : [],
      commissionRate: Number.isFinite(a?.commissionRate) ? a.commissionRate : AFFILIATE_COMMISSION_RATE,
    })) : [];
    let changed = false;
    for (const affiliate of affiliatesDb) {
      if (!affiliate.codes || !affiliate.codes.length) {
        const fallbackCode = generateTrackingCode();
        affiliate.codes = [{
          code: fallbackCode,
          label: 'Default link',
          createdAt: affiliate.createdAt || new Date().toISOString(),
        }];
        changed = true;
      }
    }
    if (changed) await persistAffiliatesDb();
    log('Loaded affiliates database', { count: affiliatesDb.length });
  } catch (error) {
    affiliatesDb = [];
    log('Failed to load affiliates database, starting fresh', { error: String(error) });
  }
}

async function persistAffiliatesDb() {
  await ensureStateFile();
  const payload = JSON.stringify(affiliatesDb, null, 2);
  await safeWriteFile(AFFILIATES_FILE, payload);
}

// Withdrawals database
let withdrawalsDb = [];

async function loadWithdrawalsDb() {
  try {
    await ensureStateFile();
    let raw = '[]';
    try {
      raw = await fs.readFile(WITHDRAWALS_FILE, 'utf8');
    } catch (_) { /* new file */ }
    withdrawalsDb = Array.isArray(JSON.parse(raw || '[]')) ? JSON.parse(raw || '[]') : [];
    log('Loaded withdrawals database', { count: withdrawalsDb.length });
  } catch (error) {
    withdrawalsDb = [];
    log('Failed to load withdrawals database, starting fresh', { error: String(error) });
  }
}

async function persistWithdrawalsDb() {
  await ensureStateFile();
  const payload = JSON.stringify(withdrawalsDb, null, 2);
  await safeWriteFile(WITHDRAWALS_FILE, payload);
}

// Feature Requests functions
async function loadFeatureRequestsDb() {
  try {
    await ensureStateFile();
    let raw = '[]';
    try {
      raw = await fs.readFile(FEATURE_REQUESTS_FILE, 'utf8');
    } catch (_) { /* new file */ }
    const parsed = JSON.parse(raw || '[]');
    featureRequestsDb = Array.isArray(parsed) ? parsed : [];
    log('Loaded feature requests database', { count: featureRequestsDb.length });
  } catch (error) {
    featureRequestsDb = [];
    log('Failed to load feature requests database, starting fresh', { error: String(error) });
  }
}

async function persistFeatureRequestsDb() {
  await ensureStateFile();
  const payload = JSON.stringify(featureRequestsDb, null, 2);
  await safeWriteFile(FEATURE_REQUESTS_FILE, payload);
}

async function loadContactMessagesDb() {
  let contactMessagesDb = [];
  try {
    await ensureStateFile();
    let raw = '[]';
    try {
      raw = await fs.readFile(CONTACT_MESSAGES_FILE, 'utf8');
    } catch (_) { /* new file */ }
    const parsed = JSON.parse(raw || '[]');
    contactMessagesDb = Array.isArray(parsed) ? parsed : [];
    log('Loaded contact messages database', { count: contactMessagesDb.length });
  } catch (error) {
    contactMessagesDb = [];
    log('Failed to load contact messages database, starting fresh', { error: String(error) });
  }
  return contactMessagesDb;
}

async function persistContactMessagesDb() {
  await ensureStateFile();
  const payload = JSON.stringify(contactMessagesDb, null, 2);
  await safeWriteFile(CONTACT_MESSAGES_FILE, payload);
}

// Tracking functions
async function loadTrackingData() {
  try {
    await fs.access(TRACKING_FILE);
    const raw = await fs.readFile(TRACKING_FILE, 'utf8');
    const parsed = JSON.parse(raw);
    
    // Convert dailyVisits uniqueVisitors arrays back to Sets
    const dailyVisits = parsed.summary?.dailyVisits || {};
    for (const dateKey in dailyVisits) {
      if (dailyVisits[dateKey]) {
        // Convert array to Set, or create empty Set if missing
        dailyVisits[dateKey].uniqueVisitors = new Set(dailyVisits[dateKey].uniqueVisitors || []);
      }
    }

    // Convert user analytics Sets back to Sets
    const userAnalytics = parsed.userAnalytics || {};
    
    // Convert DAU/WAU/MAU Sets back to Sets
    for (const dateKey in userAnalytics.dailyActiveUsers || {}) {
      userAnalytics.dailyActiveUsers[dateKey] = new Set(userAnalytics.dailyActiveUsers[dateKey] || []);
    }
    for (const weekKey in userAnalytics.weeklyActiveUsers || {}) {
      userAnalytics.weeklyActiveUsers[weekKey] = new Set(userAnalytics.weeklyActiveUsers[weekKey] || []);
    }
    for (const monthKey in userAnalytics.monthlyActiveUsers || {}) {
      userAnalytics.monthlyActiveUsers[monthKey] = new Set(userAnalytics.monthlyActiveUsers[monthKey] || []);
    }

    trackingData = {
      visits: Array.isArray(parsed.visits) ? parsed.visits : [],
      summary: {
        totalVisits: parsed.summary?.totalVisits || 0,
        uniqueVisitors: new Set(parsed.summary?.uniqueVisitors || []),
        referrers: parsed.summary?.referrers || {},
        pages: parsed.summary?.pages || {},
        dailyVisits: dailyVisits,
        conversions: parsed.summary?.conversions || { signup: 0, paid: 0 },
        financials: parsed.summary?.financials || { totalRevenue: 0, dailyRevenue: {} },
        referrersToUpgrade: parsed.summary?.referrersToUpgrade || {},
        conversionSources: parsed.summary?.conversionSources || {
          signup: { home: 0, pricing: 0, other: 0 },
          paid: { home: 0, pricing: 0, other: 0 }
        }
      },
      userAnalytics: {
        userSessions: userAnalytics.userSessions || {},
        dailyActiveUsers: userAnalytics.dailyActiveUsers || {},
        weeklyActiveUsers: userAnalytics.weeklyActiveUsers || {},
        monthlyActiveUsers: userAnalytics.monthlyActiveUsers || {},
        sessionDurations: Array.isArray(userAnalytics.sessionDurations) ? userAnalytics.sessionDurations : [],
        projectData: userAnalytics.projectData || {},
        featureUsage: userAnalytics.featureUsage || {},
        modelUsage: userAnalytics.modelUsage || {},
        exportUsage: userAnalytics.exportUsage || {},
        errorRates: userAnalytics.errorRates || {},
        retentionCohorts: userAnalytics.retentionCohorts || {},
        conversionFunnels: userAnalytics.conversionFunnels || {},
        resourceUtilization: userAnalytics.resourceUtilization || {},
        queueMetrics: userAnalytics.queueMetrics || {},
        planUpgradePatterns: userAnalytics.planUpgradePatterns || {},
      },
      businessMetrics: {
        mrr: parsed.businessMetrics?.mrr || 0,
        ltv: parsed.businessMetrics?.ltv || 0,
        churnRate: parsed.businessMetrics?.churnRate || 0,
        customerAcquisitionCost: parsed.businessMetrics?.customerAcquisitionCost || 0,
        averageRevenuePerUser: parsed.businessMetrics?.averageRevenuePerUser || 0,
        trialConversions: parsed.businessMetrics?.trialConversions || {},
        upgradeDowngradePatterns: parsed.businessMetrics?.upgradeDowngradePatterns || {},
        featureAdoptionByPlan: parsed.businessMetrics?.featureAdoptionByPlan || {},
      },
      technicalMetrics: {
        aiResponseTimes: Array.isArray(parsed.technicalMetrics?.aiResponseTimes) ? parsed.technicalMetrics.aiResponseTimes : [],
        aiErrorRates: parsed.technicalMetrics?.aiErrorRates || {},
        modelSelectionTrends: parsed.technicalMetrics?.modelSelectionTrends || {},
        queueWaitTimes: Array.isArray(parsed.technicalMetrics?.queueWaitTimes) ? parsed.technicalMetrics.queueWaitTimes : [],
        resourceUsage: Array.isArray(parsed.technicalMetrics?.resourceUsage) ? parsed.technicalMetrics.resourceUsage : [],
        systemHealth: parsed.technicalMetrics?.systemHealth || {
          uptime: 0,
          errors: 0,
          lastRestart: null
        }
      }
    };
    log('Loaded tracking data', { 
      totalVisits: trackingData.summary.totalVisits,
      uniqueVisitors: trackingData.summary.uniqueVisitors.size,
      userAnalytics: Object.keys(trackingData.userAnalytics.userSessions).length
    });
  } catch (error) {
    // Initialize with default structure
    trackingData = {
      visits: [],
      summary: {
        totalVisits: 0,
        uniqueVisitors: new Set(),
        referrers: {},
        pages: {},
        dailyVisits: {},
        conversions: { signup: 0, paid: 0 },
        financials: { totalRevenue: 0, dailyRevenue: {} },
        referrersToUpgrade: {},
        conversionSources: {
          signup: { home: 0, pricing: 0, other: 0 },
          paid: { home: 0, pricing: 0, other: 0 }
        }
      },
      userAnalytics: {
        userSessions: {},
        dailyActiveUsers: {},
        weeklyActiveUsers: {},
        monthlyActiveUsers: {},
        sessionDurations: [],
        projectData: {},
        featureUsage: {},
        modelUsage: {},
        exportUsage: {},
        errorRates: {},
        retentionCohorts: {},
        conversionFunnels: {},
        resourceUtilization: {},
        queueMetrics: {},
        planUpgradePatterns: {},
      },
      businessMetrics: {
        mrr: 0,
        ltv: 0,
        churnRate: 0,
        customerAcquisitionCost: 0,
        averageRevenuePerUser: 0,
        trialConversions: {},
        upgradeDowngradePatterns: {},
        featureAdoptionByPlan: {},
      },
      technicalMetrics: {
        aiResponseTimes: [],
        aiErrorRates: {},
        modelSelectionTrends: {},
        queueWaitTimes: [],
        resourceUsage: [],
        systemHealth: {
          uptime: 0,
          errors: 0,
          lastRestart: null
        }
      }
    };
    log('Failed to load tracking data, starting fresh', { error: String(error) });
  }
}

async function persistTrackingData() {
  await ensureStateFile();
  
  // Convert dailyVisits Sets to arrays for JSON serialization
  const serializedDailyVisits = {};
  for (const dateKey in trackingData.summary.dailyVisits) {
    const dayData = trackingData.summary.dailyVisits[dateKey];
    serializedDailyVisits[dateKey] = {
      count: dayData.count,
      uniqueVisitors: Array.from(dayData.uniqueVisitors || [])
    };
  }

  // Convert user analytics Sets to arrays for JSON serialization
  const serializedUserAnalytics = {
    userSessions: trackingData.userAnalytics.userSessions,
    dailyActiveUsers: {},
    weeklyActiveUsers: {},
    monthlyActiveUsers: {},
    sessionDurations: trackingData.userAnalytics.sessionDurations,
    projectData: trackingData.userAnalytics.projectData,
    featureUsage: trackingData.userAnalytics.featureUsage,
    modelUsage: trackingData.userAnalytics.modelUsage,
    exportUsage: trackingData.userAnalytics.exportUsage,
    errorRates: trackingData.userAnalytics.errorRates,
    retentionCohorts: trackingData.userAnalytics.retentionCohorts,
    conversionFunnels: trackingData.userAnalytics.conversionFunnels,
    resourceUtilization: trackingData.userAnalytics.resourceUtilization,
    queueMetrics: trackingData.userAnalytics.queueMetrics,
    planUpgradePatterns: trackingData.userAnalytics.planUpgradePatterns,
  };

  // Convert DAU/WAU/MAU Sets to arrays
  for (const dateKey in trackingData.userAnalytics.dailyActiveUsers) {
    serializedUserAnalytics.dailyActiveUsers[dateKey] = Array.from(trackingData.userAnalytics.dailyActiveUsers[dateKey] || []);
  }
  for (const weekKey in trackingData.userAnalytics.weeklyActiveUsers) {
    serializedUserAnalytics.weeklyActiveUsers[weekKey] = Array.from(trackingData.userAnalytics.weeklyActiveUsers[weekKey] || []);
  }
  for (const monthKey in trackingData.userAnalytics.monthlyActiveUsers) {
    serializedUserAnalytics.monthlyActiveUsers[monthKey] = Array.from(trackingData.userAnalytics.monthlyActiveUsers[monthKey] || []);
  }
  
  const payload = {
    visits: trackingData.visits.slice(-10000), // Keep last 10k visits
    summary: {
      totalVisits: trackingData.summary.totalVisits,
      uniqueVisitors: Array.from(trackingData.summary.uniqueVisitors),
      referrers: trackingData.summary.referrers,
      pages: trackingData.summary.pages,
      dailyVisits: serializedDailyVisits,
      conversions: trackingData.summary.conversions,
      financials: trackingData.summary.financials,
      referrersToUpgrade: trackingData.summary.referrersToUpgrade,
      conversionSources: trackingData.summary.conversionSources
    },
    userAnalytics: serializedUserAnalytics,
    businessMetrics: trackingData.businessMetrics,
    technicalMetrics: trackingData.technicalMetrics
  };
  await safeWriteFile(TRACKING_FILE, JSON.stringify(payload, null, 2));
}

function scheduleTrackingPersist() {
  if (trackingPersistTimer) clearTimeout(trackingPersistTimer);
  trackingPersistTimer = setTimeout(async () => {
    try {
      await persistTrackingData();
    } catch (error) {
      log('Failed to persist tracking data', { error: String(error) });
    }
  }, TRACKING_PERSIST_INTERVAL_MS);
}

function sanitizeUrl(urlString) {
  // Fix malformed URLs with double slashes or other common issues
  if (!urlString || urlString === '') return '/';
  
  // Fix URLs starting with '//' which creates ambiguous protocol-relative URLs
  if (urlString.startsWith('//')) {
    return urlString.slice(1);
  }
  
  return urlString;
}

function trackVisit(req, res) {
  try {
    let pathname;
    try {
      // Sanitize URL before parsing
      const urlString = sanitizeUrl(req.url);
      pathname = new URL(urlString, 'http://localhost').pathname;
    } catch (urlError) {
      // If URL parsing fails, skip tracking
      log('Tracking skipped - invalid URL', { url: req.url, error: String(urlError) });
      return;
    }
    
    // Skip tracking for static assets, API calls, and admin pages
    if (
      pathname.startsWith('/assets/') ||
      pathname.startsWith('/api/') ||
      pathname.startsWith('/admin/') ||
      pathname.match(/\.(css|js|png|jpg|jpeg|gif|svg|ico|woff|woff2|ttf|map|txt|pdf|xml|json)$/i)
    ) {
      return;
    }

    const referrer = req.headers.referer || req.headers.referrer || 'direct';
    const userAgent = req.headers['user-agent'] || 'unknown';
    const ip = req.headers['x-forwarded-for']?.split(',')[0]?.trim() || 
               req.headers['x-real-ip'] || 
               req.socket.remoteAddress || 
               'unknown';
    
    // Check if we should count this as a new session visit
    const visitSession = readCookieValue(req, 'v_session');
    const isNewSession = !visitSession;
    if (isNewSession && res) {
      const sessionToken = randomUUID();
      const parts = [
        `v_session=${sessionToken}`,
        'Path=/',
        'Max-Age=1800',
        'SameSite=Lax'
      ];
      if (process.env.COOKIE_SECURE !== '0') parts.push('Secure');
      res.setHeader('Set-Cookie', parts.join('; '));
      
      // Store landing info for conversion tracking
      const landingInfo = {
        landingPage: pathname,
        originalReferrer: referrer,
        timestamp: new Date().toISOString()
      };
      res.setHeader('Set-Cookie', `v_landing=${encodeURIComponent(JSON.stringify(landingInfo))}; Path=/; Max-Age=86400; SameSite=Lax`);
    }

    const visit = {
      timestamp: new Date().toISOString(),
      path: pathname,
      referrer: referrer,
      userAgent: userAgent,
      ip: ip
    };

    // Add to visits array
    trackingData.visits.push(visit);

    // Update summary
    if (isNewSession) {
      trackingData.summary.totalVisits += 1;
    }
    trackingData.summary.uniqueVisitors.add(ip);

    // Track referrer
    const referrerDomain = getReferrerDomain(referrer);
    trackingData.summary.referrers[referrerDomain] = (trackingData.summary.referrers[referrerDomain] || 0) + 1;

    // Track pages
    trackingData.summary.pages[pathname] = (trackingData.summary.pages[pathname] || 0) + 1;
    
    // Track referrers to upgrade/pricing page
    if (pathname === '/upgrade.html' || pathname === '/pricing.html' || pathname === '/select-plan.html') {
      trackingData.summary.referrersToUpgrade[referrerDomain] = (trackingData.summary.referrersToUpgrade[referrerDomain] || 0) + 1;
    }

    // Track daily visits
    const dateKey = new Date().toISOString().split('T')[0];
    if (!trackingData.summary.dailyVisits[dateKey]) {
      trackingData.summary.dailyVisits[dateKey] = {
        count: 0,
        uniqueVisitors: new Set()
      };
    }
    if (isNewSession) {
      trackingData.summary.dailyVisits[dateKey].count += 1;
    }
    trackingData.summary.dailyVisits[dateKey].uniqueVisitors.add(ip);

    scheduleTrackingPersist();
  } catch (error) {
    // Silent fail - tracking should not break the app
    log('Tracking error', { error: String(error) });
  }
}

function trackConversion(type, req) {
  try {
    if (!trackingData.summary.conversions) {
      trackingData.summary.conversions = { signup: 0, paid: 0 };
    }
    if (!trackingData.summary.conversions[type]) {
      trackingData.summary.conversions[type] = 0;
    }
    trackingData.summary.conversions[type] += 1;
    
    // Track source
    const landingInfoRaw = readCookieValue(req, 'v_landing');
    let source = 'other';
    if (landingInfoRaw) {
      try {
        const landingInfo = JSON.parse(decodeURIComponent(landingInfoRaw));
        if (landingInfo.landingPage === '/' || landingInfo.landingPage === '/home.html') source = 'home';
        else if (landingInfo.landingPage === '/pricing.html') source = 'pricing';
      } catch (_) {}
    }
    
    if (!trackingData.summary.conversionSources) {
      trackingData.summary.conversionSources = {
        signup: { home: 0, pricing: 0, other: 0 },
        paid: { home: 0, pricing: 0, other: 0 }
      };
    }
    if (!trackingData.summary.conversionSources[type]) {
      trackingData.summary.conversionSources[type] = { home: 0, pricing: 0, other: 0 };
    }
    trackingData.summary.conversionSources[type][source] += 1;
    
    scheduleTrackingPersist();
  } catch (error) {
    log('Conversion tracking error', { error: String(error) });
  }
}

function trackFinancial(amount, plan = 'unknown') {
  try {
    if (!trackingData.summary.financials) {
      trackingData.summary.financials = { totalRevenue: 0, dailyRevenue: {} };
    }
    const val = Number(amount) || 0;
    trackingData.summary.financials.totalRevenue += val;

    const dateKey = new Date().toISOString().split('T')[0];
    if (!trackingData.summary.financials.dailyRevenue[dateKey]) {
      trackingData.summary.financials.dailyRevenue[dateKey] = 0;
    }
    trackingData.summary.financials.dailyRevenue[dateKey] += val;

    scheduleTrackingPersist();
  } catch (error) {
    log('Financial tracking error', { error: String(error) });
  }
}

// Enhanced Analytics Tracking Functions
function trackUserSession(userId, action = 'login', data = {}) {
  try {
    if (!trackingData.userAnalytics.userSessions[userId]) {
      trackingData.userAnalytics.userSessions[userId] = {
        loginTime: null,
        lastActivity: null,
        sessionDuration: 0,
        pageViews: [],
        featuresUsed: {},
        modelUsage: {},
        projectCount: 0,
        exports: 0,
        errors: 0
      };
    }

    const session = trackingData.userAnalytics.userSessions[userId];
    const now = Date.now();

    switch (action) {
      case 'login':
        session.loginTime = now;
        session.lastActivity = now;
        break;
      case 'activity':
        session.lastActivity = now;
        if (data.page) session.pageViews.push({ path: data.page, timestamp: now });
        if (data.feature) {
          session.featuresUsed[data.feature] = (session.featuresUsed[data.feature] || 0) + 1;
        }
        if (data.model) {
          session.modelUsage[data.model] = (session.modelUsage[data.model] || 0) + 1;
        }
        break;
      case 'logout':
        if (session.loginTime) {
          session.sessionDuration = now - session.loginTime;
          trackingData.userAnalytics.sessionDurations.push(session.sessionDuration / 1000); // Store in seconds
        }
        break;
      case 'project_created':
        session.projectCount += 1;
        if (data.sessionId) {
          trackingData.userAnalytics.projectData[data.sessionId] = {
            createdAt: now,
            completedAt: null,
            status: 'created',
            featuresUsed: session.featuresUsed,
            plan: data.plan || 'unknown'
          };
        }
        break;
      case 'project_completed':
        if (data.sessionId && trackingData.userAnalytics.projectData[data.sessionId]) {
          trackingData.userAnalytics.projectData[data.sessionId].completedAt = now;
          trackingData.userAnalytics.projectData[data.sessionId].status = 'completed';
        }
        break;
      case 'export':
        session.exports += 1;
        const exportType = data.exportType || 'unknown';
        trackingData.userAnalytics.exportUsage[exportType] = (trackingData.userAnalytics.exportUsage[exportType] || 0) + 1;
        break;
      case 'error':
        session.errors += 1;
        const errorType = data.errorType || 'unknown';
        trackingData.userAnalytics.errorRates[errorType] = (trackingData.userAnalytics.errorRates[errorType] || 0) + 1;
        break;
    }

    // Update DAU/WAU/MAU
    updateActiveUsers(userId);
    
    scheduleTrackingPersist();
  } catch (error) {
    log('User session tracking error', { userId, action, error: String(error) });
  }
}

function updateActiveUsers(userId) {
  const now = new Date();
  const dateKey = now.toISOString().split('T')[0];
  const weekKey = `${now.getFullYear()}-W${Math.ceil((now.getDate() + now.getDay()) / 7)}`;
  const monthKey = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;

  // Initialize if needed
  if (!trackingData.userAnalytics.dailyActiveUsers[dateKey]) {
    trackingData.userAnalytics.dailyActiveUsers[dateKey] = new Set();
  }
  if (!trackingData.userAnalytics.weeklyActiveUsers[weekKey]) {
    trackingData.userAnalytics.weeklyActiveUsers[weekKey] = new Set();
  }
  if (!trackingData.userAnalytics.monthlyActiveUsers[monthKey]) {
    trackingData.userAnalytics.monthlyActiveUsers[monthKey] = new Set();
  }

  // Add user to all periods
  trackingData.userAnalytics.dailyActiveUsers[dateKey].add(userId);
  trackingData.userAnalytics.weeklyActiveUsers[weekKey].add(userId);
  trackingData.userAnalytics.monthlyActiveUsers[monthKey].add(userId);
}

function trackFeatureUsage(featureName, userId = null, plan = 'unknown') {
  try {
    trackingData.userAnalytics.featureUsage[featureName] = (trackingData.userAnalytics.featureUsage[featureName] || 0) + 1;
    
    // Track feature adoption by plan
    if (!trackingData.businessMetrics.featureAdoptionByPlan[featureName]) {
      trackingData.businessMetrics.featureAdoptionByPlan[featureName] = {};
    }
    trackingData.businessMetrics.featureAdoptionByPlan[featureName][plan] = 
      (trackingData.businessMetrics.featureAdoptionByPlan[featureName][plan] || 0) + 1;

    scheduleTrackingPersist();
  } catch (error) {
    log('Feature usage tracking error', { featureName, error: String(error) });
  }
}

function trackModelUsage(modelName, userId = null, plan = 'unknown') {
  try {
    trackingData.userAnalytics.modelUsage[modelName] = (trackingData.userAnalytics.modelUsage[modelName] || 0) + 1;
    
    // Track model selection trends over time
    const timeKey = new Date().toISOString().slice(0, 7); // YYYY-MM format
    if (!trackingData.technicalMetrics.modelSelectionTrends[timeKey]) {
      trackingData.technicalMetrics.modelSelectionTrends[timeKey] = {};
    }
    trackingData.technicalMetrics.modelSelectionTrends[timeKey][modelName] = 
      (trackingData.technicalMetrics.modelSelectionTrends[timeKey][modelName] || 0) + 1;

    scheduleTrackingPersist();
  } catch (error) {
    log('Model usage tracking error', { modelName, error: String(error) });
  }
}

function trackConversionFunnel(funnelName, step, userId = null, data = {}) {
  try {
    if (!trackingData.userAnalytics.conversionFunnels[funnelName]) {
      trackingData.userAnalytics.conversionFunnels[funnelName] = {};
    }
    
    if (!trackingData.userAnalytics.conversionFunnels[funnelName][step]) {
      trackingData.userAnalytics.conversionFunnels[funnelName][step] = {
        count: 0,
        users: new Set(),
        data: {}
      };
    }

    trackingData.userAnalytics.conversionFunnels[funnelName][step].count += 1;
    if (userId) {
      trackingData.userAnalytics.conversionFunnels[funnelName][step].users.add(userId);
    }

    scheduleTrackingPersist();
  } catch (error) {
    log('Conversion funnel tracking error', { funnelName, step, error: String(error) });
  }
}

function trackResourceUtilization() {
  try {
    const timestamp = Date.now();
    const usage = getResourceUsageSnapshot();
    
    trackingData.userAnalytics.resourceUtilization[timestamp] = {
      memory: usage.rss,
      heapUsed: usage.heapUsed,
      cpu: usage.load1,
      activeProcesses: usage.running
    };

    trackingData.technicalMetrics.resourceUsage.push({
      timestamp,
      memory: usage.rss,
      heapUsed: usage.heapUsed,
      cpu: usage.load1,
      activeProcesses: usage.running
    });

    // Keep only last 1000 entries
    if (trackingData.technicalMetrics.resourceUsage.length > 1000) {
      trackingData.technicalMetrics.resourceUsage = trackingData.technicalMetrics.resourceUsage.slice(-1000);
    }

    scheduleTrackingPersist();
  } catch (error) {
    log('Resource utilization tracking error', { error: String(error) });
  }
}

function trackQueueMetrics(waitTime, processedCount = 1) {
  try {
    const timestamp = Date.now();
    
    trackingData.userAnalytics.queueMetrics[timestamp] = {
      waitTime,
      processedCount
    };

    trackingData.technicalMetrics.queueWaitTimes.push(waitTime);

    // Keep only last 1000 entries
    if (trackingData.technicalMetrics.queueWaitTimes.length > 1000) {
      trackingData.technicalMetrics.queueWaitTimes = trackingData.technicalMetrics.queueWaitTimes.slice(-1000);
    }

    scheduleTrackingPersist();
  } catch (error) {
    log('Queue metrics tracking error', { waitTime, error: String(error) });
  }
}

function trackAIResponseTime(responseTime, provider, success = true, errorType = null) {
  try {
    trackingData.technicalMetrics.aiResponseTimes.push({
      timestamp: Date.now(),
      responseTime,
      provider,
      success
    });

    // Track error rates by provider
    if (!trackingData.technicalMetrics.aiErrorRates[provider]) {
      trackingData.technicalMetrics.aiErrorRates[provider] = {
        total: 0,
        errors: 0,
        errorRate: 0
      };
    }

    trackingData.technicalMetrics.aiErrorRates[provider].total += 1;
    if (!success) {
      trackingData.technicalMetrics.aiErrorRates[provider].errors += 1;
      trackingData.userAnalytics.errorRates[errorType || 'ai_error'] = 
        (trackingData.userAnalytics.errorRates[errorType || 'ai_error'] || 0) + 1;
    }
    
    trackingData.technicalMetrics.aiErrorRates[provider].errorRate = 
      (trackingData.technicalMetrics.aiErrorRates[provider].errors / 
       trackingData.technicalMetrics.aiErrorRates[provider].total * 100);

    // Keep only last 1000 entries
    if (trackingData.technicalMetrics.aiResponseTimes.length > 1000) {
      trackingData.technicalMetrics.aiResponseTimes = trackingData.technicalMetrics.aiResponseTimes.slice(-1000);
    }

    scheduleTrackingPersist();
  } catch (error) {
    log('AI response time tracking error', { responseTime, provider, error: String(error) });
  }
}

function trackPlanUpgrade(fromPlan, toPlan, userId = null) {
  try {
    if (!trackingData.userAnalytics.planUpgradePatterns[fromPlan]) {
      trackingData.userAnalytics.planUpgradePatterns[fromPlan] = {};
    }
    trackingData.userAnalytics.planUpgradePatterns[fromPlan][toPlan] = 
      (trackingData.userAnalytics.planUpgradePatterns[fromPlan][toPlan] || 0) + 1;

    if (!trackingData.businessMetrics.upgradeDowngradePatterns[fromPlan]) {
      trackingData.businessMetrics.upgradeDowngradePatterns[fromPlan] = {};
    }
    trackingData.businessMetrics.upgradeDowngradePatterns[fromPlan][toPlan] = 
      (trackingData.businessMetrics.upgradeDowngradePatterns[fromPlan][toPlan] || 0) + 1;

    scheduleTrackingPersist();
  } catch (error) {
    log('Plan upgrade tracking error', { fromPlan, toPlan, error: String(error) });
  }
}

function calculateBusinessMetrics() {
  try {
    const now = Date.now();
    const thirtyDaysAgo = now - (30 * 24 * 60 * 60 * 1000);
    const activeUsers = usersDb.filter(u => {
      const lastActive = u.lastActiveAt ? new Date(u.lastActiveAt).getTime() : 0;
      return lastActive > thirtyDaysAgo;
    });

    // Calculate MRR (Monthly Recurring Revenue)
    let mrr = 0;
    const planPrices = {
      hobby: 0,
      starter: 7.50,
      business: 25,
      enterprise: 75
    };

    usersDb.forEach(user => {
      if (PAID_PLANS.has(user.plan)) {
        mrr += planPrices[user.plan] || 0;
      }
    });

    trackingData.businessMetrics.mrr = mrr;

    // Calculate LTV (simplified)
    const avgRevenue = mrr / Math.max(activeUsers.length, 1);
    const avgLifespanMonths = 12; // Assume 12 months average
    trackingData.businessMetrics.ltv = avgRevenue * avgLifespanMonths;

    // Calculate churn rate (users who cancelled in last 30 days)
    const cancelledUsers = usersDb.filter(u => 
      u.plan === 'cancelled' && 
      u.cancelledAt && 
      new Date(u.cancelledAt).getTime() > thirtyDaysAgo
    ).length;
    
    const totalUsers30DaysAgo = usersDb.filter(u => 
      new Date(u.createdAt).getTime() <= thirtyDaysAgo
    ).length;

    trackingData.businessMetrics.churnRate = totalUsers30DaysAgo > 0 
      ? (cancelledUsers / totalUsers30DaysAgo * 100) 
      : 0;

    // Calculate ARPU (Average Revenue Per User)
    trackingData.businessMetrics.averageRevenuePerUser = 
      activeUsers.length > 0 ? mrr / activeUsers.length : 0;

  } catch (error) {
    log('Business metrics calculation error', { error: String(error) });
  }
}

function calculateRetentionCohorts() {
  try {
    const cohorts = {};
    const now = Date.now();
    
    usersDb.forEach(user => {
      const cohortMonth = new Date(user.createdAt).toISOString().slice(0, 7); // YYYY-MM
      
      if (!cohorts[cohortMonth]) {
        cohorts[cohortMonth] = {
          cohortSize: 0,
          users: [],
          retention: {
            '1week': 0,
            '1month': 0,
            '3month': 0
          }
        };
      }
      
      cohorts[cohortMonth].cohortSize += 1;
      cohorts[cohortMonth].users.push(user);
    });

    // Calculate retention for each cohort
    Object.keys(cohorts).forEach(cohortMonth => {
      const cohort = cohorts[cohortMonth];
      const cohortStart = new Date(cohortMonth + '-01').getTime();
      
      // 1 week retention
      const oneWeekAgo = cohortStart + (7 * 24 * 60 * 60 * 1000);
      const oneWeekActive = cohort.users.filter(u => {
        const lastActive = u.lastActiveAt ? new Date(u.lastActiveAt).getTime() : 0;
        return lastActive > oneWeekAgo;
      }).length;
      cohort.retention['1week'] = cohort.cohortSize > 0 ? (oneWeekActive / cohort.cohortSize * 100) : 0;

      // 1 month retention  
      const oneMonthAgo = cohortStart + (30 * 24 * 60 * 60 * 1000);
      const oneMonthActive = cohort.users.filter(u => {
        const lastActive = u.lastActiveAt ? new Date(u.lastActiveAt).getTime() : 0;
        return lastActive > oneMonthAgo;
      }).length;
      cohort.retention['1month'] = cohort.cohortSize > 0 ? (oneMonthActive / cohort.cohortSize * 100) : 0;

      // 3 month retention
      const threeMonthAgo = cohortStart + (90 * 24 * 60 * 60 * 1000);
      const threeMonthActive = cohort.users.filter(u => {
        const lastActive = u.lastActiveAt ? new Date(u.lastActiveAt).getTime() : 0;
        return lastActive > threeMonthAgo;
      }).length;
      cohort.retention['3month'] = cohort.cohortSize > 0 ? (threeMonthActive / cohort.cohortSize * 100) : 0;
    });

    trackingData.userAnalytics.retentionCohorts = cohorts;
  } catch (error) {
    log('Retention cohort calculation error', { error: String(error) });
  }
}

function getAnalyticsSummary() {
  try {
    // Update business metrics
    calculateBusinessMetrics();
    calculateRetentionCohorts();
    
    const now = new Date();
    const today = now.toISOString().split('T')[0];
    const thisWeek = `${now.getFullYear()}-W${Math.ceil((now.getDate() + now.getDay()) / 7)}`;
    const thisMonth = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;

    // Get DAU/WAU/MAU counts
    const dau = trackingData.userAnalytics.dailyActiveUsers[today]?.size || 0;
    const wau = trackingData.userAnalytics.weeklyActiveUsers[thisWeek]?.size || 0;
    const mau = trackingData.userAnalytics.monthlyActiveUsers[thisMonth]?.size || 0;

    // Calculate average session duration
    const avgSessionDuration = trackingData.userAnalytics.sessionDurations.length > 0
      ? trackingData.userAnalytics.sessionDurations.reduce((a, b) => a + b, 0) / trackingData.userAnalytics.sessionDurations.length
      : 0;

    // Calculate project completion rate
    const totalProjects = Object.keys(trackingData.userAnalytics.projectData).length;
    const completedProjects = Object.values(trackingData.userAnalytics.projectData)
      .filter(p => p.status === 'completed').length;
    const projectCompletionRate = totalProjects > 0 ? (completedProjects / totalProjects * 100) : 0;

    // Calculate return user rate
    const totalUsers = usersDb.length;
    const returningUsers = Object.keys(trackingData.userAnalytics.userSessions)
      .filter(uid => {
        const session = trackingData.userAnalytics.userSessions[uid];
        return session.loginTime && session.sessionDuration > 0 && session.pageViews.length > 1;
      }).length;
    const returnUserRate = totalUsers > 0 ? (returningUsers / totalUsers * 100) : 0;

    // Calculate free to paid conversion rate
    const freeUsers = usersDb.filter(u => u.plan === 'hobby').length;
    const convertedUsers = usersDb.filter(u => PAID_PLANS.has(u.plan)).length;
    const freeToPaidConversionRate = freeUsers > 0 ? (convertedUsers / freeUsers * 100) : 0;

    // Calculate trial to subscription conversion
    const trialUsers = usersDb.filter(u => u.trialStarted).length;
    const trialConversions = usersDb.filter(u => u.trialStarted && PAID_PLANS.has(u.plan)).length;
    const trialToSubscriptionConversion = trialUsers > 0 ? (trialConversions / trialUsers * 100) : 0;

    return {
      userEngagement: {
        dau,
        wau, 
        mau,
        averageSessionDuration: Math.round(avgSessionDuration),
        projectCompletionRate: Math.round(projectCompletionRate),
        returnUserRate: Math.round(returnUserRate),
        freeToPaidConversionRate: Math.round(freeToPaidConversionRate),
        trialToSubscriptionConversion: Math.round(trialToSubscriptionConversion)
      },
      featureUsage: trackingData.userAnalytics.featureUsage,
      modelUsage: trackingData.userAnalytics.modelUsage,
      exportUsage: trackingData.userAnalytics.exportUsage,
      errorRates: trackingData.userAnalytics.errorRates,
      retentionCohorts: trackingData.userAnalytics.retentionCohorts,
      businessMetrics: trackingData.businessMetrics,
      technicalMetrics: {
        aiResponseTimes: trackingData.technicalMetrics.aiResponseTimes.slice(-100), // Last 100
        aiErrorRates: trackingData.technicalMetrics.aiErrorRates,
        modelSelectionTrends: trackingData.technicalMetrics.modelSelectionTrends,
        averageQueueTime: trackingData.technicalMetrics.queueWaitTimes.length > 0
          ? Math.round(trackingData.technicalMetrics.queueWaitTimes.reduce((a, b) => a + b, 0) / trackingData.technicalMetrics.queueWaitTimes.length)
          : 0,
        resourceUtilization: trackingData.userAnalytics.resourceUtilization,
        systemHealth: trackingData.technicalMetrics.systemHealth
      },
      planUpgradePatterns: trackingData.userAnalytics.planUpgradePatterns,
      conversionFunnels: trackingData.userAnalytics.conversionFunnels,
      featureAdoptionByPlan: trackingData.businessMetrics.featureAdoptionByPlan
    };
  } catch (error) {
    log('Analytics summary calculation error', { error: String(error) });
    return {
      userEngagement: {
        dau: 0, wau: 0, mau: 0, averageSessionDuration: 0, projectCompletionRate: 0,
        returnUserRate: 0, freeToPaidConversionRate: 0, trialToSubscriptionConversion: 0
      },
      featureUsage: {},
      modelUsage: {},
      exportUsage: {},
      errorRates: {},
      retentionCohours: {},
      businessMetrics: {},
      technicalMetrics: {},
      planUpgradePatterns: {},
      conversionFunnels: {},
      featureAdoptionByPlan: {}
    };
  }
}

async function handleUpgradePopupTracking(req, res) {
  try {
    const body = await parseJsonBody(req);
    const source = (body && body.source) ? String(body.source).toLowerCase() : 'unknown';

    // Initialize upgradeSources tracking
    if (!trackingData.summary.upgradeSources) {
      trackingData.summary.upgradeSources = {
        apps_page: 0,
        builder_model: 0,
        usage_limit: 0,
        other: 0
      };
    }

    // Normalize source to known keys
    const sourceKey = {
      'apps_page': 'apps_page',
      'apps': 'apps_page',
      'builder_model': 'builder_model',
      'model_select': 'builder_model',
      'usage_limit': 'usage_limit',
      'token_limit': 'usage_limit',
      'out_of_tokens': 'usage_limit',
    }[source] || 'other';

    // Increment counter for this source
    trackingData.summary.upgradeSources[sourceKey] = (trackingData.summary.upgradeSources[sourceKey] || 0) + 1;

    log('Upgrade popup source tracked', { source, sourceKey });
    scheduleTrackingPersist();

    sendJson(res, 200, { ok: true });
  } catch (error) {
    log('Upgrade popup tracking error', { error: String(error) });
    sendJson(res, 400, { error: 'Failed to track upgrade popup source' });
  }
}

function calculateRetention() {
  try {
    const now = Date.now();
    const oneWeekMs = 7 * 24 * 60 * 60 * 1000;
    
    // 1-week retention: Users who signed up 7-14 days ago and were active in the last 7 days
    const cohortStart = now - (14 * 24 * 60 * 60 * 1000);
    const cohortEnd = now - (7 * 24 * 60 * 60 * 1000);
    
    const cohort = usersDb.filter(u => {
      const created = new Date(u.createdAt).getTime();
      return created >= cohortStart && created <= cohortEnd;
    });
    
    if (cohort.length === 0) return 0;
    
    const retained = cohort.filter(u => {
      const lastActive = u.lastActiveAt ? new Date(u.lastActiveAt).getTime() : 
                        (u.lastLoginAt ? new Date(u.lastLoginAt).getTime() : 0);
      return lastActive > cohortEnd;
    });
    
    return parseFloat(((retained.length / cohort.length) * 100).toFixed(1));
  } catch (error) {
    log('Retention calculation error', { error: String(error) });
    return 0;
  }
}

function getReferrerDomain(referrer) {
  if (!referrer || referrer === 'direct') return 'direct';
  try {
    const url = new URL(referrer);
    return url.hostname;
  } catch {
    return 'unknown';
  }
}

function findAffiliateByEmail(email) {
  const normalized = (email || '').trim().toLowerCase();
  return affiliatesDb.find((a) => a.email === normalized);
}

function findAffiliateByCode(code) {
  const normalized = sanitizeAffiliateCode(code);
  if (!normalized) return null;
  return affiliatesDb.find((a) => Array.isArray(a.codes) && a.codes.some((c) => c.code === normalized));
}

function generateTrackingCode(seed = '') {
  const base = sanitizeAffiliateCode(seed);
  const randomBit = randomBytes(3).toString('hex');
  let candidate = base ? `${base}-${randomBit}` : randomBytes(5).toString('hex');
  let attempt = 0;
  while (findAffiliateByCode(candidate) && attempt < 5) {
    candidate = base ? `${base}-${randomBytes(4).toString('hex')}` : randomBytes(6).toString('hex');
    attempt += 1;
  }
  return candidate;
}

async function registerAffiliate({ email, password, name }) {
  const normalized = (email || '').trim().toLowerCase();
  if (!EMAIL_REGEX.test(normalized)) throw new Error('Email is invalid');
  if (findAffiliateByEmail(normalized)) throw new Error('Affiliate already exists with this email');
  if (!password || password.length < 6) throw new Error('Password must be at least 6 characters long');
  const hashedPassword = await bcrypt.hash(password, PASSWORD_SALT_ROUNDS);
  const createdAt = new Date().toISOString();
  const code = generateTrackingCode();
  const affiliate = {
    id: randomUUID(),
    email: normalized,
    name: (name || '').trim() || normalized.split('@')[0],
    password: hashedPassword,
    createdAt,
    lastLoginAt: null,
    commissionRate: AFFILIATE_COMMISSION_RATE,
    codes: [{ code, label: 'Default link', createdAt }],
    earnings: [],
    lastPayoutAt: null,
    emailVerified: false,
    verificationToken: '',
    verificationExpiresAt: null,
  };
  assignVerificationToken(affiliate);
  affiliatesDb.push(affiliate);
  await persistAffiliatesDb();
  return affiliate;
}

async function verifyAffiliatePassword(email, password) {
  const affiliate = findAffiliateByEmail(email);
  if (!affiliate || !affiliate.password) return null;
  const valid = await bcrypt.compare(password, affiliate.password);
  if (!valid) return null;
  affiliate.lastLoginAt = new Date().toISOString();
  await persistAffiliatesDb();
  return affiliate;
}

function summarizeAffiliate(affiliate) {
  if (!affiliate) return null;
  const earnings = Array.isArray(affiliate.earnings) ? affiliate.earnings : [];
  const total = earnings.reduce((sum, e) => sum + Number(e.amount || 0), 0);
  return {
    id: affiliate.id,
    email: affiliate.email,
    name: affiliate.name || affiliate.email,
    emailVerified: affiliate.emailVerified || false,
    commissionRate: affiliate.commissionRate || AFFILIATE_COMMISSION_RATE,
    trackingLinks: affiliate.codes || [],
    earnings: {
      total,
      currency: 'USD',
      records: earnings.slice(-20),
    },
    createdAt: affiliate.createdAt,
    lastLoginAt: affiliate.lastLoginAt,
  };
}

function readCookieValue(req, name) {
  try {
    const cookieHeader = req?.headers?.cookie || '';
    if (!cookieHeader) return '';
    const parts = cookieHeader.split(';').map((p) => p.trim());
    const match = parts.find((p) => p.startsWith(`${name}=`));
    if (!match) return '';
    return decodeURIComponent(match.split('=').slice(1).join('=') || '');
  } catch (_) {
    return '';
  }
}

function readAffiliateSessionToken(req) {
  return readCookieValue(req, AFFILIATE_COOKIE_NAME);
}

function getAffiliateSession(req) {
  const token = readAffiliateSessionToken(req);
  if (!token) return null;
  const session = affiliateSessions.get(token);
  if (!session) return null;
  if (session.expiresAt && session.expiresAt < Date.now()) {
    affiliateSessions.delete(token);
    return null;
  }
  return { token, affiliateId: session.affiliateId, expiresAt: session.expiresAt };
}

function startAffiliateSession(res, affiliateId) {
  const token = randomUUID();
  const expiresAt = Date.now() + AFFILIATE_SESSION_TTL_MS;
  affiliateSessions.set(token, { affiliateId, expiresAt });
  const parts = [
    `${AFFILIATE_COOKIE_NAME}=${encodeURIComponent(token)}`,
    'Path=/',
    'HttpOnly',
    'SameSite=Lax',
    `Max-Age=${Math.floor(AFFILIATE_SESSION_TTL_MS / 1000)}`,
  ];
  if (process.env.COOKIE_SECURE !== '0') parts.push('Secure');
  res.setHeader('Set-Cookie', parts.join('; '));
  return token;
}

function clearAffiliateSession(res) {
  res.setHeader('Set-Cookie', `${AFFILIATE_COOKIE_NAME}=deleted; Path=/; Max-Age=0; SameSite=Lax`);
}

function requireAffiliateAuth(req, res) {
  const session = getAffiliateSession(req);
  if (!session) {
    sendJson(res, 401, { error: 'Affiliate authentication required' });
    return null;
  }
  const affiliate = affiliatesDb.find((a) => a.id === session.affiliateId);
  if (!affiliate) {
    sendJson(res, 401, { error: 'Affiliate session is invalid' });
    return null;
  }
  return { session, affiliate };
}

function setAffiliateReferralCookie(res, code) {
  const parts = [
    `${AFFILIATE_REF_COOKIE}=${encodeURIComponent(code)}`,
    'Path=/',
    'SameSite=Lax',
    `Max-Age=${AFFILIATE_REF_COOKIE_TTL_SECONDS}`,
  ];
  if (process.env.COOKIE_SECURE !== '0') parts.push('Secure');
  res.setHeader('Set-Cookie', parts.join('; '));
}

function readAffiliateReferralCode(req) {
  return sanitizeAffiliateCode(readCookieValue(req, AFFILIATE_REF_COOKIE));
}

async function trackAffiliateCommission(user, plan) {
  const normalizedPlan = normalizePlanSelection(plan);
  if (!normalizedPlan || !PAID_PLANS.has(normalizedPlan)) return;
  if (!user || !user.referredByAffiliateCode) return;
  user.affiliatePayouts = Array.isArray(user.affiliatePayouts) ? user.affiliatePayouts : [];
  const affiliate = findAffiliateByCode(user.referredByAffiliateCode);
  if (!affiliate) return;
  const price = PLAN_PRICES[normalizedPlan] || 0;
  if (!price) return;
  const amount = price * (affiliate.commissionRate || AFFILIATE_COMMISSION_RATE);
  const record = {
    id: randomUUID(),
    userId: user.id,
    plan: normalizedPlan,
    amount,
    currency: 'USD',
    description: `${normalizedPlan} subscription`,
    createdAt: new Date().toISOString(),
  };
  affiliate.earnings = Array.isArray(affiliate.earnings) ? affiliate.earnings : [];
  affiliate.earnings.push(record);
  affiliate.lastPayoutAt = record.createdAt;
  user.affiliatePayouts.push(normalizedPlan);
  await persistAffiliatesDb();
  await persistUsersDb();
}

function ensureMailTransport() {
  if (mailTransport) return mailTransport;

  if (!MAILPILOT_TOKEN) {
    console.log('');
    console.log('┌─────────────────────────────────────────────────────────────┐');
    console.log('│  📧  MAILPILOT NOT CONFIGURED                              │');
    console.log('├─────────────────────────────────────────────────────────────┤');
    console.log('│  Password reset and verification emails will NOT be sent.   │');
    console.log('│  To enable real emails, configure MailPilot in .env file:   │');
    console.log('│                                                             │');
    console.log('│    MAILPILOT_URL=https://emailmarketing.modelrailway3d.co.uk│');
    console.log('│    MAILPILOT_TOKEN=tx_abc123def456...                       │');
    console.log('│                                                             │');
    console.log('│  💡 Tip: Emails will be logged below when triggered          │');
    console.log('└─────────────────────────────────────────────────────────────┘');
    console.log('');
    mailTransport = {
      sendMail: async (payload) => {
        console.log('');
        console.log('┌─────────────────────────────────────────────────────────────┐');
        console.log('│  📧  [MOCK EMAIL - Not Sent]                                │');
        console.log('├─────────────────────────────────────────────────────────────┤');
        console.log(`│  To:    ${payload.to}`);
        console.log(`│  Subject: ${payload.subject}`);
        console.log('│  Body preview:');
        const preview = (payload.text || payload.html || '').slice(0, 200).replace(/\n/g, ' ');
        console.log(`│    ${preview}...`);
        console.log('│                                                             │');
        console.log('│  Configure MAILPILOT_TOKEN in .env to send real emails      │');
        console.log('└─────────────────────────────────────────────────────────────┘');
        console.log('');
        return { messageId: 'mock-' + Date.now() };
      },
      verify: (cb) => cb(null, true)
    };
    return mailTransport;
  }

  log('MailPilot configured', summarizeMailConfig());
  mailTransport = { url: MAILPILOT_URL, token: MAILPILOT_TOKEN };
  return mailTransport;
}

async function sendEmail({ to, subject, text, html }) {
  try {
    const transport = ensureMailTransport();
    
    if (!MAILPILOT_TOKEN) {
      await transport.sendMail({ to, subject, text, html });
      return { messageId: 'mock-' + Date.now() };
    }

    const payload = {
      from: SMTP_FROM || 'info@plugincompass.com',
      to,
      subject,
      html: html || text || '',
    };

    log('sending email via MailPilot', { to, subject });
    
    const response = await fetch(`${MAILPILOT_URL}/api/transactional-emails/trigger/${MAILPILOT_TOKEN}`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(payload),
    });

    if (!response.ok) {
      const errorText = await response.text();
      throw new Error(`MailPilot API error: ${response.status} - ${errorText}`);
    }

    const result = await response.json();
    log('email sent successfully via MailPilot', { to, status: result.status });
    return { messageId: result.status || 'sent' };
  } catch (err) {
    log('FAILED TO SEND EMAIL', {
      to,
      subject,
      error: String(err),
      hint: 'Check MAILPILOT_URL and MAILPILOT_TOKEN configuration in .env file'
    });
    throw err;
  }
}

function assignVerificationToken(user) {
  user.emailVerified = false;
  user.verificationToken = randomBytes(32).toString('hex');
  user.verificationExpiresAt = new Date(Date.now() + EMAIL_VERIFICATION_TTL_MS).toISOString();
}

function assignPasswordResetToken(user) {
  user.resetToken = randomBytes(32).toString('hex');
  user.resetExpiresAt = new Date(Date.now() + PASSWORD_RESET_TTL_MS).toISOString();
}

function normalizeVerificationState(user) {
  const hasExplicitFlag = user?.emailVerified === true || user?.emailVerified === false;
  let verified = user?.emailVerified === true || (!hasExplicitFlag && !!user?.lastLoginAt);
  let verificationToken = user?.verificationToken || '';
  let verificationExpiresAt = user?.verificationExpiresAt || null;
  let shouldPersist = false;

  if (!verified && !verificationToken) {
    const tmp = { emailVerified: false, verificationToken: '', verificationExpiresAt: null };
    assignVerificationToken(tmp);
    verificationToken = tmp.verificationToken;
    verificationExpiresAt = tmp.verificationExpiresAt;
    shouldPersist = true;
  }

  if (!hasExplicitFlag) shouldPersist = true;

  return { verified, verificationToken, verificationExpiresAt, shouldPersist };
}

function renderBrandedEmail({ title, preheader, bodyHtml, buttonText, buttonLink, showHero = false, heroTitle = '', heroSubtitle = '' }) {
  const logoBase = (PUBLIC_BASE_URL || '').replace(/\/+/g, '');
  const logoUrl = (logoBase ? `${logoBase}/assets/Plugin.png` : '/assets/Plugin.png');
  const accent = '#004225';
  const accent2 = '#006B3D';
  const accentLight = '#E8F5EC';
  const bg = '#f9f7f4';
  const cardBg = '#ffffff';
  const textPrimary = '#1a1a1a';
  const textSecondary = '#666666';
  const textMuted = '#999999';
  const borderColor = '#e8e4de';
  const safeTitle = escapeHtml(title || '');
  const safePre = escapeHtml(preheader || '');
  const safeBtnLink = escapeHtml(buttonLink || '');

  const heroSection = showHero ? `
    <tr>
      <td style="background: linear-gradient(135deg, ${accent} 0%, ${accent2} 100%); padding: 32px 40px; text-align: center;">
        <img src="${logoUrl}" alt="Plugin Compass" width="56" height="56" style="border-radius:12px; display: inline-block; margin-bottom: 16px; background: rgba(255,255,255,0.15);">
        <h1 style="color: #ffffff; margin: 0; font-size: 26px; font-weight: 700; letter-spacing: -0.5px;">${escapeHtml(heroTitle || safeTitle)}</h1>
        ${heroSubtitle ? `<p style="color: rgba(255,255,255,0.85); margin: 8px 0 0 0; font-size: 15px;">${escapeHtml(heroSubtitle)}</p>` : ''}
      </td>
    </tr>
  ` : `
    <tr>
      <td style="padding: 28px 32px 16px 32px; text-align: center; border-bottom: 1px solid ${borderColor};">
        <img src="${logoUrl}" alt="Plugin Compass" width="52" height="52" style="border-radius:10px; display: inline-block; margin-bottom: 8px;">
        <div style="color: ${accent}; font-size: 16px; font-weight: 700; letter-spacing: -0.3px;">${safeTitle}</div>
      </td>
    </tr>
  `;

  const html = `<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>${safeTitle}</title>
  <style>
    @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
    body { margin: 0; padding: 0; background: ${bg}; font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; }
    .wrapper { width: 100%; max-width: 620px; margin: 0 auto; padding: 24px 16px; }
    .card { background: ${cardBg}; border-radius: 16px; overflow: hidden; box-shadow: 0 2px 12px rgba(0,0,0,0.06); border: 1px solid ${borderColor}; }
    .card-inner { background: ${cardBg}; }
    .body-content { padding: 28px 32px 24px 32px; color: ${textPrimary}; font-size: 15px; line-height: 1.7; }
    .body-content p { margin: 0 0 16px 0; }
    .body-content p:last-child { margin-bottom: 0; }
    .details-box { background: ${bg}; border-radius: 10px; padding: 20px; margin: 20px 0; border: 1px solid ${borderColor}; }
    .detail-row { display: flex; justify-content: space-between; padding: 10px 0; border-bottom: 1px solid ${borderColor}; }
    .detail-row:last-child { border-bottom: none; padding-bottom: 0; }
    .detail-row:first-child { padding-top: 0; }
    .detail-label { color: ${textSecondary}; font-size: 14px; }
    .detail-value { font-weight: 600; color: ${textPrimary}; font-size: 14px; }
    .cta-section { padding: 0 32px 28px 32px; text-align: center; }
    .btn { display: inline-block; padding: 14px 28px; border-radius: 10px; color: #ffffff; text-decoration: none; font-weight: 600; font-size: 15px; background: linear-gradient(135deg, ${accent} 0%, ${accent2} 100%); box-shadow: 0 4px 12px rgba(0, 66, 37, 0.25); }
    .btn:hover { opacity: 0.95; }
    .success-icon { width: 64px; height: 64px; background: ${accentLight}; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; margin-bottom: 16px; }
    .success-icon svg { width: 32px; height: 32px; color: ${accent}; }
    .footer { padding: 24px 32px; border-top: 1px solid ${borderColor}; text-align: center; font-size: 13px; color: ${textMuted}; }
    .footer a { color: ${accent}; text-decoration: none; }
    .footer a:hover { text-decoration: underline; }
    .preheader { display: none !important; visibility: hidden; mso-hide: all; font-size: 0; line-height: 0; }
    .divider { height: 1px; background: ${borderColor}; margin: 0; }
    .highlight-box { background: ${accentLight}; border-left: 4px solid ${accent}; padding: 16px 20px; margin: 16px 0; border-radius: 0 8px 8px 0; }
    @media only screen and (max-width: 640px) {
      .wrapper { padding: 12px; }
      .body-content { padding: 20px 24px 20px 24px; }
      .cta-section { padding: 0 24px 24px 24px; }
      .footer { padding: 20px 24px; }
    }
  </style>
</head>
<body>
  <span class="preheader">${safePre}</span>
  <div class="wrapper">
    <table width="100%" cellpadding="0" cellspacing="0" border="0">
      <tr>
        <td>
          <table class="card" width="100%" cellpadding="0" cellspacing="0" border="0">
            ${heroSection}
            <tr>
              <td class="body-content">
                ${bodyHtml}
              </td>
            </tr>
            ${buttonText ? `
            <tr>
              <td class="cta-section">
                <a class="btn" href="${safeBtnLink}">${escapeHtml(buttonText)}</a>
              </td>
            </tr>
            ` : ''}
            <tr>
              <td class="divider"></td>
            </tr>
            <tr>
              <td class="footer">
                <p style="margin: 0 0 8px 0;">Need help? Contact us at <a href="mailto:info@plugincompass.com">info@plugincompass.com</a></p>
                <p style="margin: 0 0 4px 0;">© ${new Date().getFullYear()} Plugin Compass. All rights reserved.</p>
                <p style="margin: 0;"><a href="${escapeHtml(PUBLIC_BASE_URL || '')}">${escapeHtml(PUBLIC_BASE_URL || '') || 'Visit our website'}</a> &nbsp;|&nbsp; <a href="${escapeHtml(PUBLIC_BASE_URL || '' || '')}/privacy">Privacy</a> &nbsp;|&nbsp; <a href="${escapeHtml(PUBLIC_BASE_URL || '' || '')}/terms">Terms</a></p>
              </td>
            </tr>
          </table>
        </td>
      </tr>
    </table>
  </div>
</body>
</html>`;

  return html;
}

async function sendVerificationEmail(user, baseUrl) {
  if (!user || !user.email || !user.verificationToken) return;
  const link = `${baseUrl.replace(/\/+$/, '')}/verify-email?token=${encodeURIComponent(user.verificationToken)}`;
  const safeLink = escapeHtml(link);

  const text = `Welcome!\n\nPlease verify your email address by visiting the link below:\n${link}\n\nThis link expires in ${Math.round(EMAIL_VERIFICATION_TTL_MS / (60 * 60 * 1000))} hours.`;

  const bodyHtml = `
    <div style="text-align: center; margin-bottom: 24px;">
      <div style="width: 72px; height: 72px; background: #E8F5EC; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; margin-bottom: 16px;">
        <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="#004225" style="width: 36px; height: 36px;">
          <path stroke-linecap="round" stroke-linejoin="round" d="M21.75 6.75v10.5a2.25 2.25 0 01-2.25 2.25h-15a2.25 2.25 0 01-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25m19.5 0v.243a2.25 2.25 0 01-1.07 1.916l-7.5 4.615a2.25 2.25 0 01-2.36 0L3.32 8.91a2.25 2.25 0 01-1.07-1.916V6.75" />
        </svg>
      </div>
      <p style="margin: 0; color: #1a1a1a; font-size: 16px;">Welcome to Plugin Compass!</p>
    </div>

    <p style="margin-top: 0; margin-bottom: 16px; color: #1a1a1a;">Thanks for signing up! Please verify your email address to get started with AI-powered development.</p>

    <div style="text-align: center; margin: 28px 0;">
      <a class="btn" href="${safeLink}" style="display: inline-block; padding: 14px 28px; border-radius: 10px; color: #ffffff; text-decoration: none; font-weight: 600; font-size: 15px; background: linear-gradient(135deg, #004225 0%, #006B3D 100%); box-shadow: 0 4px 12px rgba(0, 66, 37, 0.25);">Verify Email Address</a>
    </div>

    <p style="margin-bottom: 8px; color: #666; font-size: 14px;">Or copy and paste this link into your browser:</p>
    <p style="word-break: break-all; margin: 0; color: #004225; font-size: 13px;"><a href="${safeLink}" style="color: #004225; text-decoration: none;">${safeLink}</a></p>

    <div class="highlight-box" style="background: #E8F5EC; border-left: 4px solid #004225; padding: 16px 20px; margin: 24px 0 16px 0; border-radius: 0 8px 8px 0;">
      <p style="margin: 0; font-size: 14px; color: #1a1a1a;"><strong>This link expires in ${Math.round(EMAIL_VERIFICATION_TTL_MS / (60 * 60 * 1000))} hours.</strong><br>If you didn't sign up for Plugin Compass, you can safely ignore this email.</p>
    </div>
  `;

  const html = renderBrandedEmail({
    title: 'Verify your email',
    preheader: 'Confirm your email to get started',
    bodyHtml,
    buttonText: '',
    buttonLink: '',
  });

  await sendEmail({ to: user.email, subject: 'Plugin Compass — Verify your email', text, html });
}

async function sendAffiliateVerificationEmail(affiliate, baseUrl) {
  if (!affiliate || !affiliate.email || !affiliate.verificationToken) return;
  const link = `${baseUrl.replace(/\/+$/, '')}/affiliate-verify-email?token=${encodeURIComponent(affiliate.verificationToken)}`;
  const safeLink = escapeHtml(link);

  const text = `Welcome to the Plugin Compass Affiliate Program!\n\nPlease verify your email address by visiting the link below:\n${link}\n\nThis link expires in ${Math.round(EMAIL_VERIFICATION_TTL_MS / (60 * 60 * 1000))} hours.`;

  const bodyHtml = `
    <div style="text-align: center; margin-bottom: 24px;">
      <div style="width: 72px; height: 72px; background: #E8F5EC; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; margin-bottom: 16px;">
        <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="#004225" style="width: 36px; height: 36px;">
          <path stroke-linecap="round" stroke-linejoin="round" d="M21.75 6.75v10.5a2.25 2.25 0 01-2.25 2.25h-15a2.25 2.25 0 01-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25m19.5 0v.243a2.25 2.25 0 01-1.07 1.916l-7.5 4.615a2.25 2.25 0 01-2.36 0L3.32 8.91a2.25 2.25 0 01-1.07-1.916V6.75" />
        </svg>
      </div>
      <p style="margin: 0; color: #1a1a1a; font-size: 16px;">Welcome to the Affiliate Program!</p>
    </div>

    <p style="margin-top: 0; margin-bottom: 16px; color: #1a1a1a;">Thanks for joining our Affiliate Program! Please verify your email address to start earning 7.5% recurring commissions.</p>

    <div style="text-align: center; margin: 28px 0;">
      <a class="btn" href="${safeLink}" style="display: inline-block; padding: 14px 28px; border-radius: 10px; color: #ffffff; text-decoration: none; font-weight: 600; font-size: 15px; background: linear-gradient(135deg, #004225 0%, #006B3D 100%); box-shadow: 0 4px 12px rgba(0, 66, 37, 0.25);">Verify Email Address</a>
    </div>

    <p style="margin-bottom: 8px; color: #666; font-size: 14px;">Or copy and paste this link into your browser:</p>
    <p style="word-break: break-all; margin: 0; color: #004225; font-size: 13px;"><a href="${safeLink}" style="color: #004225; text-decoration: none;">${safeLink}</a></p>

    <div class="highlight-box" style="background: #E8F5EC; border-left: 4px solid #004225; padding: 16px 20px; margin: 24px 0 16px 0; border-radius: 0 8px 8px 0;">
      <p style="margin: 0; font-size: 14px; color: #1a1a1a;"><strong>This link expires in ${Math.round(EMAIL_VERIFICATION_TTL_MS / (60 * 60 * 1000))} hours.</strong><br>If you didn't sign up for the Affiliate Program, you can safely ignore this email.</p>
    </div>
  `;

  const html = renderBrandedEmail({
    title: 'Verify your affiliate email',
    preheader: 'Confirm your email to start earning commissions',
    bodyHtml,
    buttonText: '',
    buttonLink: '',
  });

  await sendEmail({ to: affiliate.email, subject: 'Plugin Compass — Verify your affiliate email', text, html });
}

async function sendPasswordResetEmail(user, baseUrl) {
  if (!user || !user.email || !user.resetToken) return;
  const link = `${baseUrl.replace(/\/+$/, '')}/reset-password?token=${encodeURIComponent(user.resetToken)}`;
  const safeLink = escapeHtml(link);

  const text = `You requested a password reset.\n\nReset your password using the link below:\n${link}\n\nThis link expires in ${Math.round(PASSWORD_RESET_TTL_MS / (60 * 1000))} minutes.`;

  const bodyHtml = `
    <div style="text-align: center; margin-bottom: 24px;">
      <div style="width: 72px; height: 72px; background: #FFF3E0; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; margin-bottom: 16px;">
        <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="#E65100" style="width: 36px; height: 36px;">
          <path stroke-linecap="round" stroke-linejoin="round" d="M15.75 5.25a3 3 0 013 3m3 0a6 6 0 01-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1121.75 8.25z" />
        </svg>
      </div>
      <p style="margin: 0; color: #1a1a1a; font-size: 16px;">Reset your password</p>
    </div>

    <p style="margin-top: 0; margin-bottom: 16px; color: #1a1a1a;">We received a request to reset your password. Click the button below to create a new password for your account.</p>

    <div style="text-align: center; margin: 28px 0;">
      <a class="btn" href="${safeLink}" style="display: inline-block; padding: 14px 28px; border-radius: 10px; color: #ffffff; text-decoration: none; font-weight: 600; font-size: 15px; background: linear-gradient(135deg, #004225 0%, #006B3D 100%); box-shadow: 0 4px 12px rgba(0, 66, 37, 0.25);">Reset Password</a>
    </div>

    <p style="margin-bottom: 8px; color: #666; font-size: 14px;">Or copy and paste this link into your browser:</p>
    <p style="word-break: break-all; margin: 0; color: #004225; font-size: 13px;"><a href="${safeLink}" style="color: #004225; text-decoration: none;">${safeLink}</a></p>

    <div class="highlight-box" style="background: #FFF3E0; border-left: 4px solid #E65100; padding: 16px 20px; margin: 24px 0 16px 0; border-radius: 0 8px 8px 0;">
      <p style="margin: 0; font-size: 14px; color: #1a1a1a;"><strong>This link expires in ${Math.round(PASSWORD_RESET_TTL_MS / (60 * 1000))} minutes.</strong><br>If you didn't request a password reset, you can safely ignore this email.</p>
    </div>
  `;

  const html = renderBrandedEmail({
    title: 'Reset your password',
    preheader: 'Reset access to your Plugin Compass account',
    bodyHtml,
    buttonText: '',
    buttonLink: '',
  });

  await sendEmail({ to: user.email, subject: 'Plugin Compass — Reset your password', text, html });
}

async function sendPaymentConfirmationEmail(user, paymentType, details) {
  if (!user || !user.email) return;

  let title, preheader, bodyHtml, buttonText, buttonLink, subject, showHero, heroTitle, heroSubtitle;

  if (paymentType === 'topup') {
    const tokens = details.tokens || 0;
    const currency = (details.currency || 'USD').toUpperCase();
    const amount = (details.amount || 0) / 100;
    const formattedAmount = new Intl.NumberFormat('en-US', { style: 'currency', currency: currency }).format(amount);

    subject = 'Plugin Compass — Payment Confirmed';
    title = 'Payment Successful';
    preheader = 'Your token purchase is complete';
    showHero = true;
    heroTitle = 'Payment Confirmed';
    heroSubtitle = 'Your tokens have been added to your account';

    bodyHtml = `
      <div style="text-align: center; margin-bottom: 24px;">
        <div class="success-icon" style="width: 72px; height: 72px; background: #E8F5EC; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; margin-bottom: 16px;">
          <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="#004225" style="width: 36px; height: 36px;">
            <path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" />
          </svg>
        </div>
        <p style="margin: 0; color: #1a1a1a; font-size: 16px;">Thank you for your purchase!</p>
      </div>

      <div class="details-box" style="background: #f9f7f4; border-radius: 10px; padding: 20px; margin: 20px 0; border: 1px solid #e8e4de;">
        <div class="detail-row" style="display: flex; justify-content: space-between; padding: 10px 0; border-bottom: 1px solid #e8e4de;">
          <span class="detail-label" style="color: #666; font-size: 14px;">Description</span>
          <span class="detail-value" style="font-weight: 600; color: #1a1a1a; font-size: 14px;">Token Top-up</span>
        </div>
        <div class="detail-row" style="display: flex; justify-content: space-between; padding: 10px 0; border-bottom: 1px solid #e8e4de;">
          <span class="detail-label" style="color: #666; font-size: 14px;">Tokens Added</span>
          <span class="detail-value" style="font-weight: 600; color: #004225; font-size: 14px;">${tokens.toLocaleString()}</span>
        </div>
        <div class="detail-row" style="display: flex; justify-content: space-between; padding: 10px 0; border-bottom: 1px solid #e8e4de;">
          <span class="detail-label" style="color: #666; font-size: 14px;">Amount Paid</span>
          <span class="detail-value" style="font-weight: 600; color: #1a1a1a; font-size: 14px;">${formattedAmount}</span>
        </div>
        <div class="detail-row" style="display: flex; justify-content: space-between; padding: 10px 0;">
          <span class="detail-label" style="color: #666; font-size: 14px;">Status</span>
          <span class="detail-value" style="font-weight: 600; color: #006B3D; font-size: 14px;">Completed</span>
        </div>
      </div>

      <div class="highlight-box" style="background: #E8F5EC; border-left: 4px solid #004225; padding: 16px 20px; margin: 16px 0; border-radius: 0 8px 8px 0;">
        <p style="margin: 0; font-size: 14px; color: #1a1a1a;"><strong>Your tokens are ready to use!</strong> You can now use them for AI-powered development and building.</p>
      </div>
    `;

    buttonText = 'View Your Account';
    buttonLink = `${PUBLIC_BASE_URL || ''}/settings`;

  } else if (paymentType === 'payg') {
    const tokens = details.tokens || 0;
    const currency = (details.currency || 'USD').toUpperCase();
    const amount = (details.amount || 0) / 100;
    const formattedAmount = new Intl.NumberFormat('en-US', { style: 'currency', currency: currency }).format(amount);

    subject = 'Plugin Compass — Payment Confirmed';
    title = 'Pay-as-you-go Billing';
    preheader = 'Your overage billing is complete';
    showHero = true;
    heroTitle = 'Payment Processed';
    heroSubtitle = 'Your usage has been billed';

    bodyHtml = `
      <div style="text-align: center; margin-bottom: 24px;">
        <div class="success-icon" style="width: 72px; height: 72px; background: #E8F5EC; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; margin-bottom: 16px;">
          <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="#004225" style="width: 36px; height: 36px;">
            <path stroke-linecap="round" stroke-linejoin="round" d="M9 7h6m0 10v-3m-3 3h.01M9 17h.01M9 14h.01M12 14h.01M15 11h.01M12 11h.01M9 11h.01M7 21h10a2 2 0 002-2V5a2 2 0 00-2-2H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
          </svg>
        </div>
        <p style="margin: 0; color: #1a1a1a; font-size: 16px;">Your pay-as-you-go billing has been processed</p>
      </div>

      <div class="details-box" style="background: #f9f7f4; border-radius: 10px; padding: 20px; margin: 20px 0; border: 1px solid #e8e4de;">
        <div class="detail-row" style="display: flex; justify-content: space-between; padding: 10px 0; border-bottom: 1px solid #e8e4de;">
          <span class="detail-label" style="color: #666; font-size: 14px;">Type</span>
          <span class="detail-value" style="font-weight: 600; color: #1a1a1a; font-size: 14px;">Pay-as-you-go Usage</span>
        </div>
        <div class="detail-row" style="display: flex; justify-content: space-between; padding: 10px 0; border-bottom: 1px solid #e8e4de;">
          <span class="detail-label" style="color: #666; font-size: 14px;">Tokens Billed</span>
          <span class="detail-value" style="font-weight: 600; color: #004225; font-size: 14px;">${tokens.toLocaleString()}</span>
        </div>
        <div class="detail-row" style="display: flex; justify-content: space-between; padding: 10px 0; border-bottom: 1px solid #e8e4de;">
          <span class="detail-label" style="color: #666; font-size: 14px;">Amount Charged</span>
          <span class="detail-value" style="font-weight: 600; color: #1a1a1a; font-size: 14px;">${formattedAmount}</span>
        </div>
        <div class="detail-row" style="display: flex; justify-content: space-between; padding: 10px 0;">
          <span class="detail-label" style="color: #666; font-size: 14px;">Status</span>
          <span class="detail-value" style="font-weight: 600; color: #006B3D; font-size: 14px;">Completed</span>
        </div>
      </div>
    `;

    buttonText = 'View Usage';
    buttonLink = `${PUBLIC_BASE_URL || ''}/settings`;

  } else if (paymentType === 'subscription') {
    const plan = details.plan || '';
    const billingCycle = details.billingCycle || 'monthly';
    const currency = (details.currency || 'USD').toUpperCase();
    const amount = (details.amount || 0) / 100;
    const formattedAmount = new Intl.NumberFormat('en-US', { style: 'currency', currency: currency }).format(amount);
    const nextBillingDate = new Date();
    nextBillingDate.setMonth(nextBillingDate.getMonth() + (billingCycle === 'yearly' ? 12 : 1));

    subject = 'Plugin Compass — Subscription Activated';
    title = 'Welcome to ' + plan.charAt(0).toUpperCase() + plan.slice(1);
    preheader = 'Your subscription is now active';
    showHero = true;
    heroTitle = 'Subscription Active';
    heroSubtitle = 'Welcome to ' + plan.charAt(0).toUpperCase() + plan.slice(1) + ' Plan';

    bodyHtml = `
      <div style="text-align: center; margin-bottom: 24px;">
        <div class="success-icon" style="width: 72px; height: 72px; background: linear-gradient(135deg, #004225 0%, #006B3D 100%); border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; margin-bottom: 16px;">
          <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="#ffffff" style="width: 36px; height: 36px;">
            <path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
          </svg>
        </div>
        <p style="margin: 0; color: #1a1a1a; font-size: 16px;">Welcome to the ${plan.charAt(0).toUpperCase() + plan.slice(1)} Plan!</p>
      </div>

      <div class="highlight-box" style="background: #E8F5EC; border-left: 4px solid #004225; padding: 16px 20px; margin: 16px 0; border-radius: 0 8px 8px 0;">
        <p style="margin: 0; font-size: 14px; color: #1a1a1a;"><strong>Your subscription is now active!</strong> You have full access to all ${plan} features and benefits.</p>
      </div>

      <div class="details-box" style="background: #f9f7f4; border-radius: 10px; padding: 20px; margin: 20px 0; border: 1px solid #e8e4de;">
        <div class="detail-row" style="display: flex; justify-content: space-between; padding: 10px 0; border-bottom: 1px solid #e8e4de;">
          <span class="detail-label" style="color: #666; font-size: 14px;">Plan</span>
          <span class="detail-value" style="font-weight: 600; color: #004225; font-size: 14px;">${plan.charAt(0).toUpperCase() + plan.slice(1)}</span>
        </div>
        <div class="detail-row" style="display: flex; justify-content: space-between; padding: 10px 0; border-bottom: 1px solid #e8e4de;">
          <span class="detail-label" style="color: #666; font-size: 14px;">Billing Cycle</span>
          <span class="detail-value" style="font-weight: 600; color: #1a1a1a; font-size: 14px;">${billingCycle.charAt(0).toUpperCase() + billingCycle.slice(1)}</span>
        </div>
        <div class="detail-row" style="display: flex; justify-content: space-between; padding: 10px 0; border-bottom: 1px solid #e8e4de;">
          <span class="detail-label" style="color: #666; font-size: 14px;">Amount</span>
          <span class="detail-value" style="font-weight: 600; color: #1a1a1a; font-size: 14px;">${formattedAmount}/${billingCycle === 'yearly' ? 'year' : 'month'}</span>
        </div>
        <div class="detail-row" style="display: flex; justify-content: space-between; padding: 10px 0; border-bottom: 1px solid #e8e4de;">
          <span class="detail-label" style="color: #666; font-size: 14px;">Next Billing Date</span>
          <span class="detail-value" style="font-weight: 600; color: #1a1a1a; font-size: 14px;">${nextBillingDate.toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' })}</span>
        </div>
        <div class="detail-row" style="display: flex; justify-content: space-between; padding: 10px 0;">
          <span class="detail-label" style="color: #666; font-size: 14px;">Status</span>
          <span class="detail-value" style="font-weight: 600; color: #006B3D; font-size: 14px;">Active</span>
        </div>
      </div>
    `;

    buttonText = 'Explore Your Plan';
    buttonLink = `${PUBLIC_BASE_URL || ''}/settings`;
  }

  const html = renderBrandedEmail({
    title,
    preheader,
    bodyHtml,
    buttonText,
    buttonLink,
    showHero,
    heroTitle,
    heroSubtitle,
  });

  const text = bodyHtml.replace(/<[^>]*>/g, '').replace(/\n\s*\n/g, '\n\n').trim();

  try {
    await sendEmail({ to: user.email, subject, text, html });
  } catch (err) {
    log('failed to send payment confirmation email', { userId: user.id, email: user.email, paymentType, error: String(err) });
  }
}

async function sendPaymentCancelledEmail(user, data) {
  if (!user || !user.email) return;

  const bodyHtml = `
    <div style="text-align: center; margin-bottom: 24px;">
      <div style="width: 72px; height: 72px; background: #FFEBEE; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; margin-bottom: 16px;">
        <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="#D32F2F" style="width: 36px; height: 36px;">
          <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
        </svg>
      </div>
      <p style="margin: 0; color: #1a1a1a; font-size: 16px;">Payment Cancelled</p>
    </div>

    <p style="margin-top: 0; margin-bottom: 16px; color: #1a1a1a;">Your payment has been cancelled. This could be due to a timeout, customer cancellation, or payment method issue.</p>

    <div class="highlight-box" style="background: #FFF3E0; border-left: 4px solid #FF9800; padding: 16px 20px; margin: 16px 0; border-radius: 0 8px 8px 0;">
      <p style="margin: 0; font-size: 14px; color: #1a1a1a;"><strong>No charges were made.</strong> You can try making the payment again if you wish.</p>
    </div>
  `;

  const html = renderBrandedEmail({
    title: 'Payment Cancelled',
    preheader: 'Your payment was cancelled',
    bodyHtml,
  });

  try {
    await sendEmail({ to: user.email, subject: 'Plugin Compass — Payment Cancelled', text: bodyHtml.replace(/<[^>]*>/g, '').trim(), html });
  } catch (err) {
    log('failed to send payment cancelled email', { userId: user.id, email: user.email, error: String(err) });
  }
}

async function sendDisputeAcceptedEmail(user, data) {
  if (!user || !user.email) return;

  const bodyHtml = `
    <div style="text-align: center; margin-bottom: 24px;">
      <div style="width: 72px; height: 72px; background: #FFEBEE; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; margin-bottom: 16px;">
        <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="#D32F2F" style="width: 36px; height: 36px;">
          <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" />
        </svg>
      </div>
      <p style="margin: 0; color: #1a1a1a; font-size: 16px;">Dispute Accepted</p>
    </div>

    <p style="margin-top: 0; margin-bottom: 16px; color: #1a1a1a;">A dispute related to your payment has been accepted by the bank. The funds have been returned to the customer.</p>

    <div class="highlight-box" style="background: #FFEBEE; border-left: 4px solid #D32F2F; padding: 16px 20px; margin: 16px 0; border-radius: 0 8px 8px 0;">
      <p style="margin: 0; font-size: 14px; color: #1a1a1a;"><strong>Your subscription has been cancelled.</strong> Please contact support if you have any questions.</p>
    </div>
  `;

  const html = renderBrandedEmail({
    title: 'Dispute Accepted',
    preheader: 'Payment dispute accepted by bank',
    bodyHtml,
  });

  try {
    await sendEmail({ to: user.email, subject: 'Plugin Compass — Dispute Accepted', text: bodyHtml.replace(/<[^>]*>/g, '').trim(), html });
  } catch (err) {
    log('failed to send dispute accepted email', { userId: user.id, email: user.email, error: String(err) });
  }
}

async function sendDisputeCancelledEmail(user, data) {
  if (!user || !user.email) return;

  const bodyHtml = `
    <div style="text-align: center; margin-bottom: 24px;">
      <div style="width: 72px; height: 72px; background: #E8F5EC; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; margin-bottom: 16px;">
        <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="#004225" style="width: 36px; height: 36px;">
          <path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
        </svg>
      </div>
      <p style="margin: 0; color: #1a1a1a; font-size: 16px;">Dispute Cancelled</p>
    </div>

    <p style="margin-top: 0; margin-bottom: 16px; color: #1a1a1a;">Good news! The dispute related to your payment has been cancelled and your account is in good standing.</p>

    <div class="highlight-box" style="background: #E8F5EC; border-left: 4px solid #004225; padding: 16px 20px; margin: 16px 0; border-radius: 0 8px 8px 0;">
      <p style="margin: 0; font-size: 14px; color: #1a1a1a;"><strong>Your subscription remains active.</strong> Thank you for your continued support!</p>
    </div>
  `;

  const html = renderBrandedEmail({
    title: 'Dispute Cancelled',
    preheader: 'Payment dispute has been cancelled',
    bodyHtml,
  });

  try {
    await sendEmail({ to: user.email, subject: 'Plugin Compass — Dispute Cancelled', text: bodyHtml.replace(/<[^>]*>/g, '').trim(), html });
  } catch (err) {
    log('failed to send dispute cancelled email', { userId: user.id, email: user.email, error: String(err) });
  }
}

async function sendDisputeChallengedEmail(user, data) {
  if (!user || !user.email) return;

  const bodyHtml = `
    <div style="text-align: center; margin-bottom: 24px;">
      <div style="width: 72px; height: 72px; background: #FFF3E0; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; margin-bottom: 16px;">
        <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="#FF9800" style="width: 36px; height: 36px;">
          <path stroke-linecap="round" stroke-linejoin="round" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
        </svg>
      </div>
      <p style="margin: 0; color: #1a1a1a; font-size: 16px;">Dispute Challenged</p>
    </div>

    <p style="margin-top: 0; margin-bottom: 16px; color: #1a1a1a;">A dispute related to your payment has been challenged. The bank is reviewing the case.</p>

    <div class="highlight-box" style="background: #FFF3E0; border-left: 4px solid #FF9800; padding: 16px 20px; margin: 16px 0; border-radius: 0 8px 8px 0;">
      <p style="margin: 0; font-size: 14px; color: #1a1a1a;"><strong>Your subscription may be temporarily affected.</strong> We'll notify you once the review is complete.</p>
    </div>
  `;

  const html = renderBrandedEmail({
    title: 'Dispute Challenged',
    preheader: 'Payment dispute is being reviewed',
    bodyHtml,
  });

  try {
    await sendEmail({ to: user.email, subject: 'Plugin Compass — Dispute Challenged', text: bodyHtml.replace(/<[^>]*>/g, '').trim(), html });
  } catch (err) {
    log('failed to send dispute challenged email', { userId: user.id, email: user.email, error: String(err) });
  }
}

async function sendDisputeExpiredEmail(user, data) {
  if (!user || !user.email) return;

  const bodyHtml = `
    <div style="text-align: center; margin-bottom: 24px;">
      <div style="width: 72px; height: 72px; background: #E8F5EC; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; margin-bottom: 16px;">
        <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="#004225" style="width: 36px; height: 36px;">
          <path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
        </svg>
      </div>
      <p style="margin: 0; color: #1a1a1a; font-size: 16px;">Dispute Expired</p>
    </div>

    <p style="margin-top: 0; margin-bottom: 16px; color: #1a1a1a;">Good news! The dispute related to your payment has expired. The dispute period has ended and the original charge stands.</p>

    <div class="highlight-box" style="background: #E8F5EC; border-left: 4px solid #004225; padding: 16px 20px; margin: 16px 0; border-radius: 0 8px 8px 0;">
      <p style="margin: 0; font-size: 14px; color: #1a1a1a;"><strong>Your account remains in good standing.</strong> Thank you for your continued support!</p>
    </div>
  `;

  const html = renderBrandedEmail({
    title: 'Dispute Expired',
    preheader: 'Payment dispute has expired',
    bodyHtml,
  });

  try {
    await sendEmail({ to: user.email, subject: 'Plugin Compass — Dispute Expired', text: bodyHtml.replace(/<[^>]*>/g, '').trim(), html });
  } catch (err) {
    log('failed to send dispute expired email', { userId: user.id, email: user.email, error: String(err) });
  }
}

async function sendDisputeLostEmail(user, data) {
  if (!user || !user.email) return;

  const bodyHtml = `
    <div style="text-align: center; margin-bottom: 24px;">
      <div style="width: 72px; height: 72px; background: #FFEBEE; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; margin-bottom: 16px;">
        <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="#D32F2F" style="width: 36px; height: 36px;">
          <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
        </svg>
      </div>
      <p style="margin: 0; color: #1a1a1a; font-size: 16px;">Dispute Lost</p>
    </div>

    <p style="margin-top: 0; margin-bottom: 16px; color: #1a1a1a;">We regret to inform you that the dispute has been decided in favor of the customer. The funds have been refunded.</p>

    <div class="highlight-box" style="background: #FFEBEE; border-left: 4px solid #D32F2F; padding: 16px 20px; margin: 16px 0; border-radius: 0 8px 8px 0;">
      <p style="margin: 0; font-size: 14px; color: #1a1a1a;"><strong>Your subscription has been cancelled.</strong> If you believe this is an error, please contact our support team.</p>
    </div>
  `;

  const html = renderBrandedEmail({
    title: 'Dispute Lost',
    preheader: 'Payment dispute was not successful',
    bodyHtml,
  });

  try {
    await sendEmail({ to: user.email, subject: 'Plugin Compass — Dispute Lost', text: bodyHtml.replace(/<[^>]*>/g, '').trim(), html });
  } catch (err) {
    log('failed to send dispute lost email', { userId: user.id, email: user.email, error: String(err) });
  }
}

async function sendDisputeWonEmail(user, data) {
  if (!user || !user.email) return;

  const bodyHtml = `
    <div style="text-align: center; margin-bottom: 24px;">
      <div style="width: 72px; height: 72px; background: #E8F5EC; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; margin-bottom: 16px;">
        <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="#004225" style="width: 36px; height: 36px;">
          <path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
        </svg>
      </div>
      <p style="margin: 0; color: #1a1a1a; font-size: 16px;">Dispute Won</p>
    </div>

    <p style="margin-top: 0; margin-bottom: 16px; color: #1a1a1a;">Great news! The dispute has been decided in your favor. The original charge remains valid.</p>

    <div class="highlight-box" style="background: #E8F5EC; border-left: 4px solid #004225; padding: 16px 20px; margin: 16px 0; border-radius: 0 8px 8px 0;">
      <p style="margin: 0; font-size: 14px; color: #1a1a1a;"><strong>Your account remains in good standing.</strong> Thank you for your patience during this process.</p>
    </div>
  `;

  const html = renderBrandedEmail({
    title: 'Dispute Won',
    preheader: 'Payment dispute was successful',
    bodyHtml,
  });

  try {
    await sendEmail({ to: user.email, subject: 'Plugin Compass — Dispute Won', text: bodyHtml.replace(/<[^>]*>/g, '').trim(), html });
  } catch (err) {
    log('failed to send dispute won email', { userId: user.id, email: user.email, error: String(err) });
  }
}

async function sendRefundFailedEmail(user, data) {
  if (!user || !user.email) return;

  const bodyHtml = `
    <div style="text-align: center; margin-bottom: 24px;">
      <div style="width: 72px; height: 72px; background: #FFF3E0; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; margin-bottom: 16px;">
        <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="#FF9800" style="width: 36px; height: 36px;">
          <path stroke-linecap="round" stroke-linejoin="round" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
        </svg>
      </div>
      <p style="margin: 0; color: #1a1a1a; font-size: 16px;">Refund Failed</p>
    </div>

    <p style="margin-top: 0; margin-bottom: 16px; color: #1a1a1a;">We attempted to process a refund but it was unsuccessful. This could be due to expired card details or payment processor issues.</p>

    <div class="highlight-box" style="background: #FFF3E0; border-left: 4px solid #FF9800; padding: 16px 20px; margin: 16px 0; border-radius: 0 8px 8px 0;">
      <p style="margin: 0; font-size: 14px; color: #1a1a1a;"><strong>Please contact support</strong> if you were expecting a refund but haven't received it.</p>
    </div>
  `;

  const html = renderBrandedEmail({
    title: 'Refund Failed',
    preheader: 'Refund processing failed',
    bodyHtml,
  });

  try {
    await sendEmail({ to: user.email, subject: 'Plugin Compass — Refund Failed', text: bodyHtml.replace(/<[^>]*>/g, '').trim(), html });
  } catch (err) {
    log('failed to send refund failed email', { userId: user.id, email: user.email, error: String(err) });
  }
}

async function sendPaymentFailedEmail(user, data) {
  if (!user || !user.email) return;

  const bodyHtml = `
    <div style="text-align: center; margin-bottom: 24px;">
      <div style="width: 72px; height: 72px; background: #FFEBEE; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; margin-bottom: 16px;">
        <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="#D32F2F" style="width: 36px; height: 36px;">
          <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
        </svg>
      </div>
      <p style="margin: 0; color: #1a1a1a; font-size: 16px;">Payment Failed</p>
    </div>

    <p style="margin-top: 0; margin-bottom: 16px; color: #1a1a1a;">Your payment could not be processed. This could be due to insufficient funds, expired card, or other payment issues.</p>

    <div class="highlight-box" style="background: #FFEBEE; border-left: 4px solid #D32F2F; padding: 16px 20px; margin: 16px 0; border-radius: 0 8px 8px 0;">
      <p style="margin: 0; font-size: 14px; color: #1a1a1a;"><strong>Your subscription has been cancelled.</strong> Please update your payment method to reactivate.</p>
    </div>
  `;

  const html = renderBrandedEmail({
    title: 'Payment Failed',
    preheader: 'Your payment could not be processed',
    bodyHtml,
    buttonText: 'Update Payment',
    buttonLink: `${PUBLIC_BASE_URL || ''}/settings`,
  });

  try {
    await sendEmail({ to: user.email, subject: 'Plugin Compass — Payment Failed', text: bodyHtml.replace(/<[^>]*>/g, '').trim(), html });
  } catch (err) {
    log('failed to send payment failed email', { userId: user.id, email: user.email, error: String(err) });
  }
}

async function sendSubscriptionActiveEmail(user, data) {
  if (!user || !user.email) return;

  const bodyHtml = `
    <div style="text-align: center; margin-bottom: 24px;">
      <div class="success-icon" style="width: 72px; height: 72px; background: linear-gradient(135deg, #004225 0%, #006B3D 100%); border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; margin-bottom: 16px;">
        <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="#ffffff" style="width: 36px; height: 36px;">
          <path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
        </svg>
      </div>
      <p style="margin: 0; color: #1a1a1a; font-size: 16px;">Your subscription is active!</p>
    </div>

    <p style="margin-top: 0; margin-bottom: 16px; color: #1a1a1a;">Great news! Your subscription has been activated successfully. You now have full access to your plan features.</p>

    <div class="highlight-box" style="background: #E8F5EC; border-left: 4px solid #004225; padding: 16px 20px; margin: 16px 0; border-radius: 0 8px 8px 0;">
      <p style="margin: 0; font-size: 14px; color: #1a1a1a;"><strong>Current Plan:</strong> ${user.plan ? user.plan.charAt(0).toUpperCase() + user.plan.slice(1) : 'Hobby'}</p>
    </div>
  `;

  const html = renderBrandedEmail({
    title: 'Subscription Active',
    preheader: 'Your subscription is now active',
    bodyHtml,
  });

  try {
    await sendEmail({ to: user.email, subject: 'Plugin Compass — Subscription Active', text: bodyHtml.replace(/<[^>]*>/g, '').trim(), html });
  } catch (err) {
    log('failed to send subscription active email', { userId: user.id, email: user.email, error: String(err) });
  }
}

async function sendSubscriptionExpiredEmail(user, data) {
  if (!user || !user.email) return;

  const bodyHtml = `
    <div style="text-align: center; margin-bottom: 24px;">
      <div style="width: 72px; height: 72px; background: #FFEBEE; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; margin-bottom: 16px;">
        <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="#D32F2F" style="width: 36px; height: 36px;">
          <path stroke-linecap="round" stroke-linejoin="round" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
        </svg>
      </div>
      <p style="margin: 0; color: #1a1a1a; font-size: 16px;">Subscription Expired</p>
    </div>

    <p style="margin-top: 0; margin-bottom: 16px; color: #1a1a1a;">Your subscription has expired and you have been downgraded to the Hobby plan.</p>

    <div class="details-box" style="background: #f9f7f4; border-radius: 10px; padding: 20px; margin: 20px 0; border: 1px solid #e8e4de;">
      <div class="detail-row" style="display: flex; justify-content: space-between; padding: 10px 0; border-bottom: 1px solid #e8e4de;">
        <span class="detail-label" style="color: #666; font-size: 14px;">Current Plan</span>
        <span class="detail-value" style="font-weight: 600; color: #1a1a1a; font-size: 14px;">Hobby</span>
      </div>
      <div class="detail-row" style="display: flex; justify-content: space-between; padding: 10px 0;">
        <span class="detail-label" style="color: #666; font-size: 14px;">Status</span>
        <span class="detail-value" style="font-weight: 600; color: #D32F2F; font-size: 14px;">Expired</span>
      </div>
    </div>

    <div class="highlight-box" style="background: #E8F5EC; border-left: 4px solid #004225; padding: 16px 20px; margin: 16px 0; border-radius: 0 8px 8px 0;">
      <p style="margin: 0; font-size: 14px; color: #1a1a1a;"><strong>Resubscribe anytime</strong> to regain access to all premium features and benefits.</p>
    </div>
  `;

  const html = renderBrandedEmail({
    title: 'Subscription Expired',
    preheader: 'Your subscription has expired',
    bodyHtml,
    buttonText: 'Renew Subscription',
    buttonLink: `${PUBLIC_BASE_URL || ''}/settings`,
  });

  try {
    await sendEmail({ to: user.email, subject: 'Plugin Compass — Subscription Expired', text: bodyHtml.replace(/<[^>]*>/g, '').trim(), html });
  } catch (err) {
    log('failed to send subscription expired email', { userId: user.id, email: user.email, error: String(err) });
  }
}

async function sendSubscriptionOnHoldEmail(user, data) {
  if (!user || !user.email) return;

  const bodyHtml = `
    <div style="text-align: center; margin-bottom: 24px;">
      <div style="width: 72px; height: 72px; background: #FFF3E0; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; margin-bottom: 16px;">
        <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="#FF9800" style="width: 36px; height: 36px;">
          <path stroke-linecap="round" stroke-linejoin="round" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
        </svg>
      </div>
      <p style="margin: 0; color: #1a1a1a; font-size: 16px;">Subscription On Hold</p>
    </div>

    <p style="margin-top: 0; margin-bottom: 16px; color: #1a1a1a;">Your subscription has been placed on hold. This could be due to a payment issue or account review.</p>

    <div class="highlight-box" style="background: #FFF3E0; border-left: 4px solid #FF9800; padding: 16px 20px; margin: 16px 0; border-radius: 0 8px 8px 0;">
      <p style="margin: 0; font-size: 14px; color: #1a1a1a;"><strong>Please update your payment method</strong> or contact support to reactivate your subscription.</p>
    </div>
  `;

  const html = renderBrandedEmail({
    title: 'Subscription On Hold',
    preheader: 'Your subscription is temporarily on hold',
    bodyHtml,
    buttonText: 'Update Payment',
    buttonLink: `${PUBLIC_BASE_URL || ''}/settings`,
  });

  try {
    await sendEmail({ to: user.email, subject: 'Plugin Compass — Subscription On Hold', text: bodyHtml.replace(/<[^>]*>/g, '').trim(), html });
  } catch (err) {
    log('failed to send subscription on hold email', { userId: user.id, email: user.email, error: String(err) });
  }
}

async function sendSubscriptionPlanChangedEmail(user, data) {
  if (!user || !user.email) return;

  const bodyHtml = `
    <div style="text-align: center; margin-bottom: 24px;">
      <div style="width: 72px; height: 72px; background: #E3F2FD; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; margin-bottom: 16px;">
        <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="#1976D2" style="width: 36px; height: 36px;">
          <path stroke-linecap="round" stroke-linejoin="round" d="M7.5 21L3 16.5m0 0L7.5 12M3 16.5h13.5m0-13.5L21 7.5m0 0L16.5 12M21 7.5H7.5" />
        </svg>
      </div>
      <p style="margin: 0; color: #1a1a1a; font-size: 16px;">Plan Changed</p>
    </div>

    <p style="margin-top: 0; margin-bottom: 16px; color: #1a1a1a;">Your subscription plan has been updated successfully.</p>

    <div class="highlight-box" style="background: #E3F2FD; border-left: 4px solid #1976D2; padding: 16px 20px; margin: 16px 0; border-radius: 0 8px 8px 0;">
      <p style="margin: 0; font-size: 14px; color: #1a1a1a;"><strong>New Plan:</strong> ${user.plan ? user.plan.charAt(0).toUpperCase() + user.plan.slice(1) : 'Hobby'}</p>
    </div>
  `;

  const html = renderBrandedEmail({
    title: 'Plan Changed',
    preheader: 'Your subscription plan has been updated',
    bodyHtml,
  });

  try {
    await sendEmail({ to: user.email, subject: 'Plugin Compass — Plan Changed', text: bodyHtml.replace(/<[^>]*>/g, '').trim(), html });
  } catch (err) {
    log('failed to send plan changed email', { userId: user.id, email: user.email, error: String(err) });
  }
}

async function sendSubscriptionRenewedEmail(user, data) {
  if (!user || !user.email) return;

  const bodyHtml = `
    <div style="text-align: center; margin-bottom: 24px;">
      <div class="success-icon" style="width: 72px; height: 72px; background: linear-gradient(135deg, #004225 0%, #006B3D 100%); border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; margin-bottom: 16px;">
        <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="#ffffff" style="width: 36px; height: 36px;">
          <path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
        </svg>
      </div>
      <p style="margin: 0; color: #1a1a1a; font-size: 16px;">Subscription Renewed!</p>
    </div>

    <p style="margin-top: 0; margin-bottom: 16px; color: #1a1a1a;">Your subscription has been renewed successfully. Thank you for your continued support!</p>

    <div class="highlight-box" style="background: #E8F5EC; border-left: 4px solid #004225; padding: 16px 20px; margin: 16px 0; border-radius: 0 8px 8px 0;">
      <p style="margin: 0; font-size: 14px; color: #1a1a1a;"><strong>Current Plan:</strong> ${user.plan ? user.plan.charAt(0).toUpperCase() + user.plan.slice(1) : 'Hobby'}</p>
    </div>
  `;

  const html = renderBrandedEmail({
    title: 'Subscription Renewed',
    preheader: 'Your subscription has been renewed',
    bodyHtml,
  });

  try {
    await sendEmail({ to: user.email, subject: 'Plugin Compass — Subscription Renewed', text: bodyHtml.replace(/<[^>]*>/g, '').trim(), html });
  } catch (err) {
    log('failed to send subscription renewed email', { userId: user.id, email: user.email, error: String(err) });
  }
}

async function sendPaymentDisputeCreatedEmail(user, data) {
  if (!user || !user.email) return;

  const bodyHtml = `
    <div style="text-align: center; margin-bottom: 24px;">
      <div style="width: 72px; height: 72px; background: #FFEBEE; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; margin-bottom: 16px;">
        <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="#D32F2F" style="width: 36px; height: 36px;">
          <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" />
        </svg>
      </div>
      <p style="margin: 0; color: #1a1a1a; font-size: 16px;">Payment Dispute Created</p>
    </div>

    <p style="margin-top: 0; margin-bottom: 16px; color: #1a1a1a;">A payment dispute has been created against your account. We are reviewing the case.</p>

    <div class="highlight-box" style="background: #FFEBEE; border-left: 4px solid #D32F2F; padding: 16px 20px; margin: 16px 0; border-radius: 0 8px 8px 0;">
      <p style="margin: 0; font-size: 14px; color: #1a1a1a;"><strong>Your subscription has been temporarily suspended.</strong> We'll notify you once the dispute is resolved.</p>
    </div>
  `;

  const html = renderBrandedEmail({
    title: 'Payment Dispute Created',
    preheader: 'A dispute has been created',
    bodyHtml,
  });

  try {
    await sendEmail({ to: user.email, subject: 'Plugin Compass — Payment Dispute Created', text: bodyHtml.replace(/<[^>]*>/g, '').trim(), html });
  } catch (err) {
    log('failed to send payment dispute created email', { userId: user.id, email: user.email, error: String(err) });
  }
}

async function sendChargeRefundedEmail(user, data) {
  if (!user || !user.email) return;

  const amount = data.amount || 0;
  const formattedAmount = new Intl.NumberFormat('en-US', { style: 'currency', currency: data.currency || 'USD' }).format(amount / 100);

  const bodyHtml = `
    <div style="text-align: center; margin-bottom: 24px;">
      <div style="width: 72px; height: 72px; background: #E8F5EC; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; margin-bottom: 16px;">
        <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="#004225" style="width: 36px; height: 36px;">
          <path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
        </svg>
      </div>
      <p style="margin: 0; color: #1a1a1a; font-size: 16px;">Refund Processed</p>
    </div>

    <p style="margin-top: 0; margin-bottom: 16px; color: #1a1a1a;">We have processed a refund for your payment.</p>

    <div class="details-box" style="background: #f9f7f4; border-radius: 10px; padding: 20px; margin: 20px 0; border: 1px solid #e8e4de;">
      <div class="detail-row" style="display: flex; justify-content: space-between; padding: 10px 0; border-bottom: 1px solid #e8e4de;">
        <span class="detail-label" style="color: #666; font-size: 14px;">Refund Amount</span>
        <span class="detail-value" style="font-weight: 600; color: #1a1a1a; font-size: 14px;">${formattedAmount}</span>
      </div>
      <div class="detail-row" style="display: flex; justify-content: space-between; padding: 10px 0;">
        <span class="detail-label" style="color: #666; font-size: 14px;">Status</span>
        <span class="detail-value" style="font-weight: 600; color: #006B3D; font-size: 14px;">Refunded</span>
      </div>
    </div>
  `;

  const html = renderBrandedEmail({
    title: 'Refund Processed',
    preheader: 'A refund has been processed',
    bodyHtml,
  });

  try {
    await sendEmail({ to: user.email, subject: 'Plugin Compass — Refund Processed', text: bodyHtml.replace(/<[^>]*>/g, '').trim(), html });
  } catch (err) {
    log('failed to send refund processed email', { userId: user.id, email: user.email, error: String(err) });
  }
}

async function sendSubscriptionCancelledEmail(user, data) {
  if (!user || !user.email) return;

  const bodyHtml = `
    <div style="text-align: center; margin-bottom: 24px;">
      <div style="width: 72px; height: 72px; background: #FFEBEE; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; margin-bottom: 16px;">
        <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="#D32F2F" style="width: 36px; height: 36px;">
          <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
        </svg>
      </div>
      <p style="margin: 0; color: #1a1a1a; font-size: 16px;">Subscription Cancelled</p>
    </div>

    <p style="margin-top: 0; margin-bottom: 16px; color: #1a1a1a;">Your subscription has been cancelled and you have been downgraded to Hobby plan.</p>

    <div class="details-box" style="background: #f9f7f4; border-radius: 10px; padding: 20px; margin: 20px 0; border: 1px solid #e8e4de;">
      <div class="detail-row" style="display: flex; justify-content: space-between; padding: 10px 0; border-bottom: 1px solid #e8e4de;">
        <span class="detail-label" style="color: #666; font-size: 14px;">Current Plan</span>
        <span class="detail-value" style="font-weight: 600; color: #1a1a1a; font-size: 14px;">Hobby</span>
      </div>
      <div class="detail-row" style="display: flex; justify-content: space-between; padding: 10px 0;">
        <span class="detail-label" style="color: #666; font-size: 14px;">Status</span>
        <span class="detail-value" style="font-weight: 600; color: #D32F2F; font-size: 14px;">Cancelled</span>
      </div>
    </div>

    <div class="highlight-box" style="background: #E8F5EC; border-left: 4px solid #004225; padding: 16px 20px; margin: 16px 0; border-radius: 0 8px 8px 0;">
      <p style="margin: 0; font-size: 14px; color: #1a1a1a;"><strong>Resubscribe anytime</strong> to regain access to all premium features and benefits.</p>
    </div>
  `;

  const html = renderBrandedEmail({
    title: 'Subscription Cancelled',
    preheader: 'Your subscription has been cancelled',
    bodyHtml,
    buttonText: 'Resubscribe',
    buttonLink: `${PUBLIC_BASE_URL || ''}/settings`,
  });

  try {
    await sendEmail({ to: user.email, subject: 'Plugin Compass — Subscription Cancelled', text: bodyHtml.replace(/<[^>]*>/g, '').trim(), html });
  } catch (err) {
    log('failed to send subscription cancelled email', { userId: user.id, email: user.email, error: String(err) });
  }
}

async function sendSubscriptionPaymentFailedEmail(user, data) {
  if (!user || !user.email) return;

  const bodyHtml = `
    <div style="text-align: center; margin-bottom: 24px;">
      <div style="width: 72px; height: 72px; background: #FFEBEE; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; margin-bottom: 16px;">
        <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="#D32F2F" style="width: 36px; height: 36px;">
          <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
        </svg>
      </div>
      <p style="margin: 0; color: #1a1a1a; font-size: 16px;">Subscription Payment Failed</p>
    </div>

    <p style="margin-top: 0; margin-bottom: 16px; color: #1a1a1a;">Your subscription payment failed. This could be due to insufficient funds, expired card, or other payment issues.</p>

    <div class="highlight-box" style="background: #FFEBEE; border-left: 4px solid #D32F2F; padding: 16px 20px; margin: 16px 0; border-radius: 0 8px 8px 0;">
      <p style="margin: 0; font-size: 14px; color: #1a1a1a;"><strong>Your subscription has been cancelled.</strong> Please update your payment method to reactivate.</p>
    </div>
  `;

  const html = renderBrandedEmail({
    title: 'Subscription Payment Failed',
    preheader: 'Your subscription payment failed',
    bodyHtml,
    buttonText: 'Update Payment',
    buttonLink: `${PUBLIC_BASE_URL || ''}/settings`,
  });

  try {
    await sendEmail({ to: user.email, subject: 'Plugin Compass — Subscription Payment Failed', text: bodyHtml.replace(/<[^>]*>/g, '').trim(), html });
  } catch (err) {
    log('failed to send subscription payment failed email', { userId: user.id, email: user.email, error: String(err) });
  }
}

function findUserByEmail(email) {
  const normalized = (email || '').trim().toLowerCase();
  return usersDb.find(u => u.email === normalized);
}

function findUserByProvider(provider, providerId) {
  if (!provider || !providerId) return null;
  const normalizedProvider = provider.toLowerCase();
  return usersDb.find((u) =>
    Array.isArray(u.providers) &&
    u.providers.some((p) => p.provider === normalizedProvider && p.id === providerId)
  );
}

async function upsertOAuthUser(provider, providerId, email, profile = {}) {
  const normalizedProvider = (provider || '').toLowerCase();
  const normalizedEmail = (email || '').trim().toLowerCase();
  if (!normalizedProvider || !providerId) {
    throw new Error('OAuth provider and id are required');
  }

  const now = new Date().toISOString();
  let user = findUserByProvider(normalizedProvider, providerId) || (normalizedEmail ? findUserByEmail(normalizedEmail) : null);

  if (!user) {
    const placeholderPassword = await bcrypt.hash(randomBytes(32).toString('hex'), PASSWORD_SALT_ROUNDS);
    user = {
      id: randomUUID(),
      email: normalizedEmail || `noreply+${providerId}@example.com`,
      password: placeholderPassword,
      createdAt: now,
      lastLoginAt: now,
      providers: [],
      plan: null,
      billingStatus: DEFAULT_BILLING_STATUS,
      billingEmail: normalizedEmail,
      stripeCustomerId: '',
      stripePaymentMethodId: '',
      paymentMethodBrand: '',
      paymentMethodExpMonth: null,
      paymentMethodExpYear: null,
      paymentMethodLast4: '',
      dodoCustomerId: '',
      dodoSubscriptionId: '',
      billingCycle: null,
      subscriptionCurrency: null,
      subscriptionRenewsAt: null,
    };
    usersDb.push(user);
  }

  user.providers = Array.isArray(user.providers) ? user.providers : [];
  const existingProvider = user.providers.find((p) => p.provider === normalizedProvider && p.id === providerId);
  const providerPayload = {
    provider: normalizedProvider,
    id: providerId,
    email: normalizedEmail || existingProvider?.email || '',
    name: profile.name || profile.login || existingProvider?.name || '',
    avatar: profile.picture || profile.avatar_url || existingProvider?.avatar || '',
  };

  if (existingProvider) {
    Object.assign(existingProvider, providerPayload);
  } else {
    user.providers.push(providerPayload);
  }

  if (normalizedEmail && (!user.email || user.email.endsWith('@example.com'))) {
    user.email = normalizedEmail;
  }
  user.lastLoginAt = now;
  await persistUsersDb();
  log('OAuth user upserted', { provider: normalizedProvider, providerId, userId: user.id, email: user.email });
  return user;
}

function findUserById(userId) {
  return usersDb.find(u => u.id === userId);
}

async function createUser(email, password, options = {}) {
  const normalized = (email || '').trim().toLowerCase();
  if (!normalized || !password) {
    throw new Error('Email and password are required');
  }

  if (findUserByEmail(normalized)) {
    throw new Error('User already exists with this email');
  }

  const hashedPassword = await bcrypt.hash(password, PASSWORD_SALT_ROUNDS);
  const user = {
    id: randomUUID(),
    email: normalized,
    password: hashedPassword,
    createdAt: new Date().toISOString(),
    lastLoginAt: null,
    providers: [],
    emailVerified: false,
    verificationToken: '',
    verificationExpiresAt: null,
    resetToken: '',
    resetExpiresAt: null,
    plan: null,
    billingStatus: DEFAULT_BILLING_STATUS,
    billingEmail: normalized,
    stripeCustomerId: '',
    stripePaymentMethodId: '',
    paymentMethodBrand: '',
    paymentMethodExpMonth: null,
    paymentMethodExpYear: null,
    paymentMethodLast4: '',
    dodoCustomerId: '',
    dodoSubscriptionId: '',
    billingCycle: null,
    subscriptionCurrency: null,
    subscriptionRenewsAt: null,
    referredByAffiliateCode: sanitizeAffiliateCode(options.referredByAffiliateCode),
    affiliateAttributionAt: options.referredByAffiliateCode ? new Date().toISOString() : null,
    affiliatePayouts: [],
    onboardingCompleted: false,
  };

  assignVerificationToken(user);
  usersDb.push(user);
  await persistUsersDb();
  log('Created new user', { userId: user.id, email: user.email });
  return user;
}

async function verifyUserPassword(email, password) {
  const user = findUserByEmail(email);
  if (!user || typeof user.password !== 'string' || !user.password.length) return null;

  const isValid = await bcrypt.compare(password, user.password);
  if (!isValid) {
    // Track failed attempts
    user.failedLogins = (user.failedLogins || 0) + 1;

    // Lock account after 5 failed attempts
    if (user.failedLogins >= 5) {
      user.lockedUntil = Date.now() + LOGIN_LOCKOUT_MS;
      log('account locked due to failed attempts', { email, failedAttempts: user.failedLogins });
    }

    await persistUsersDb();
    return null;
  }

  // Update last login
  user.lastLoginAt = new Date().toISOString();
  user.failedLogins = 0;
  user.lockedUntil = null;
  await persistUsersDb();
  return user;
}

function normalizePlanSelection(plan) {
  const candidate = (plan || '').toString().trim().toLowerCase();
  return USER_PLANS.includes(candidate) ? candidate : null;
}

function resolveUserPlan(userId) {
  const user = findUserById(userId);
  return normalizePlanSelection(user?.plan) || DEFAULT_PLAN;
}

function getPlanAppLimit(plan) {
  const key = normalizePlanSelection(plan) || DEFAULT_PLAN;
  const limit = PLAN_APP_LIMITS[key];
  return Number.isFinite(limit) ? limit : Infinity;
}

function isPaidPlan(plan) {
  const normalized = (plan || '').toLowerCase();
  return PAID_PLANS.has(normalized);
}

function planPriority(plan) {
  const normalized = normalizePlanSelection(plan) || DEFAULT_PLAN;
  if (normalized === 'enterprise') return 4;
  if (normalized === 'professional') return 3;
  if (normalized === 'starter') return 2;
  return 1; // hobby
}

async function applyPlanPriorityDelay(plan) {
  const priority = planPriority(plan);
  if (priority === 4) return;
  let delayMs = HOBBY_PRIORITY_DELAY_MS;
  if (priority === 3) delayMs = BUSINESS_PRIORITY_DELAY_MS;
  else if (priority === 2) delayMs = STARTER_PRIORITY_DELAY_MS;

  await delay(delayMs);
}

function resolveFallbackModel(cli = 'opencode') {
  const configured = getConfiguredModels(cli);
  if (Array.isArray(configured) && configured.length) {
    return configured[0].name || configured[0].id || 'default';
  }
  return 'default';
}

function refreshAdminModelIndex() {
  const next = new Map();
  // Index public models (new structure)
  publicModels.forEach((model) => {
    if (!model) return;
    const idKey = String(model.id || '').trim();
    const nameKey = String(model.name || '').trim();
    if (idKey) next.set(`id:${idKey}`, model);
    if (nameKey) next.set(`name:${nameKey}`, model);
  });
  // Also index legacy admin models for backward compatibility
  adminModels.forEach((model) => {
    if (!model) return;
    const idKey = String(model.id || '').trim();
    const nameKey = String(model.name || '').trim();
    if (idKey && !next.has(`id:${idKey}`)) next.set(`id:${idKey}`, model);
    if (nameKey && !next.has(`name:${nameKey}`)) next.set(`name:${nameKey}`, model);
  });
  adminModelIndex = next;
}

function getAdminModelByIdOrName(value) {
  const key = String(value || '').trim();
  if (!key) return null;
  return adminModelIndex.get(`id:${key}`) || adminModelIndex.get(`name:${key}`) || null;
}

function resolvePlanModel(plan, requestedModel) {
  const normalized = normalizePlanSelection(plan) || DEFAULT_PLAN;
  const rawRequested = (requestedModel || '').trim();
  const requestedRecord = rawRequested ? getAdminModelByIdOrName(rawRequested) : null;
  const normalizedRequested = requestedRecord ? requestedRecord.name : '';
  const requested = normalizedRequested || rawRequested;
  const requestedTier = getModelTier(requested);
  
  // If user explicitly requests a valid model (not 'auto'), use it
  if (requested && requestedTier && requested !== AUTO_MODEL_TOKEN) {
    return requested;
  }
  
  const candidateList = getConfiguredModels();
  
  // For hobby/free plan users, use the admin-configured auto model
  if (normalized === 'hobby') {
    const adminDefault = (planSettings.freePlanModel || '').trim();
    if (adminDefault) return adminDefault;
    const firstModel = candidateList[0];
    if (firstModel) return firstModel.name;
    return resolveFallbackModel();
  }
  
  // For paid plans: respect user's model choice
  // If they requested a specific model (even without a tier), use it
  // Only fall back to first configured if they didn't request anything or requested 'auto'
  if (requested && requested !== AUTO_MODEL_TOKEN) {
    return requested;
  }
  
  // No specific request or 'auto' requested - use first configured model
  if (candidateList.length) {
    return candidateList[0].name;
  }
  
  return resolveFallbackModel();
}

function addMonthsSafely(date, months) {
  const base = new Date(date);
  const day = base.getDate();
  base.setDate(1);
  base.setMonth(base.getMonth() + months);
  const lastDay = new Date(base.getFullYear(), base.getMonth() + 1, 0).getDate();
  base.setDate(Math.min(day, lastDay));
  return base;
}

function computeRenewalDate(cycle = 'monthly') {
  const now = new Date();
  if (cycle === 'yearly') return addMonthsSafely(now, 12).toISOString();
  return addMonthsSafely(now, 1).toISOString();
}

async function serializeAccount(user) {
  if (!user) return null;
  const normalizedPayouts = Array.isArray(user.affiliatePayouts)
    ? user.affiliatePayouts.map((p) => normalizePlanSelection(p)).filter(Boolean)
    : [];
  
  let paymentMethod = null;
  if (user.dodoSubscriptionId && DODO_ENABLED) {
    try {
      const subscription = await dodoRequest(`/subscriptions/${user.dodoSubscriptionId}`, {
        method: 'GET',
      });
      
      if (subscription && subscription.payment_method) {
        const pm = subscription.payment_method;
        paymentMethod = {
          brand: pm.card?.brand || pm.payment_method || 'Card',
          last4: pm.card?.last4 || pm.card?.last_digits || '',
          expiresAt: pm.card?.expiry || pm.card?.expires_at || '',
        };
      }
    } catch (error) {
      console.error('[SerializeAccount] Failed to fetch payment method:', error.message);
    }
  }

  return {
    id: user.id,
    email: user.email,
    plan: user.plan || DEFAULT_PLAN,
    billingStatus: user.billingStatus || DEFAULT_BILLING_STATUS,
    billingEmail: user.billingEmail || user.email || '',
    subscriptionRenewsAt: user.subscriptionRenewsAt || null,
    billingCycle: user.billingCycle || null,
    subscriptionCurrency: user.subscriptionCurrency || user.currency || 'usd',
    referredByAffiliateCode: sanitizeAffiliateCode(user.referredByAffiliateCode),
    affiliateAttributionAt: user.affiliateAttributionAt || null,
    affiliatePayouts: normalizedPayouts,
    createdAt: user.createdAt || null,
    lastLoginAt: user.lastLoginAt || null,
    unlimitedUsage: Boolean(user.unlimitedUsage),
    balance: Number(user.balance || 0),
    currency: String(user.currency || 'usd').toLowerCase(),
    limits: {
      uploadZipBytes: MAX_UPLOAD_ZIP_SIZE,
    },
    tokenUsage: getTokenUsageSummary(user.id, user.plan || DEFAULT_PLAN),
    externalTestingUsage: getExternalTestUsageSummary(user.id, user.plan || DEFAULT_PLAN),
    paymentMethod,
    scheduledPlanChange: user.scheduledPlanChange || null,
  };
}

function readUserSessionToken(req) {
  try {
    const cookieHeader = req?.headers?.cookie || '';
    if (!cookieHeader) return '';
    const parts = cookieHeader.split(';').map((p) => p.trim());
    const match = parts.find((p) => p.startsWith(`${USER_COOKIE_NAME}=`));
    if (!match) return '';
    return decodeURIComponent(match.split('=').slice(1).join('=') || '');
  } catch (_) {
    return '';
  }
}

function getUserSession(req) {
  const token = readUserSessionToken(req);
  if (!token) return null;

  const session = userSessions.get(token);
  if (!session) return null;

  if (session.expiresAt && session.expiresAt < Date.now()) {
    userSessions.delete(token);
    persistUserSessions().catch(() => {});
    return null;
  }

  // Update last active for the user (throttled to once every 5 mins)
  try {
    const user = findUserById(session.userId);
    if (user) {
      const now = new Date();
      const lastActive = user.lastActiveAt ? new Date(user.lastActiveAt) : new Date(0);
      if (now.getTime() - lastActive.getTime() > 300000) {
        user.lastActiveAt = now.toISOString();
        persistUsersDb().catch(() => {});
      }
    }
  } catch (_) {}

  return { token, userId: session.userId, expiresAt: session.expiresAt };
}

function startUserSession(res, userId, remember = false) {
  const token = randomUUID();
  const ttl = remember ? USER_SESSION_TTL_MS : USER_SESSION_SHORT_TTL_MS;
  const expiresAt = Date.now() + ttl;
  userSessions.set(token, { userId, expiresAt });
  persistUserSessions().catch(() => {});

  const parts = [
    `${USER_COOKIE_NAME}=${encodeURIComponent(token)}`,
    'Path=/',
    'HttpOnly',
    'SameSite=Lax',
    `Max-Age=${Math.floor(ttl / 1000)}`,
  ];
  if (process.env.COOKIE_SECURE === '0') parts.push('Secure');
  res.setHeader('Set-Cookie', parts.join('; '));

  return token;
}

function clearUserSession(res) {
  res.setHeader('Set-Cookie', `${USER_COOKIE_NAME}=deleted; Path=/; Max-Age=0; SameSite=Lax`);
}

function requireUserAuth(req, res) {
  const session = getUserSession(req);
  if (!session) {
    sendJson(res, 401, { error: 'User authentication required' });
    return null;
  }
  return session;
}

function buildWorkspacePaths(session) {
  const userSegment = sanitizeSegment(session.userId || 'anonymous', 'anonymous');
  const appSegment = sanitizeSegment(session.appId || session.id || 'app', 'app');
  const workspaceDir = path.join(WORKSPACES_ROOT, userSegment, appSegment);
  const uploadsDir = path.join(workspaceDir, 'uploads');
  return { workspaceDir, uploadsDir };
}

async function ensureSessionPaths(session) {
  const paths = buildWorkspacePaths(session);
  session.workspaceDir = paths.workspaceDir;
  session.uploadsDir = paths.uploadsDir;
  session.attachmentKey = session.attachmentKey || randomUUID();
  await fs.mkdir(session.workspaceDir, { recursive: true });
  await fs.mkdir(session.uploadsDir, { recursive: true });
  await ensureOpencodeConfig(session);
  return session;
}

async function ensureOpencodeConfig(session) {
  if (!ENABLE_EXTERNAL_DIR_RESTRICTION) {
    return;
  }
  if (!session?.workspaceDir || !session?.appId) {
    return;
  }

  const userSegment = sanitizeSegment(session.userId || 'anonymous', 'anonymous');
  const appSegment = sanitizeSegment(session.appId || session.id || 'app', 'app');

  const providerName = OPENCODE_OLLAMA_PROVIDER || 'openai';
  const modelId = OPENCODE_OLLAMA_MODEL || 'qwen3:0.6b';
  // Respect the base URL exactly as provided by OPENCODE_OLLAMA_BASE_URL (trim trailing slashes)
  const baseUrl = (OPENCODE_OLLAMA_BASE_URL || 'https://ollama.plugincompass.com').replace(/\/+$/, '');

  const providerCfg = {
    options: {
      baseURL: baseUrl
    },
    models: {
      [modelId]: {
        id: modelId,
        name: modelId,
        tool_call: true,
        temperature: true
      }
    }
  };
  if (OPENCODE_OLLAMA_API_KEY) {
    providerCfg.options.apiKey = OPENCODE_OLLAMA_API_KEY;
  }

  const providers = {
    [providerName]: providerCfg
  };

  // Ensure models are loaded
  if (!adminModels || adminModels.length === 0) {
    log('adminModels empty, loading from store', { sessionId: session.id });
    await loadAdminModelStore();
  }

  // Find which providers are used in models
  const usedProviders = new Set();
  
  // Check opencode models for provider prefixes in names (e.g., "openrouter/model-name")
  for (const model of opencodeModels) {
    if (model.name && model.name.includes('/')) {
      const providerFromName = model.name.split('/')[0].toLowerCase();
      if (providerFromName && providerFromName !== 'opencode') {
        usedProviders.add(providerFromName);
      }
    }
  }
  
  // Also check public models for provider prefixes in names
  for (const model of publicModels) {
    if (model.name && model.name.includes('/')) {
      const providerFromName = model.name.split('/')[0].toLowerCase();
      if (providerFromName && providerFromName !== 'opencode') {
        usedProviders.add(providerFromName);
      }
    }
  }
  
  // Fallback to legacy adminModels
  for (const model of adminModels) {
    if (model.name && model.name.includes('/')) {
      const providerFromName = model.name.split('/')[0].toLowerCase();
      if (providerFromName && providerFromName !== 'opencode') {
        usedProviders.add(providerFromName);
      }
    }
    
    if (Array.isArray(model.providers)) {
      for (const p of model.providers) {
        if (typeof p === 'string') {
          usedProviders.add(p.toLowerCase());
        } else if (p && typeof p === 'object' && p.provider) {
          usedProviders.add(p.provider.toLowerCase());
        }
      }
    }
    if (model.primaryProvider) {
      usedProviders.add(model.primaryProvider.toLowerCase());
    }
  }

  log('Detected providers from models', {
    usedProviders: Array.from(usedProviders),
    count: usedProviders.size
  });

  // Provider configurations with their base URLs
  const providerConfigs = {
    chutes: {
      apiKey: CHUTES_API_KEY,
      baseURL: 'https://llm.chutes.ai/v1'
    },
    cerebras: {
      apiKey: process.env.CEREBRAS_API_KEY,
      baseURL: 'https://api.cerebras.ai/v1'
    },
    groq: {
      apiKey: GROQ_API_KEY,
      baseURL: 'https://api.groq.com/openai/v1'
    },
    google: {
      apiKey: GOOGLE_API_KEY,
      baseURL: 'https://generativelanguage.googleapis.com/v1beta'
    },
    nvidia: {
      apiKey: NVIDIA_API_KEY,
      baseURL: 'https://integrate.api.nvidia.com/v1'
    },
    kilo: {
      apiKey: process.env.KILO_API_KEY,
      baseURL: 'https://api.kilo.ai/api/gateway'
    }
  };

  // Add providers that are both configured and used
  for (const [providerId, config] of Object.entries(providerConfigs)) {
    if (config.apiKey && usedProviders.has(providerId)) {
      // Fetch all models from models.dev for this provider
      let models = {};
      try {
        models = await fetchModelsDevProviderModels(providerId);
      } catch (err) {
        log('Failed to fetch models from models.dev, using empty models list', { 
          provider: providerId, 
          error: String(err) 
        });
      }
      
      providers[providerId] = {
        options: {
          apiKey: config.apiKey,
          baseURL: config.baseURL
        },
        models: models
      };
      
      log('Configured provider', {
        provider: providerId,
        modelCount: Object.keys(models).length,
        source: Object.keys(models).length > 0 ? 'models.dev' : 'built-in'
      });
    }
  }

  const config = {
    $schema: 'https://opencode.ai/config.json',
    model: `${providerName}/${modelId}`,
    small_model: `${providerName}/${modelId}`,
    permission: {
      external_directory: {
        '*': 'deny',
        [`*/${appSegment}/*`]: 'allow',
        [`apps/${userSegment}/${appSegment}/*`]: 'allow'
      }
    },
    provider: providers
  };

  const configPath = path.join(session.workspaceDir, 'opencode.json');

  try {
    await fs.writeFile(
      configPath,
      JSON.stringify(config, null, 2),
      'utf8'
    );
    log('Created opencode config for session', {
      sessionId: session.id,
      appId: appSegment,
      userId: userSegment,
      providers: Object.keys(providers)
    });
  } catch (err) {
    log('Failed to create opencode config', {
      sessionId: session.id,
      error: String(err)
    });
  }
}

function newOpencodeSessionId() {
  return `ses-${randomUUID()}`;
}

async function ensureOpencodeSession(session, model) {
  if (!session) return null;
  await ensureSessionPaths(session);

  const targetModel = model || session.model;
  const workspaceDir = session.workspaceDir;
  if (!workspaceDir) throw new Error('Session workspace directory not initialized');

  // If we have an initial session ID locked, always use it without validation
  // This ensures session continuity across all messages in the builder
  if (session.initialOpencodeSessionId) {
    log('Using locked initial opencode session', {
      sessionId: session.id,
      lockedSessionId: session.initialOpencodeSessionId,
      currentSessionId: session.opencodeSessionId
    });

    // Ensure session.opencodeSessionId is synced with initial
    if (session.opencodeSessionId !== session.initialOpencodeSessionId) {
      session.opencodeSessionId = session.initialOpencodeSessionId;
      await persistState();
    }
    return session.initialOpencodeSessionId;
  }

  // If session already has an opencode session ID (but no locked initial), verify it exists in CLI listing.
  // (Previously we only checked that "list" ran, which doesn't validate the id.)
  if (session.opencodeSessionId) {
    try {
      const cliCommand = resolveCliCommand('opencode');
      const listCandidates = [
        ['session', 'list', '--format', 'json'],
        ['sessions', 'list', '--format', 'json'],
      ];

      let stdout = '';
      for (const args of listCandidates) {
        try {
          const result = await runCommand(cliCommand, args, { timeout: 7000, cwd: workspaceDir });
          stdout = result.stdout || '';
          if (stdout) break;
        } catch (_) {
          // try next candidate
        }
      }

      if (stdout) {
        try {
          const parsed = JSON.parse(stdout);
          const items = Array.isArray(parsed)
            ? parsed
            : (Array.isArray(parsed.sessions) ? parsed.sessions : (Array.isArray(parsed.data) ? parsed.data : []));
          const ids = items.map((it) => it?.id || it?.sessionId || it?.session_id).filter(Boolean);
          if (ids.includes(session.opencodeSessionId)) {
            return session.opencodeSessionId;
          }
          log('stored opencode session id not found in list; using stored id for continuity', { stored: session.opencodeSessionId });
          return session.opencodeSessionId;
        } catch (err) {
          // If JSON parse fails, try to continue with existing id
          log('opencode session list unparseable; attempting to use stored id', { stored: session.opencodeSessionId, err: String(err) });
          return session.opencodeSessionId;
        }
      } else {
        // Cannot list sessions; try to use stored id and let CLI handle it
        log('cannot list sessions; falling back to stored id', { stored: session.opencodeSessionId });
        return session.opencodeSessionId;
      }
    } catch (err) {
      log('existing opencode session validation failed', { sessionId: session.opencodeSessionId, err: String(err) });
      // CRITICAL FIX: If validation fails, don't create a new session - return existing one
      // This prevents creating a new OpenCode session on every message when CLI operations fail
      log('Using existing opencode session ID despite validation failure for continuity', { stored: session.opencodeSessionId });
      return session.opencodeSessionId;
    }
    // If we reach here, the existing session ID may be invalid - continue to create new below
  }

  // Create a new opencode session using the createOpencodeSession helper
  const freshId = newOpencodeSessionId();
  try {
    const sessionId = await createOpencodeSession(freshId, targetModel, workspaceDir);
    if (sessionId) {
      session.opencodeSessionId = sessionId;
      // CRITICAL FIX: Only set initialOpencodeSessionId if not already set
      // This prevents overwriting the initial session ID when creating subsequent sessions
      if (!session.initialOpencodeSessionId) {
        session.initialOpencodeSessionId = sessionId; // Lock this as the initial session
        log('Created and locked new opencode session', { sessionId, model: targetModel });
      } else {
        log('Updated opencode session ID (preserving initial)', {
          sessionId,
          initialSessionId: session.initialOpencodeSessionId,
          model: targetModel
        });
      }
      await persistState();
      return sessionId;
    }
  } catch (err) {
    log('Failed to create opencode session', { err: String(err), desiredId: freshId, model: targetModel });
  }

  // If createOpencodeSession failed, try a simple direct approach
  // This handles cases where CLI doesn't support expected session commands
  try {
    // We cannot just generate a random ID because CLI will crash if session file doesn't exist.
    // Instead, we return null to let the run command create a new session implicitly.
    log('Cannot create explicit session, will rely on auto-creation during run', { model: targetModel });
    return null;
  } catch (error) {
    log('Failed to setup opencode session', { error: String(error), model: targetModel });
    return null;
  }
}

function normalizeCli(cli) {
  const name = (cli || '').toLowerCase();
  return SUPPORTED_CLIS.includes(name) ? name : 'opencode';
}

function resolveCliCommand(cli) {
  const normalized = normalizeCli(cli);
  const binDir = process.env.OPENCODE_BIN_DIR || '/root/.opencode/bin';
  const candidates = [];
  if (OPENCODE_REPO_CLI) candidates.push(OPENCODE_REPO_CLI);
  if (binDir) candidates.push(path.join(binDir, normalized));
  candidates.push(`/usr/local/bin/${normalized}`);
  candidates.push(normalized);
  for (const candidate of candidates) {
    try {
      fsSync.accessSync(candidate, fsSync.constants.X_OK);
      return candidate;
    } catch (_) { }
  }
  log('CLI command not found on PATH; falling back to raw name', { cli: normalized, candidates });
  return normalized;
}

const opencodeVerificationCache = {
  checked: false,
  ok: false,
  error: null,
  cliPath: null,
};

function verifyOpencodePrompts() {
  const promptFiles = [
    'codex_header.txt',
    'beast.txt',
    'gemini.txt',
    'anthropic.txt',
    'qwen.txt',
    'trinity.txt',
    'wordpress-plugin.txt',
    'wordpress-plugin-subsequent.txt'
  ];
  const missing = [];
  const nonWordPress = [];

  for (const file of promptFiles) {
    const fullPath = path.join(OPENCODE_PROMPT_DIR, file);
    if (!fsSync.existsSync(fullPath)) {
      missing.push(fullPath);
      continue;
    }
    const content = fsSync.readFileSync(fullPath, 'utf8');
    if (!/wordpress/i.test(content)) {
      nonWordPress.push(fullPath);
    }
  }

  if (missing.length) {
    throw new Error(`OpenCode prompt verification failed: missing prompt files: ${missing.join(', ')}`);
  }
  if (nonWordPress.length) {
    throw new Error(`OpenCode prompt verification failed: prompts are not WordPress-specific: ${nonWordPress.join(', ')}`);
  }
}

function verifyOpencodeCli(cliCommand) {
  if (!OPENCODE_REQUIRE_REPO) return;
  const repoRootResolved = fsSync.existsSync(OPENCODE_REPO_ROOT)
    ? fsSync.realpathSync(OPENCODE_REPO_ROOT)
    : null;
  if (!repoRootResolved) {
    throw new Error('OpenCode repo root not found; cannot verify CLI build source.');
  }
  let cliResolved = null;
  try {
    cliResolved = fsSync.realpathSync(cliCommand);
  } catch (_) {
    cliResolved = null;
  }
  if (!cliResolved || !cliResolved.startsWith(repoRootResolved)) {
    throw new Error(`OpenCode CLI is not using the repo build. Expected CLI under ${repoRootResolved} but got ${cliCommand}.`);
  }
}

function verifyOpencodeSetup(cliCommand) {
  if (!OPENCODE_REQUIRE_REPO) {
    log('OpenCode verification skipped (OPENCODE_REQUIRE_REPO=false)');
    return;
  }
  if (opencodeVerificationCache.checked) {
    if (opencodeVerificationCache.error) throw opencodeVerificationCache.error;
    return;
  }
  try {
    verifyOpencodeCli(cliCommand);
    verifyOpencodePrompts();
    opencodeVerificationCache.checked = true;
    opencodeVerificationCache.ok = true;
    opencodeVerificationCache.cliPath = cliCommand;
  } catch (err) {
    opencodeVerificationCache.checked = true;
    opencodeVerificationCache.ok = false;
    opencodeVerificationCache.error = err;
    throw err;
  }
}

async function ensureStateFile() {
  try {
    await fs.mkdir(STATE_DIR, { recursive: true });
    await fs.mkdir(UPLOADS_DIR, { recursive: true });
    await fs.mkdir(WORKSPACES_ROOT, { recursive: true });
  } catch (error) {
    log('Error creating state directories', { error: String(error), STATE_DIR, UPLOADS_DIR, WORKSPACES_ROOT });
  }
  try { await fs.access(STATE_FILE); } catch (_) {
    try {
      await fs.writeFile(STATE_FILE, JSON.stringify(state, null, 2), 'utf8');
    } catch (writeError) {
      log('Error initializing state file', { error: String(writeError), STATE_FILE });
    }
  }
}

async function loadState() {
  try {
    await ensureStateFile();
    const raw = await fs.readFile(STATE_FILE, 'utf8');
    const parsed = JSON.parse(raw || '{}');
    const savedSessions = Array.isArray(parsed.sessions) ? parsed.sessions : [];
    state.sessions = [];
    for (const saved of savedSessions) {
      const session = {
        ...saved,
        cli: normalizeCli(saved.cli),
        userId: sanitizeSegment(saved.userId || 'anonymous', 'anonymous'),
        appId: sanitizeSegment(saved.appId || saved.id || 'app', 'app'),
        entryMode: saved.entryMode === 'opencode' ? 'opencode' : 'plan',
        source: saved.source || 'builder',
        planApproved: !!saved.planApproved,
        opencodeSessionId: saved.opencodeSessionId || saved.opencodeSession,
        initialOpencodeSessionId: saved.initialOpencodeSessionId || saved.opencodeSessionId,
      };
      if (session.opencodeSession) delete session.opencodeSession;
      try {
        await ensureSessionPaths(session);
      } catch (err) {
        log('Failed to ensure session paths while loading state', { err: String(err), sessionId: session.id });
      }

      // Restore running/queued messages after server restart to allow continuation
      if (Array.isArray(session.messages)) {
        let sessionModified = false;
        session.messages.forEach(msg => {
          if (msg.status === 'running' || msg.status === 'queued') {
            log('Restoring interrupted message after restart', { sessionId: session.id, messageId: msg.id, prevStatus: msg.status });
            // Reset to queued state so it can be retried automatically
            msg.status = 'queued';
            msg.retryAfterRestart = true;
            msg.restartedAt = new Date().toISOString();
            sessionModified = true;
          }
        });
        if (sessionModified) {
          // Keep pending count to trigger automatic retry
          session.updatedAt = new Date().toISOString();
          session.restoredAfterRestart = true;
        }
      }

      state.sessions.push(session);
    }
    log(`Loaded ${state.sessions.length} sessions from ${STATE_FILE}`);
    // If any sessions were modified during cleanup, persist the changes
    await persistState();
  } catch (error) {
    log('Failed to load state, starting fresh', { error: String(error) });
    state.sessions = [];
  }
}

async function persistState() {
  try {
    // Prepare state with additional runtime information for graceful restart
    const stateToSave = {
      sessions: state.sessions.map(session => ({
        ...session,
        lastSavedAt: new Date().toISOString(),
        activeQueues: sessionQueues.has(session.id),
        hasActiveStreams: activeStreams.has(session.id)
      })),
      serverVersion: '2.0',
      savedAt: new Date().toISOString()
    };
    const safe = JSON.stringify(stateToSave, null, 2);
    await safeWriteFile(STATE_FILE, safe);
  } catch (error) {
    log('persistState failed', { error: String(error), STATE_FILE });
  }
}

async function restoreInterruptedSessions() {
  try {
    let restoredCount = 0;
    let messageCount = 0;
    
    for (const session of state.sessions) {
      if (!session.restoredAfterRestart) continue;
      
      let hasQueuedMessages = false;
      const messagesToProcess = [];
      if (Array.isArray(session.messages)) {
        for (const msg of session.messages) {
          if (msg.status === 'queued' && msg.retryAfterRestart) {
            hasQueuedMessages = true;
            messageCount++;
            messagesToProcess.push(msg);
            log('Restoring message after restart', { 
              sessionId: session.id, 
              messageId: msg.id,
              role: msg.role 
            });
          }
        }
        
        if (messagesToProcess.length > 0) {
          let prev = Promise.resolve();
          for (const msg of messagesToProcess) {
            prev = prev.then(async () => {
              delete msg.retryAfterRestart;
              await processMessage(session.id, msg);
            });
          }
          await prev;
        }
      }
      
      if (hasQueuedMessages) {
        restoredCount++;
        // Clear the restart flag
        delete session.restoredAfterRestart;
      }
    }
    
    if (restoredCount > 0) {
      log(`Restored ${restoredCount} sessions with ${messageCount} queued messages after restart`);
      await persistState();
    } else {
      log('No interrupted sessions to restore');
    }
  } catch (error) {
    log('Failed to restore interrupted sessions', { error: String(error) });
  }
}

async function cleanupOrphanedWorkspaces() {
  try {
    const workspaceRoot = path.resolve(WORKSPACES_ROOT);
    
    // Check if workspaces root exists
    try {
      await fs.access(workspaceRoot);
    } catch {
      log('Workspaces root does not exist, skipping orphaned workspace cleanup');
      return;
    }
    
    // Create a set of all active workspace directories from current sessions
    const activeWorkspaceDirs = new Set();
    for (const session of state.sessions) {
      if (session.workspaceDir) {
        const normalizedPath = path.resolve(session.workspaceDir);
        if (normalizedPath.startsWith(workspaceRoot)) {
          activeWorkspaceDirs.add(normalizedPath);
        }
      }
    }
    
    let cleanedCount = 0;
    let errorCount = 0;
    
    // Recursively walk through workspaces directory
    async function scanAndClean(dirPath) {
      const entries = await fs.readdir(dirPath, { withFileTypes: true });
      
      for (const entry of entries) {
        const fullPath = path.join(dirPath, entry.name);
        const normalizedPath = path.resolve(fullPath);
        
        if (!normalizedPath.startsWith(workspaceRoot)) {
          continue; // Skip paths outside workspace root
        }
        
        if (entry.isDirectory()) {
          // Check if this directory is an active workspace
          if (activeWorkspaceDirs.has(normalizedPath)) {
            // This is an active workspace, skip it but don't recurse into it
            continue;
          }
          
          // Recursively scan subdirectories
          await scanAndClean(fullPath);
          
          // After scanning children, if this directory is empty (we cleaned all children) and not active, delete it
          try {
            const remainingEntries = await fs.readdir(fullPath);
            if (remainingEntries.length === 0) {
              await fs.rm(fullPath, { recursive: true, force: true });
              cleanedCount++;
              log('Cleaned orphaned workspace directory', { path: fullPath });
            }
          } catch (err) {
            errorCount++;
            log('Failed to check/clean orphaned workspace', { path: fullPath, error: String(err) });
          }
        }
      }
    }
    
    await scanAndClean(workspaceRoot);
    
    if (cleanedCount > 0) {
      log(`Orphaned workspace cleanup completed: ${cleanedCount} directories cleaned${errorCount > 0 ? `, ${errorCount} errors` : ''}`);
    } else {
      log('Orphaned workspace cleanup: no orphaned directories found');
    }
  } catch (error) {
    log('Failed to clean orphaned workspaces', { error: String(error) });
  }
}

function sanitizeMessage(message) { if (!message) return ''; return message.toString().trim(); }

async function ensureAssetsDir() { await fs.mkdir(ASSETS_DIR, { recursive: true }); }

async function loadAdminModelStore() {
  try {
    await ensureStateFile();
    await ensureAssetsDir();
    const raw = await fs.readFile(ADMIN_MODELS_FILE, 'utf8').catch(() => '{}');
    const parsed = JSON.parse(raw || '{}');
    
    // Load simple structure
    if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
      opencodeModels = Array.isArray(parsed.opencodeModels) ? parsed.opencodeModels : [];
      publicModels = Array.isArray(parsed.publicModels) ? parsed.publicModels : [];
      adminModels = Array.isArray(parsed.adminModels) ? parsed.adminModels : [];
    } else if (Array.isArray(parsed)) {
      // Old array structure - migrate
      adminModels = parsed;
      opencodeModels = parsed.map((m) => ({
        id: m.id || randomUUID(),
        name: m.name,
        label: m.label || m.name,
        icon: m.icon || '',
        tier: normalizeTier(m.tier),
        supportsMedia: m.supportsMedia ?? false,
        multiplier: getTierMultiplier(m.tier),
      })).filter((m) => !!m.name);
      publicModels = [];
    } else {
      opencodeModels = [];
      publicModels = [];
      adminModels = [];
    }
    
    // Validate opencode models
    opencodeModels = opencodeModels.map((m) => ({
      id: m.id || randomUUID(),
      name: m.name,
      label: m.label || m.name,
      icon: m.icon || '',
      tier: normalizeTier(m.tier),
      supportsMedia: m.supportsMedia ?? false,
      multiplier: m.multiplier || getTierMultiplier(m.tier),
    })).filter((m) => !!m.name);
    
    // Validate public models
    publicModels = publicModels.map((m) => ({
      id: m.id || randomUUID(),
      name: m.name,
      label: m.label || m.name,
      icon: m.icon || '',
      tier: normalizeTier(m.tier),
      supportsMedia: m.supportsMedia ?? false,
      multiplier: m.multiplier || getTierMultiplier(m.tier),
    })).filter((m) => !!m.name);
    
    refreshAdminModelIndex();
    log('Loaded admin model store', { 
      opencodeModels: opencodeModels.length,
      publicModels: publicModels.length,
    });
  } catch (error) {
    log('Failed to load admin models, starting empty', { error: String(error) });
    opencodeModels = [];
    publicModels = [];
    adminModels = [];
    refreshAdminModelIndex();
  }
}

async function persistAdminModels() {
  await ensureStateFile();
  await ensureAssetsDir();
  // Save simple structure
  const payload = JSON.stringify({
    opencodeModels,
    publicModels,
    adminModels,
    version: 5,
  }, null, 2);
  await safeWriteFile(ADMIN_MODELS_FILE, payload);
  refreshAdminModelIndex();
}

async function loadOpenRouterSettings() {
  try {
    await ensureStateFile();
    const raw = await fs.readFile(OPENROUTER_SETTINGS_FILE, 'utf8').catch(() => null);
    if (raw) {
      const parsed = JSON.parse(raw);
      if (typeof parsed.primaryModel === 'string') openrouterSettings.primaryModel = parsed.primaryModel;
      if (typeof parsed.backupModel1 === 'string') openrouterSettings.backupModel1 = parsed.backupModel1;
      if (typeof parsed.backupModel2 === 'string') openrouterSettings.backupModel2 = parsed.backupModel2;
      if (typeof parsed.backupModel3 === 'string') openrouterSettings.backupModel3 = parsed.backupModel3;
    }
  } catch (error) {
    log('Failed to load OpenRouter settings, using defaults', { error: String(error) });
  }
}

async function persistOpenRouterSettings() {
  await ensureStateFile();
  const payload = JSON.stringify(openrouterSettings, null, 2);
  await safeWriteFile(OPENROUTER_SETTINGS_FILE, payload);
}

async function loadMistralSettings() {
  try {
    await ensureStateFile();
    const raw = await fs.readFile(MISTRAL_SETTINGS_FILE, 'utf8').catch(() => null);
    if (raw) {
      const parsed = JSON.parse(raw);
      if (typeof parsed.primaryModel === 'string') mistralSettings.primaryModel = parsed.primaryModel;
      if (typeof parsed.backupModel1 === 'string') mistralSettings.backupModel1 = parsed.backupModel1;
      if (typeof parsed.backupModel2 === 'string') mistralSettings.backupModel2 = parsed.backupModel2;
      if (typeof parsed.backupModel3 === 'string') mistralSettings.backupModel3 = parsed.backupModel3;
    }
  } catch (error) {
    log('Failed to load Mistral settings, using defaults', { error: String(error) });
  }
}

async function persistMistralSettings() {
  await ensureStateFile();
  const payload = JSON.stringify(mistralSettings, null, 2);
  await safeWriteFile(MISTRAL_SETTINGS_FILE, payload);
}

async function loadPlanSettings() {
  try {
    await ensureStateFile();
    const raw = await fs.readFile(path.join(STATE_DIR, 'plan-settings.json'), 'utf8').catch(() => null);
    if (raw) {
      const parsed = JSON.parse(raw);
      if (typeof parsed.provider === 'string' && PLANNING_PROVIDERS.includes(normalizeProviderName(parsed.provider))) {
        planSettings.provider = normalizeProviderName(parsed.provider);
      }
      if (typeof parsed.freePlanModel === 'string') {
        planSettings.freePlanModel = parsed.freePlanModel.trim();
      }
      if (Array.isArray(parsed.planningChain)) {
        planSettings.planningChain = normalizePlanningChain(parsed.planningChain);
      }
    }
    if (!planSettings.planningChain.length) {
      planSettings.planningChain = defaultPlanningChainFromSettings(planSettings.provider);
    }
  } catch (error) {
    log('Failed to load plan settings, using defaults', { error: String(error) });
  }
}

async function persistPlanSettings() {
  await ensureStateFile();
  const payload = JSON.stringify(planSettings, null, 2);
  await safeWriteFile(path.join(STATE_DIR, 'plan-settings.json'), payload);
}

function normalizePlanningChain(chain) {
  if (!Array.isArray(chain)) return [];
  const seen = new Set();
  const normalized = [];
  for (const entry of chain) {
    // Start with provider from the entry or fallback to planSettings
    let provider = normalizeProviderName(entry?.provider || planSettings.provider || 'openrouter');
    if (!PLANNING_PROVIDERS.includes(provider)) continue;

    // Preserve the original user input (raw) while still normalizing a model
    // for runtime usage. `raw` lets the admin UI show exactly what was entered
    // (e.g., "groq/compound-mini") even if the normalized model value omits
    // the provider prefix for API calls.
    const rawModel = (typeof entry?.raw === 'string' && entry.raw.trim())
      ? entry.raw.trim()
      : (typeof entry?.model === 'string' ? entry.model.trim() : '');

    const parsed = parseModelString(rawModel);
    if (parsed.provider) {
      provider = parsed.provider;
    }
    const model = parsed.model || rawModel;

    const key = `${provider}::${model || '__any__'}`;
    if (seen.has(key)) continue;
    seen.add(key);
    // Store both a normalized `model` (used at runtime) and the original `raw`
    // input so the admin UI can display what the user actually typed.
    normalized.push({ provider, model, raw: rawModel || undefined });
    if (normalized.length >= 25) break;
  }
  return normalized;
}

function defaultPlanningChainFromSettings(preferredProvider) {
  const provider = PLANNING_PROVIDERS.includes(normalizeProviderName(preferredProvider))
    ? normalizeProviderName(preferredProvider)
    : 'openrouter';

  let primaryChain = [];
  if (provider === 'mistral') {
    primaryChain = buildMistralPlanChain();
  } else if (provider === 'groq') {
    primaryChain = buildGroqPlanChain();
  } else if (provider === 'google') {
    primaryChain = buildGooglePlanChain();
  } else if (provider === 'nvidia') {
    primaryChain = buildNvidiaPlanChain();
  } else {
    primaryChain = buildOpenRouterPlanChain();
  }

  const base = primaryChain.map((model) => ({ provider, model }));

  // Add OpenRouter as fallback for non-OpenRouter providers
  if (provider !== 'openrouter') {
    buildOpenRouterPlanChain().forEach((model) => base.push({ provider: 'openrouter', model }));
  }

  return normalizePlanningChain(base);
}

function normalizeProviderName(name) {
  return (name || '').toString().trim().toLowerCase() || 'opencode';
}

const KNOWN_USAGE_PROVIDERS = new Set(['openrouter', 'mistral', 'opencode', 'google', 'groq', 'nvidia', 'cohere']);

// Treat unknown "provider" labels that are really OpenRouter model families (e.g. openai/anthropic)
// as OpenRouter for usage + rate-limits, so admin charts roll up correctly.
function normalizeUsageProvider(provider, model = '') {
  const key = normalizeProviderName(provider);
  if (KNOWN_USAGE_PROVIDERS.has(key)) return key;
  const modelStr = (model || '').toString();
  if (modelStr.includes('/')) return 'openrouter';
  return key;
}

function sanitizeLimitNumber(value) {
  const num = Number(value);
  return Number.isFinite(num) && num > 0 ? num : 0;
}

function defaultProviderLimit(provider) {
  return {
    provider: normalizeProviderName(provider),
    scope: 'provider',
    tokensPerMinute: 0,
    tokensPerHour: 0,
    tokensPerDay: 0,
    requestsPerMinute: 0,
    requestsPerHour: 0,
    requestsPerDay: 0,
    perModel: {},
  };
}

function extractProviderName(p) {
  return normalizeProviderName((p && (p.provider || p.name)) || DEFAULT_PROVIDER_FALLBACK);
}

function collectProviderSeeds() {
  const seeds = new Set(DEFAULT_PROVIDER_SEEDS);
  const validProviders = new Set(DEFAULT_PROVIDER_SEEDS);
  // Only include providers from limits/usage that are valid configured providers
  // This prevents "moonshot" or "z-ai" from being treated as providers
  Object.keys(providerLimits.limits || {}).forEach((p) => {
    const normalized = normalizeProviderName(p);
    if (validProviders.has(normalized)) seeds.add(normalized);
  });
  Object.keys(providerUsage || {}).forEach((p) => {
    const normalized = normalizeProviderName(p);
    if (validProviders.has(normalized)) seeds.add(normalized);
  });
  // Collect providers from opencode models (checking for provider prefixes in model names)
  opencodeModels.forEach((m) => {
    if (m.name && m.name.includes('/')) {
      const providerName = extractProviderName(m.name);
      if (validProviders.has(providerName)) seeds.add(providerName);
    }
  });
  
  // Also check legacy adminModels
  adminModels.forEach((m) => {
    if (m.name && m.name.includes('/')) {
      const providerName = extractProviderName(m.name);
      if (validProviders.has(providerName)) seeds.add(providerName);
    }
    (m.providers || []).forEach((p) => {
      const providerName = extractProviderName(p);
      if (validProviders.has(providerName)) seeds.add(providerName);
    });
  });
  (planSettings.planningChain || []).forEach((entry) => {
    const normalized = normalizeProviderName(entry.provider);
    if (validProviders.has(normalized)) seeds.add(normalized);
  });
  return Array.from(seeds);
}

function ensureProviderLimitDefaults(provider) {
  const key = normalizeProviderName(provider);
  if (!providerLimits.limits[key]) providerLimits.limits[key] = defaultProviderLimit(key);
  const cfg = providerLimits.limits[key];
  cfg.scope = cfg.scope === 'model' ? 'model' : 'provider';
  cfg.tokensPerMinute = sanitizeLimitNumber(cfg.tokensPerMinute);
  cfg.tokensPerHour = sanitizeLimitNumber(cfg.tokensPerHour);
  cfg.tokensPerDay = sanitizeLimitNumber(cfg.tokensPerDay);
  cfg.requestsPerMinute = sanitizeLimitNumber(cfg.requestsPerMinute);
  cfg.requestsPerHour = sanitizeLimitNumber(cfg.requestsPerHour);
  cfg.requestsPerDay = sanitizeLimitNumber(cfg.requestsPerDay);
  cfg.perModel = cfg.perModel && typeof cfg.perModel === 'object' ? cfg.perModel : {};
  Object.keys(cfg.perModel).forEach((model) => {
    const entry = cfg.perModel[model] || {};
    cfg.perModel[model] = {
      tokensPerMinute: sanitizeLimitNumber(entry.tokensPerMinute),
      tokensPerHour: sanitizeLimitNumber(entry.tokensPerHour),
      tokensPerDay: sanitizeLimitNumber(entry.tokensPerDay),
      requestsPerMinute: sanitizeLimitNumber(entry.requestsPerMinute),
      requestsPerHour: sanitizeLimitNumber(entry.requestsPerHour),
      requestsPerDay: sanitizeLimitNumber(entry.requestsPerDay),
    };
  });
  return cfg;
}

async function discoverProviderModels() {
  const map = new Map();
  const add = (provider, model) => {
    const key = normalizeProviderName(provider || DEFAULT_PROVIDER_FALLBACK);
    if (!map.has(key)) map.set(key, new Set());
    if (model) map.get(key).add(model);
  };

  collectProviderSeeds().forEach((p) => add(p));

  // Collect models from opencodeModels (checking for provider prefixes)
  opencodeModels.forEach((m) => {
    if (m.name && m.name.includes('/')) {
      add(extractProviderName(m.name), m.name);
    }
  });
  
  // Also check legacy adminModels
  adminModels.forEach((m) => {
    (m.providers || []).forEach((p) => add(extractProviderName(p), p.model || m.name));
  });

  (planSettings.planningChain || []).forEach((entry) => {
    add(entry.provider, entry.model);
  });

  try {
    const models = await listModels(DEFAULT_PROVIDER_FALLBACK);
    // Build a set of valid configured providers to avoid treating model name prefixes as providers
    const validProviders = new Set(DEFAULT_PROVIDER_SEEDS);
    models.forEach((m) => {
      // listModels may return strings or objects; handle both.
      let name = m.name || m.id || m;
      if (!name || typeof name !== 'string') return;
      
      // Handle OpenRouter model name formats:
      // 1. "openrouter/z-ai/glm-4.7" -> provider: openrouter, model: openrouter/z-ai/glm-4.7
      // 2. "z-ai/glm-4.7 (OpenRouter)" -> provider: openrouter, model: z-ai/glm-4.7
      // 3. "z-ai/glm-4.7" without prefix/label -> provider: openrouter (detected from slash), model: z-ai/glm-4.7
      
      const openRouterLabelMatch = name.match(/\s*\(openrouter\)\s*$/i);
      if (openRouterLabelMatch) {
        // Format: "z-ai/glm-4.7 (OpenRouter)" - strip the label and treat as openrouter
        const cleanName = name.slice(0, openRouterLabelMatch.index).trim();
        add('openrouter', cleanName);
        return;
      }
      
      const parts = name.split('/');
      if (parts.length > 1 && parts[0] && validProviders.has(parts[0].toLowerCase())) {
        // The first part is a known configured provider (e.g., "openrouter/z-ai/glm-4.7")
        add(parts[0], name);
      } else if (parts.length > 1) {
        // Model has a slash but first part is not a known provider (e.g., "z-ai/glm-4.7")
        // This is likely an OpenRouter model without the prefix
        add('openrouter', name);
      } else {
        // No slash in name, use default provider
        add(DEFAULT_PROVIDER_FALLBACK, name);
      }
    });
  } catch (err) {
    log('provider discovery failed', { error: String(err) });
  }

  const providers = Array.from(map.keys()).sort();
  providers.forEach((p) => ensureProviderLimitDefaults(p));

  const providerModels = {};
  providers.forEach((p) => {
    providerModels[p] = Array.from(map.get(p) || []).sort();
  });

  return { providers, providerModels };
}

async function loadProviderLimits() {
  try {
    await ensureStateFile();
    const raw = await fs.readFile(PROVIDER_LIMITS_FILE, 'utf8').catch(() => null);
    if (raw) {
      const parsed = JSON.parse(raw);
      if (parsed && typeof parsed === 'object') providerLimits = { ...providerLimits, ...parsed };
    }
  } catch (error) {
    log('Failed to load provider limits, using defaults', { error: String(error) });
  }
  
  // Clean up any invalid providers that may have been saved previously
  // (e.g., "moonshot", "z-ai" that were incorrectly extracted from model names)
  const validProviders = new Set(DEFAULT_PROVIDER_SEEDS);
  if (providerLimits.limits) {
    Object.keys(providerLimits.limits).forEach((key) => {
      if (!validProviders.has(normalizeProviderName(key))) {
        delete providerLimits.limits[key];
        log('Removed invalid provider from limits', { provider: key });
      }
    });
  }
  
  collectProviderSeeds().forEach((p) => ensureProviderLimitDefaults(p));
  if (!providerLimits.opencodeBackupModel) providerLimits.opencodeBackupModel = '';
}

async function persistProviderLimits() {
  await ensureStateFile();
  collectProviderSeeds().forEach((p) => ensureProviderLimitDefaults(p));
  const payload = JSON.stringify(providerLimits, null, 2);
  await safeWriteFile(PROVIDER_LIMITS_FILE, payload);
}

function ensureProviderUsageBucket(provider) {
  const key = normalizeProviderName(provider);
  if (!providerUsage[key]) providerUsage[key] = [];
  return providerUsage[key];
}

function mergeLegacyProviderUsageIntoOpenRouter() {
  // Historical/config-driven provider keys like "openai" or "anthropic" should be rolled up
  // into OpenRouter usage when the model name looks like an OpenRouter model.
  const openrouterBucket = ensureProviderUsageBucket('openrouter');
  Object.keys(providerUsage || {}).forEach((providerKey) => {
    const normalized = normalizeProviderName(providerKey);
    if (KNOWN_USAGE_PROVIDERS.has(normalized)) return;
    const entries = Array.isArray(providerUsage[providerKey]) ? providerUsage[providerKey] : [];
    if (!entries.length) return;
    const keep = [];
    entries.forEach((entry) => {
      const model = (entry && entry.model) ? String(entry.model) : '';
      if (model.includes('/')) openrouterBucket.push(entry);
      else keep.push(entry);
    });
    if (keep.length) providerUsage[providerKey] = keep;
    else delete providerUsage[providerKey];
  });
  pruneProviderUsage();
}

function pruneProviderUsage(now = Date.now()) {
  const cutoff = now - FORTY_EIGHT_HOURS_MS; // keep last 48h for reporting
  Object.keys(providerUsage).forEach((provider) => {
    providerUsage[provider] = (providerUsage[provider] || []).filter((entry) => entry && typeof entry.ts === 'number' && entry.ts >= cutoff);
  });
}

async function loadProviderUsage() {
  try {
    await ensureStateFile();
    const raw = await fs.readFile(PROVIDER_USAGE_FILE, 'utf8').catch(() => null);
    if (raw) {
      const parsed = JSON.parse(raw);
      if (parsed && typeof parsed === 'object') providerUsage = parsed;
    }
  } catch (error) {
    log('Failed to load provider usage, starting empty', { error: String(error) });
    providerUsage = {};
  }
  pruneProviderUsage();
  mergeLegacyProviderUsageIntoOpenRouter();
  collectProviderSeeds().forEach((p) => ensureProviderUsageBucket(p));
}

async function persistProviderUsage() {
  pruneProviderUsage();
  await ensureStateFile();
  const payload = JSON.stringify(providerUsage, null, 2);
  try {
    await safeWriteFile(PROVIDER_USAGE_FILE, payload);
  } catch (err) {
    log('Failed to persist provider usage', { error: String(err) });
  }
}

function currentMonthKey(date = new Date()) {
  const yr = date.getFullYear();
  const mo = String(date.getMonth() + 1).padStart(2, '0');
  return `${yr}-${mo}`;
}

function getModelTier(modelName) {
  if (!modelName) return null;
  const target = getAdminModelByIdOrName(modelName);
  return target ? target.tier : null;
}

function ensureTokenUsageBucket(userId) {
  const key = String(userId || '');
  if (!key) return null;
  const month = currentMonthKey();
  if (!tokenUsage[key] || tokenUsage[key].month !== month) {
    tokenUsage[key] = {
      month,
      usage: 0,
      addOns: 0,
      paygBilled: 0,
      tokenOverride: null,
    };
  } else {
    const entry = tokenUsage[key];
    entry.usage = typeof entry.usage === 'number' ? entry.usage : 0;
    entry.addOns = typeof entry.addOns === 'number' ? entry.addOns : 0;
    entry.paygBilled = typeof entry.paygBilled === 'number' ? entry.paygBilled : 0;
    if (entry.tokenOverride !== null && typeof entry.tokenOverride !== 'number') {
      entry.tokenOverride = null;
    }
  }
  return tokenUsage[key];
}

function ensureExternalTestUsageBucket(userId) {
  const key = String(userId || '');
  if (!key) return null;
  const month = currentMonthKey();
  if (!externalTestUsage[key] || externalTestUsage[key].month !== month) {
    externalTestUsage[key] = { month, count: 0 };
  } else {
    const entry = externalTestUsage[key];
    entry.count = typeof entry.count === 'number' ? entry.count : 0;
  }
  return externalTestUsage[key];
}

function getExternalTestUsageSummary(userId, plan) {
  const bucket = ensureExternalTestUsageBucket(userId) || { count: 0, month: currentMonthKey() };
  const normalizedPlan = normalizePlanSelection(plan) || DEFAULT_PLAN;
  const limit = EXTERNAL_TEST_LIMITS[normalizedPlan] ?? EXTERNAL_TEST_LIMITS[DEFAULT_PLAN];
  const used = Math.max(0, Number(bucket.count || 0));
  const remaining = Number.isFinite(limit) ? Math.max(0, limit - used) : 0;
  const percent = Number.isFinite(limit) && limit > 0 ? Math.min(100, Math.round((used / limit) * 100)) : 0;
  return {
    month: bucket.month,
    plan: normalizedPlan,
    used,
    limit,
    remaining,
    percent,
  };
}

function canUseExternalTesting(userId, plan, unlimited = false) {
  if (unlimited) return { allowed: true, summary: getExternalTestUsageSummary(userId, plan) };
  const summary = getExternalTestUsageSummary(userId, plan);
  if (!Number.isFinite(summary.limit)) return { allowed: true, summary };
  if (summary.used >= summary.limit) {
    return { allowed: false, summary };
  }
  return { allowed: true, summary };
}

function normalizeTier(tier) {
  const normalized = (tier || '').toLowerCase();
  return ['free', 'plus', 'pro'].includes(normalized) ? normalized : 'free';
}

function getTierMultiplier(tier) {
  const normalizedTier = normalizeTier(tier);
  return normalizedTier === 'pro' ? 3 : (normalizedTier === 'plus' ? 2 : 1);
}

async function loadTokenUsage() {
  try {
    await ensureStateFile();
    const raw = await fs.readFile(TOKEN_USAGE_FILE, 'utf8').catch(() => null);
    if (raw) {
      const parsed = JSON.parse(raw);
      if (parsed && typeof parsed === 'object') tokenUsage = parsed;
    }
  } catch (error) {
    log('Failed to load token usage, starting empty', { error: String(error) });
    tokenUsage = {};
  }
}

async function loadExternalTestUsage() {
  try {
    await ensureStateFile();
    const raw = await fs.readFile(EXTERNAL_TEST_USAGE_FILE, 'utf8').catch(() => null);
    if (raw) {
      const parsed = JSON.parse(raw);
      if (parsed && typeof parsed === 'object') externalTestUsage = parsed;
    }
  } catch (error) {
    log('Failed to load external test usage, starting empty', { error: String(error) });
    externalTestUsage = {};
  }
}

async function persistTokenUsage() {
  await ensureStateFile();
  const payload = JSON.stringify(tokenUsage, null, 2);
  try {
    await safeWriteFile(TOKEN_USAGE_FILE, payload);
  } catch (err) {
    log('Failed to persist token usage', { error: String(err) });
  }
}

async function persistExternalTestUsage() {
  await ensureStateFile();
  const payload = JSON.stringify(externalTestUsage, null, 2);
  try {
    await safeWriteFile(EXTERNAL_TEST_USAGE_FILE, payload);
  } catch (err) {
    log('Failed to persist external test usage', { error: String(err) });
  }
}

async function loadTopupSessions() {
  try {
    await ensureStateFile();
    const raw = await fs.readFile(TOPUP_SESSIONS_FILE, 'utf8').catch(() => null);
    if (raw) {
      const parsed = JSON.parse(raw);
      if (parsed && typeof parsed === 'object') processedTopups = parsed;
    }
  } catch (error) {
    processedTopups = {};
    log('Failed to load top-up sessions, starting empty', { error: String(error) });
  }
}

async function persistTopupSessions() {
  await ensureStateFile();
  const payload = JSON.stringify(processedTopups, null, 2);
  try {
    await safeWriteFile(TOPUP_SESSIONS_FILE, payload);
  } catch (err) {
    log('Failed to persist top-up sessions', { error: String(err) });
  }
}

async function loadPendingTopups() {
  try {
    await ensureStateFile();
    const raw = await fs.readFile(TOPUP_PENDING_FILE, 'utf8').catch(() => null);
    if (raw) {
      const parsed = JSON.parse(raw);
      if (parsed && typeof parsed === 'object') pendingTopups = parsed;
    }
  } catch (error) {
    pendingTopups = {};
    log('Failed to load pending top-ups, starting empty', { error: String(error) });
  }
}

async function persistPendingTopups() {
  await ensureStateFile();
  const payload = JSON.stringify(pendingTopups, null, 2);
  try {
    await safeWriteFile(TOPUP_PENDING_FILE, payload);
  } catch (err) {
    log('Failed to persist pending top-ups', { error: String(err) });
  }
}

async function loadInvoicesDb() {
  try {
    await ensureStateFile();
    await fs.mkdir(INVOICES_DIR, { recursive: true });
    const raw = await fs.readFile(INVOICES_FILE, 'utf8').catch(() => null);
    if (raw) {
      const parsed = JSON.parse(raw);
      if (parsed && Array.isArray(parsed)) invoicesDb = parsed;
    }
  } catch (error) {
    invoicesDb = [];
    log('Failed to load invoices, starting empty', { error: String(error) });
  }
}

async function persistInvoicesDb() {
  await ensureStateFile();
  const payload = JSON.stringify(invoicesDb, null, 2);
  try {
    await safeWriteFile(INVOICES_FILE, payload);
  } catch (err) {
    log('Failed to persist invoices', { error: String(err) });
  }
}

function generateInvoiceNumber() {
  const date = new Date();
  const year = date.getFullYear();
  const month = String(date.getMonth() + 1).padStart(2, '0');
  const count = invoicesDb.filter(inv => inv.invoiceNumber?.startsWith(`INV-${year}${month}`)).length + 1;
  return `INV-${year}${month}-${String(count).padStart(4, '0')}`;
}

function normalizeInvoiceSource(source) {
  if (!source || typeof source !== 'object') return null;
  const provider = String(source.provider || 'dodo').toLowerCase();
  const normalized = { provider };
  for (const key of ['orderId', 'checkoutId', 'paymentId', 'eventId', 'subscriptionId']) {
    if (source[key]) normalized[key] = String(source[key]);
  }
  const hasIdentifier = Object.keys(normalized).some((key) => key !== 'provider');
  return hasIdentifier ? normalized : null;
}

function findInvoiceBySource(userId, source) {
  const normalized = normalizeInvoiceSource(source);
  if (!normalized) return null;
  const matchOrder = ['paymentId', 'eventId', 'orderId', 'checkoutId'];

  return invoicesDb.find((invoice) => {
    if (!invoice || invoice.userId !== userId) return false;
    const invSource = invoice.details?.source;
    if (!invSource || typeof invSource !== 'object') return false;
    if (String(invSource.provider || 'dodo').toLowerCase() !== normalized.provider) return false;

    return matchOrder.some((key) => normalized[key] && invSource[key] && String(invSource[key]) === normalized[key]);
  }) || null;
}

async function createInvoiceIfMissing(user, type, details) {
  const existing = findInvoiceBySource(user.id, details?.source);
  if (existing) return existing;
  return createInvoice(user, type, details);
}

async function createInvoice(user, type, details) {
  const invoiceNumber = generateInvoiceNumber();
  const amountRaw = Number(details?.amount);
  const amount = Number.isFinite(amountRaw) ? Math.max(0, Math.round(amountRaw)) : 0;
  const currency = String(details?.currency || 'usd').toUpperCase();
  const invoice = {
    id: randomUUID(),
    invoiceNumber,
    userId: user.id,
    email: user.email,
    type,
    status: 'paid',
    amount,
    currency,
    createdAt: new Date().toISOString(),
    dueDate: new Date().toISOString(),
    paidAt: new Date().toISOString(),
    details: {}
  };

  const source = normalizeInvoiceSource(details?.source);
  if (source) invoice.details.source = source;

  if (type === 'topup') {
    const tokenCount = Number(details?.tokens);
    const tokenLabel = Number.isFinite(tokenCount) ? tokenCount.toLocaleString() : '0';
    invoice.description = `Top-up - ${tokenLabel} tokens`;
    invoice.details.tokens = Number.isFinite(tokenCount) ? tokenCount : 0;
    if (details?.tier) invoice.details.tier = details.tier;
  } else if (type === 'subscription') {
    invoice.description = `Subscription - ${details?.plan || 'plan'} (${details?.billingCycle || 'monthly'})`;
    invoice.details.plan = details?.plan;
    invoice.details.billingCycle = details?.billingCycle;
  } else if (type === 'payg') {
    const tokenCount = Number(details?.tokens);
    const tokenLabel = Number.isFinite(tokenCount) ? tokenCount.toLocaleString() : '0';
    invoice.description = `Pay-as-you-go - ${tokenLabel} tokens`;
    invoice.details.tokens = Number.isFinite(tokenCount) ? tokenCount : 0;
  }

  invoicesDb.push(invoice);
  await persistInvoicesDb();

  const pdfPath = path.join(INVOICES_DIR, `${invoice.id}.pdf`);
  await generateInvoicePdf(invoice, user, pdfPath);

  return invoice;
}

async function generateInvoicePdf(invoice, user, outputPath) {
  return new Promise((resolve, reject) => {
    const doc = new PDFDocument({
      size: 'A4',
      margins: { top: 50, bottom: 50, left: 50, right: 50 },
      compress: true,
      info: {
        Title: `Invoice ${invoice.invoiceNumber}`,
        Author: 'Plugin Compass',
        Subject: invoice.description,
        Creator: 'Plugin Compass'
      }
    });

    const stream = require('fs').createWriteStream(outputPath);
    doc.pipe(stream);

    const green = '#008060';
    const darkGreen = '#004c3f';
    const gray = '#6b7280';
    const lightGray = '#f3f4f6';

    doc.fontSize(24).fillColor(green).font('Helvetica-Bold').text('Plugin Compass', 50, 50);
    doc.moveDown(0.5);

    doc.fontSize(10).fillColor(gray).font('Helvetica').text('Invoice', 50, null);
    doc.fontSize(28).fillColor('#1f2937').font('Helvetica-Bold').text(invoice.invoiceNumber, 50, null);
    doc.moveDown(0.5);

    doc.fontSize(10).fillColor(gray).font('Helvetica').text('Date:', 50, null);
    doc.fontSize(12).fillColor('#1f2937').font('Helvetica').text(new Date(invoice.createdAt).toLocaleDateString(), 50, null);
    doc.moveDown(0.3);

    doc.fontSize(10).fillColor(gray).font('Helvetica').text('Due Date:', 50, null);
    doc.fontSize(12).fillColor('#1f2937').font('Helvetica').text(new Date(invoice.dueDate).toLocaleDateString(), 50, null);

    const y = 50;
    const rightX = 400;
    doc.fontSize(10).fillColor(gray).font('Helvetica').text('Bill To:', rightX, y);
    doc.fontSize(12).fillColor('#1f2937').font('Helvetica-Bold').text(user.email || 'N/A', rightX, null);

    doc.moveDown(1.5);

    doc.rect(50, doc.y, 495, 0).fill(lightGray);
    doc.fill(green).fontSize(10).font('Helvetica-Bold').text('Description', 60, doc.y - 24, { width: 250 });
    doc.text('Amount', 430, doc.y, { width: 100 });
    doc.rect(50, doc.y - 24, 495, 24).strokeColor('#e5e7eb').lineWidth(0.5).stroke();

    doc.moveDown(0.5);

    doc.fontSize(10).fillColor('#374151').font('Helvetica').text(invoice.description, 60, null, { width: 250 });
    const amountText = `${invoice.currency} ${(invoice.amount / 100).toFixed(2)}`;
    doc.text(amountText, 430, doc.y - 14, { width: 100, align: 'right' });

    doc.moveDown(0.5);
    doc.rect(50, doc.y - 12, 495, 0).strokeColor('#e5e7eb').lineWidth(0.5).stroke();

    const totalY = doc.y + 20;
    doc.fontSize(10).fillColor(gray).font('Helvetica').text('Total:', 430, totalY);
    doc.fontSize(16).fillColor(green).font('Helvetica-Bold').text(amountText, 430, totalY + 15, { width: 100, align: 'right' });

    doc.moveDown(3);

    doc.fontSize(9).fillColor(gray).font('Helvetica').text('Payment Status: PAID', 50, null);

    doc.moveDown(2);
    doc.fontSize(9).fillColor(gray).font('Helvetica').text('Thank you for your payment!', 50, null);

    doc.moveDown(1);
    doc.fontSize(8).fillColor('#9ca3af').font('Helvetica').text('Plugin Compass © ' + new Date().getFullYear() + '. All rights reserved.', 50, null);

    doc.end();

    stream.on('finish', resolve);
    stream.on('error', reject);
  });
}

function getInvoicesByUserId(userId) {
  return invoicesDb
    .filter(inv => inv.userId === userId)
    .sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
}

async function loadPaygSessions() {
  try {
    await ensureStateFile();
    const raw = await fs.readFile(PAYG_SESSIONS_FILE, 'utf8').catch(() => null);
    if (raw) {
      const parsed = JSON.parse(raw);
      if (parsed && typeof parsed === 'object') processedPayg = parsed;
    }
  } catch (error) {
    processedPayg = {};
    log('Failed to load pay-as-you-go sessions, starting empty', { error: String(error) });
  }
}

async function persistPaygSessions() {
  await ensureStateFile();
  const payload = JSON.stringify(processedPayg, null, 2);
  try {
    await safeWriteFile(PAYG_SESSIONS_FILE, payload);
  } catch (err) {
    log('Failed to persist pay-as-you-go sessions', { error: String(err) });
  }
}

async function loadPendingPayg() {
  try {
    await ensureStateFile();
    const raw = await fs.readFile(PAYG_PENDING_FILE, 'utf8').catch(() => null);
    if (raw) {
      const parsed = JSON.parse(raw);
      if (parsed && typeof parsed === 'object') pendingPayg = parsed;
    }
  } catch (error) {
    pendingPayg = {};
    log('Failed to load pending pay-as-you-go sessions, starting empty', { error: String(error) });
  }
}

async function persistPendingPayg() {
  await ensureStateFile();
  const payload = JSON.stringify(pendingPayg, null, 2);
  try {
    await safeWriteFile(PAYG_PENDING_FILE, payload);
  } catch (err) {
    log('Failed to persist pending pay-as-you-go sessions', { error: String(err) });
  }
}

// Subscription persistence functions
async function loadSubscriptionSessions() {
  try {
    await ensureStateFile();
    const raw = await fs.readFile(SUBSCRIPTION_SESSIONS_FILE, 'utf8').catch(() => null);
    if (raw) {
      const parsed = JSON.parse(raw);
      if (parsed && typeof parsed === 'object') processedSubscriptions = parsed;
    }
  } catch (error) {
    processedSubscriptions = {};
    log('Failed to load subscription sessions, starting empty', { error: String(error) });
  }
}

async function loadPendingSubscriptions() {
  try {
    await ensureStateFile();
    const raw = await fs.readFile(SUBSCRIPTION_PENDING_FILE, 'utf8').catch(() => null);
    if (raw) {
      const parsed = JSON.parse(raw);
      if (parsed && typeof parsed === 'object') pendingSubscriptions = parsed;
    }
  } catch (error) {
    pendingSubscriptions = {};
    log('Failed to load pending subscriptions, starting empty', { error: String(error) });
  }
}

async function persistPendingSubscriptions() {
  await ensureStateFile();
  const payload = JSON.stringify(pendingSubscriptions, null, 2);
  try {
    await safeWriteFile(SUBSCRIPTION_PENDING_FILE, payload);
  } catch (err) {
    log('Failed to persist pending subscriptions', { error: String(err) });
  }
}

async function persistProcessedSubscriptions() {
  await ensureStateFile();
  const payload = JSON.stringify(processedSubscriptions, null, 2);
  try {
    await safeWriteFile(SUBSCRIPTION_SESSIONS_FILE, payload);
  } catch (err) {
    log('Failed to persist processed subscriptions', { error: String(err) });
  }
}

async function dodoRequest(pathname, { method = 'GET', body, query } = {}) {
  if (!DODO_ENABLED) throw new Error('Dodo Payments is not configured');
  const url = new URL(`${DODO_BASE_URL}${pathname}`);
  if (query && typeof query === 'object') {
    Object.entries(query).forEach(([key, value]) => {
      if (value === undefined || value === null || value === '') return;
      url.searchParams.set(key, String(value));
    });
  }
  const headers = {
    Authorization: `Bearer ${DODO_PAYMENTS_API_KEY}`,
  };
  if (body) headers['Content-Type'] = 'application/json';
  const res = await fetch(url, {
    method,
    headers,
    body: body ? JSON.stringify(body) : undefined,
  });
  const payload = await res.json().catch(() => ({}));
  if (!res.ok) {
    const detail = payload?.error || payload?.message || res.statusText || 'Dodo request failed';
    const error = new Error(detail);
    error.status = res.status;
    throw error;
  }
  return payload;
}

/**
 * Cancel a Dodo subscription with support for immediate or end-of-period cancellation.
 * @param {object} user - User record containing dodoSubscriptionId.
 * @param {string} reason - Cancellation reason for logging.
 * @param {object} options - Behavior options.
 * @param {boolean} options.clearOnFailure - Whether to clear the subscription ID on failure.
 * @param {string} options.mode - Cancellation mode: 'immediate' or 'at_period_end'. Default: 'immediate'.
 * @returns {Promise<boolean>} True if a cancellation request succeeded.
 */
async function cancelDodoSubscription(user, reason = 'manual', { clearOnFailure = false, mode = 'immediate' } = {}) {
  if (!user?.dodoSubscriptionId) return false;
  const subscriptionId = user.dodoSubscriptionId;

  // Determine cancellation strategy based on mode
  const attempts = mode === 'at_period_end'
    ? [{ method: 'PATCH', body: { cancel_at_next_billing_date: true }, label: 'period_end' }]
    : [
        { method: 'DELETE', body: undefined, label: 'immediate' },
        { method: 'PATCH', body: { cancel_at_next_billing_date: true }, label: 'period_end' },
      ];

  for (const attempt of attempts) {
    try {
      await dodoRequest(`/subscriptions/${subscriptionId}`, attempt);
      log('Dodo subscription cancelled', { userId: user.id, subscriptionId, reason, method: attempt.method, mode: attempt.label });
      
      // Only clear subscription ID immediately for immediate cancellation
      // For period-end cancellation, keep it until webhook confirms actual cancellation
      if (mode === 'immediate') {
        user.dodoSubscriptionId = '';
        await persistUsersDb();
      }
      
      return true;
    } catch (error) {
      log('Failed to cancel Dodo subscription', {
        userId: user.id,
        subscriptionId,
        reason,
        method: attempt.method,
        mode: attempt.label,
        error: String(error)
      });
    }
  }
  
  if (clearOnFailure) {
    user.dodoSubscriptionId = '';
    await persistUsersDb();
  }
  return false;
}

/**
 * Change an existing Dodo subscription to a different plan using the Change Plan API.
 * This properly handles upgrades and downgrades with proration.
 * @param {object} user - User record containing dodoSubscriptionId.
 * @param {string} newPlan - The new plan to change to (starter, professional, enterprise).
 * @param {string} billingCycle - The billing cycle (monthly, yearly).
 * @param {string} currency - The currency (usd, gbp, eur).
 * @returns {Promise<object>} The change plan response from Dodo.
 */
async function changeDodoSubscriptionPlan(user, newPlan, billingCycle, currency) {
  if (!user?.dodoSubscriptionId) {
    throw new Error('No active subscription to change');
  }
  
  const subscriptionId = user.dodoSubscriptionId;
  const product = resolveSubscriptionProduct(newPlan, billingCycle, currency);
  
  if (!product) {
    throw new Error('Invalid plan, billing cycle, or currency combination');
  }
  
  try {
    // Use Dodo's Change Plan API with difference_immediately proration
    // This will charge the difference for upgrades and apply credits for downgrades
    const result = await dodoRequest(`/subscriptions/${subscriptionId}/change-plan`, {
      method: 'POST',
      body: {
        product_id: product.productId,
        quantity: 1,
        proration_billing_mode: 'difference_immediately',
      },
    });
    
    log('Dodo subscription plan changed', {
      userId: user.id,
      subscriptionId,
      oldPlan: user.plan,
      newPlan,
      billingCycle,
      currency,
      productId: product.productId,
    });
    
    return result;
  } catch (error) {
    log('Failed to change Dodo subscription plan', {
      userId: user.id,
      subscriptionId,
      oldPlan: user.plan,
      newPlan,
      error: String(error),
    });
    throw error;
  }
}

async function ensureDodoCustomer(user) {
  if (!user) return '';
  if (user.dodoCustomerId) return user.dodoCustomerId;
  const email = user.billingEmail || user.email || '';
  if (!email) return '';
  
  // First, try to find existing customer by email
  try {
    log('DEBUG: Looking up Dodo customer by email', {
      userId: user.id,
      email: email
    });
    
    const existingCustomers = await dodoRequest('/customers', {
      method: 'GET',
      query: { email: email }
    });
    
    log('DEBUG: Customer lookup response', {
      userId: user.id,
      email: email,
      responseKeys: Object.keys(existingCustomers || {}),
      hasItems: !!existingCustomers?.items,
      itemsCount: existingCustomers?.items?.length || 0,
      rawResponse: JSON.stringify(existingCustomers).substring(0, 500)
    });
    
    // Check if any customers were returned
    if (existingCustomers?.items && existingCustomers.items.length > 0) {
      // Log warning if multiple customers found for same email
      if (existingCustomers.items.length > 1) {
        log('WARNING: Multiple Dodo customers found for same email', {
          userId: user.id,
          email: email,
          customerCount: existingCustomers.items.length,
          customerIds: existingCustomers.items.map(c => c.customer_id || c.id)
        });
      }
      
      // Find the customer with active subscriptions, or use the first one
      let selectedCustomer = existingCustomers.items[0];
      
      // If multiple customers, try to find one with active subscriptions
      if (existingCustomers.items.length > 1) {
        for (const customer of existingCustomers.items) {
          const customerId = customer?.customer_id || customer?.id;
          if (customerId) {
            try {
              const subscriptions = await dodoRequest('/subscriptions', {
                method: 'GET',
                query: { customer_id: customerId, status: 'active' }
              });
              if (subscriptions?.items && subscriptions.items.length > 0) {
                selectedCustomer = customer;
                log('Selected customer with active subscriptions', {
                  userId: user.id,
                  email: email,
                  customerId: customerId,
                  activeSubscriptions: subscriptions.items.length
                });
                break;
              }
            } catch (err) {
              // Continue to next customer
            }
          }
        }
      }
      
      const customerId = selectedCustomer?.customer_id || selectedCustomer?.id || '';
      log('DEBUG: Found customer in items', {
        userId: user.id,
        customerId: customerId,
        totalCustomers: existingCustomers.items.length,
        customerKeys: Object.keys(selectedCustomer || {})
      });
      
      if (customerId) {
        user.dodoCustomerId = customerId;
        await persistUsersDb();
        log('Found existing Dodo customer by email', {
          userId: user.id,
          email: email,
          customerId: customerId,
          totalFound: existingCustomers.items.length
        });
        return customerId;
      }
    } else {
      log('DEBUG: No customers found in Dodo for email', {
        userId: user.id,
        email: email,
        fullResponse: JSON.stringify(existingCustomers)
      });
    }
  } catch (error) {
    // Log the lookup failure but continue to create new customer
    log('Failed to lookup existing Dodo customer by email', {
      userId: user.id,
      email: email,
      error: String(error),
      errorStack: error.stack
    });
  }
  
  // If no existing customer found, create a new one
  const customers = await dodoRequest('/customers', {
    method: 'POST',
    body: {
      email,
      name: email.split('@')[0] || email,
      metadata: { userId: String(user.id) },
    },
  });
  const customerId = customers?.customer_id || customers?.id || '';
  if (customerId) {
    user.dodoCustomerId = customerId;
    await persistUsersDb();
    log('Created new Dodo customer', {
      userId: user.id,
      email: email,
      customerId: customerId
    });
  }
  return customerId;
}

async function ingestUsageEvent({ user, tokens, amountCents, currency, ratePerMillion, plan, month }) {
  if (!DODO_ENABLED || !DODO_USAGE_EVENTS_ENABLED) return false;
  if (!user || !Number.isFinite(tokens) || tokens <= 0) return false;
  if (!Number.isFinite(amountCents) || amountCents <= 0) return false;
  const customerId = await ensureDodoCustomer(user);
  if (!customerId) return false;

  const metadata = {
    [DODO_USAGE_EVENT_TOKENS_FIELD]: Math.max(0, Math.round(tokens)),
    [DODO_USAGE_EVENT_COST_FIELD]: Math.max(0, Math.round(amountCents)),
    currency: String(currency || 'usd').toLowerCase(),
    ratePerMillion: Math.max(0, Number(ratePerMillion || 0)),
    plan: String(plan || '').toLowerCase(),
    month: String(month || ''),
    userId: String(user.id),
  };

  const event = {
    event_id: `usage_${randomUUID()}`,
    customer_id: customerId,
    event_name: DODO_USAGE_EVENT_NAME,
    timestamp: new Date().toISOString(),
    metadata,
  };

  await dodoRequest('/events/ingest', {
    method: 'POST',
    body: { events: [event] },
  });
  return true;
}

async function refreshDodoProductsCache() {
  const now = Date.now();
  if (dodoProductCache.items.length && (now - dodoProductCache.fetchedAt) < DODO_PRODUCTS_CACHE_TTL_MS) {
    return dodoProductCache;
  }
  const items = [];
  const pageSize = 100;
  for (let page = 0; page < 10; page += 1) {
    const data = await dodoRequest('/products', {
      method: 'GET',
      query: { page_size: pageSize, page_number: page },
    });
    const list = Array.isArray(data?.items) ? data.items : [];
    items.push(...list);
    if (list.length < pageSize) break;
  }
  const byId = new Map();
  items.forEach((item) => {
    if (item?.product_id) byId.set(String(item.product_id), item);
  });
  dodoProductCache = { fetchedAt: now, items, byId };
  return dodoProductCache;
}

async function getDodoProductById(productId) {
  if (!productId) return null;
  const cache = await refreshDodoProductsCache();
  if (cache.byId.has(productId)) return cache.byId.get(productId);
  return null;
}

// Subscription product resolution
function resolveSubscriptionProduct(plan, billingCycle, currency) {
  const normalizedPlan = String(plan || '').toLowerCase();
  const normalizedBilling = String(billingCycle || '').toLowerCase();
  const normalizedCurrency = String(currency || '').toLowerCase();
  
  // Only support paid plans for subscriptions
  if (!PAID_PLANS.has(normalizedPlan)) return null;
  if (!BILLING_CYCLES.includes(normalizedBilling)) return null;
  if (!SUPPORTED_CURRENCIES.includes(normalizedCurrency)) return null;
  
  const productKey = `${normalizedPlan}_${normalizedBilling}_${normalizedCurrency}`;
  const productId = SUBSCRIPTION_PRODUCT_IDS[productKey];
  const price = SUBSCRIPTION_PRICES[productKey];
  
  if (!productId || !price) return null;
  
  return {
    plan: normalizedPlan,
    billingCycle: normalizedBilling,
    currency: normalizedCurrency,
    productId,
    price,
    productKey,
  };
}

// Get all available subscription products for a plan
function getAvailableSubscriptionProducts(plan) {
  const normalizedPlan = String(plan || '').toLowerCase();
  if (!PAID_PLANS.has(normalizedPlan)) return [];
  
  const products = [];
  for (const billing of BILLING_CYCLES) {
    for (const currency of SUPPORTED_CURRENCIES) {
      const product = resolveSubscriptionProduct(normalizedPlan, billing, currency);
      if (product) products.push(product);
    }
  }
  return products;
}

// Validate subscription selection
function validateSubscriptionSelection(plan, billingCycle, currency) {
  const product = resolveSubscriptionProduct(plan, billingCycle, currency);
  return product !== null;
}

function getPlanTokenLimits(plan, userId) {
  const bucket = ensureTokenUsageBucket(userId) || { addOns: 0, tokenOverride: null };
  
  // Check for manual token override first
  if (typeof bucket.tokenOverride === 'number' && bucket.tokenOverride >= 0) {
    return Math.max(0, bucket.tokenOverride);
  }
  
  const normalized = normalizePlanSelection(plan) || DEFAULT_PLAN;
  const base = (planTokenLimits && typeof planTokenLimits[normalized] === 'number') ? planTokenLimits[normalized] : (PLAN_TOKEN_LIMITS[DEFAULT_PLAN] || 0);
  return Math.max(0, Number(base || 0) + Number(bucket.addOns || 0));
}

function getTokenUsageSummary(userId, plan) {
  const bucket = ensureTokenUsageBucket(userId) || { usage: 0, addOns: 0, tokenOverride: null };
  const limit = getPlanTokenLimits(plan, userId);
  const used = Math.max(0, Number(bucket.usage || 0));
  const remaining = limit > 0 ? Math.max(0, limit - used) : 0;
  return {
    month: bucket.month || currentMonthKey(),
    used,
    limit,
    remaining,
    percent: limit > 0 ? Math.min(100, parseFloat(((used / limit) * 100).toFixed(1))) : 0,
    addOn: Math.max(0, Number(bucket.addOns || 0)),
    plan: normalizePlanSelection(plan) || DEFAULT_PLAN,
    tokenOverride: bucket.tokenOverride !== undefined ? bucket.tokenOverride : null,
  };
}

async function recordExternalTestUsage(userId) {
  const bucket = ensureExternalTestUsageBucket(userId);
  if (!bucket) return null;
  bucket.count += 1;
  await persistExternalTestUsage();
  return bucket;
}

function resolveUserCurrency(user) {
  const currency = String(user?.subscriptionCurrency || user?.billingCurrency || '').toLowerCase();
  return SUPPORTED_CURRENCIES.includes(currency) ? currency : 'usd';
}

function getPaygPrice(currency = 'usd') {
  const normalized = String(currency || 'usd').toLowerCase();
  const fallback = PAYG_PRICES.usd || MIN_PAYMENT_AMOUNT;
  return Math.max(MIN_PAYMENT_AMOUNT, PAYG_PRICES[normalized] || fallback);
}

function getPendingPaygTokens(userId, monthKey = currentMonthKey()) {
  const targetMonth = monthKey || currentMonthKey();
  let total = 0;
  Object.values(pendingPayg || {}).forEach((entry) => {
    if (!entry) return;
    if (entry.userId !== userId) return;
    if (entry.month && entry.month !== targetMonth) return;
    total += Math.max(0, Number(entry.tokens || 0));
  });
  return total;
}

function computePaygSummary(userId, plan, { projectedUsage } = {}) {
  const user = findUserById(userId);
  const currency = resolveUserCurrency(user);
  const pricePerUnit = getPaygPrice(currency);
  const bucket = ensureTokenUsageBucket(userId) || { usage: 0, addOns: 0, paygBilled: 0 };
  const limit = getPlanTokenLimits(plan, userId);
  const used = Math.max(0, Number(projectedUsage !== undefined ? projectedUsage : bucket.usage || 0));
  const overageTokens = Math.max(0, used - limit);
  const billedTokens = Math.max(0, Number(bucket.paygBilled || 0));
  const pendingTokens = getPendingPaygTokens(userId, bucket.month);
  const billableTokens = Math.max(0, overageTokens - billedTokens - pendingTokens);
  const amount = PAYG_ENABLED ? Math.max(0, Math.ceil((billableTokens * pricePerUnit) / PAYG_UNIT_TOKENS)) : 0;
  const projectedAmount = PAYG_ENABLED ? Math.max(0, Math.ceil((Math.max(0, overageTokens - billedTokens - pendingTokens) * pricePerUnit) / PAYG_UNIT_TOKENS)) : 0;
  return {
    currency,
    productConfigured: !!PAYG_PRODUCT_IDS[currency],
    pricePerUnit,
    unitTokens: PAYG_UNIT_TOKENS,
    overageTokens,
    billedTokens,
    pendingTokens,
    billableTokens,
    amount,
    projectedAmount,
    month: bucket.month,
    limit,
    used,
  };
}

function computeTopupDiscount(plan) {
  const normalized = normalizePlanSelection(plan);
  if (normalized === 'enterprise') return ENTERPRISE_TOPUP_DISCOUNT;
  if (normalized === 'professional') return BUSINESS_TOPUP_DISCOUNT;
  return 0;
}

function applyTopupDiscount(baseAmount, discount) {
  // Respect gateway minimum charge; if discount pushes below minimum, cap at MIN_PAYMENT_AMOUNT
  return Math.max(MIN_PAYMENT_AMOUNT, Math.round(baseAmount * (1 - discount)));
}

function resolveTopupPack(tier, currency = 'usd') {
  const normalizedTier = (tier || '').toString().toLowerCase();
  const normalizedCurrency = (currency || 'usd').toLowerCase();
  const priceKey = `${normalizedTier}_${normalizedCurrency}`;

  // Ensure config objects exist
  const productIds = TOPUP_PRODUCT_IDS || {};
  const tokens = TOPUP_TOKENS || {};

  // Try new format with currency support
  if (productIds[priceKey]) {
    return {
      tier: normalizedTier,
      currency: normalizedCurrency,
      tokens: tokens[normalizedTier] || 0,
      productId: productIds[priceKey] || '',
    };
  }

  // Fallback to legacy mapping for backward compatibility (assumes USD)
  let legacy = 'topup_1';
  if (normalizedTier === 'free') legacy = 'topup_1';
  else if (normalizedTier === 'plus') legacy = 'topup_2';
  else if (normalizedTier === 'pro') legacy = 'topup_3';

  return {
    tier: legacy,
    currency: normalizedCurrency,
    tokens: tokens[legacy] || 0,
    productId: productIds[`${legacy}_usd`] || '',
  };
}

function getTopupPrice(tier, currency = 'usd') {
  const normalizedTier = (tier || '').toLowerCase();
  const normalizedCurrency = (currency || 'usd').toLowerCase();
  const priceKey = `${normalizedTier}_${normalizedCurrency}`;
  const prices = TOPUP_PRICES || {};
  return prices[priceKey] || 0;
}

async function recordUserTokens(userId, tokens = 0) {
  if (!userId) {
    console.error('[USAGE] recordUserTokens: userId is missing, cannot record tokens');
    return;
  }
  const roundedTokens = Math.ceil(tokens || 0); // Always round up
  if (roundedTokens <= 0) {
    console.error(`[USAGE] recordUserTokens: token count is 0 or negative, skipping. Raw: ${tokens}, Rounded: ${roundedTokens}`);
    return;
  }

  const user = findUserById(userId);
  const bucket = ensureTokenUsageBucket(userId);
  const previousTotal = Number(bucket.usage) || 0;
  bucket.usage = previousTotal + roundedTokens;

  // Usage-based billing for unlimited usage (charge only over plan limit)
  if (user?.unlimitedUsage) {
    const plan = user?.plan || DEFAULT_PLAN;
    const limit = getPlanTokenLimits(plan, userId);
    const previousOverage = Math.max(0, previousTotal - limit);
    const currentOverage = Math.max(0, bucket.usage - limit);
    const overageDelta = Math.max(0, currentOverage - previousOverage);

    if (overageDelta > 0) {
      const currency = resolveUserCurrency(user);
      const rate = tokenRates[currency] || DEFAULT_TOKEN_RATES[currency] || 250;
      const chargeAmount = Math.max(0, Math.round((overageDelta * rate) / 1_000_000));

      if (chargeAmount > 0) {
        try {
          await ingestUsageEvent({
            user,
            tokens: overageDelta,
            amountCents: chargeAmount,
            currency,
            ratePerMillion: rate,
            plan,
            month: bucket.month || currentMonthKey(),
          });
          console.log(`[BILLING] 📈 Usage event: ${overageDelta} tokens over limit, ${currency.toUpperCase()}${(chargeAmount / 100).toFixed(2)} billed for user ${userId}.`);
        } catch (err) {
          console.error(`[BILLING] ❌ Failed to ingest usage event for ${userId}:`, err?.message || err);
        }
      }
    }
  }

  // Force immediate write
  try {
    await persistTokenUsage();
    console.log(`[USAGE] ✅ Recorded ${roundedTokens} tokens for ${userId}. Previous: ${previousTotal}, New total: ${bucket.usage}`);
  } catch (err) {
    console.error(`[USAGE] ❌ Failed to persist token usage for ${userId}:`, err);
  }
}

function canConsumeTokens(userId, plan, requestedTokens = 0) {
  const user = findUserById(userId);
  const summary = getTokenUsageSummary(userId, plan);
  const requested = Math.max(0, Math.round(requestedTokens || 0));
  const grace = Math.max(TOKEN_GRACE_MIN, Math.round(summary.limit * TOKEN_GRACE_RATIO));
  const projected = (summary.used || 0) + requested;
  const unlimitedEnabled = Boolean(user?.unlimitedUsage);
  const paid = isPaidPlan(plan);
  const allowOverage = paid && PAYG_ENABLED && !unlimitedEnabled;
  const payg = allowOverage ? computePaygSummary(userId, plan, { projectedUsage: projected }) : null;

  if (unlimitedEnabled) {
    return {
      allowed: true,
      limit: summary.limit,
      used: summary.used,
      remaining: summary.remaining,
      projected,
      requestedTokens: requested,
      payg: null,
      unlimited: true,
    };
  }

  if (!allowOverage && summary.limit > 0 && projected > summary.limit + grace) {
    return {
      allowed: false,
      reason: 'You have exceeded your token allowance. Upgrade or add a boost.',
      limit: summary.limit,
      used: summary.used,
      remaining: summary.remaining,
    };
  }
  return {
    allowed: true,
    limit: summary.limit,
    used: summary.used,
    remaining: summary.remaining,
    projected,
    requestedTokens: requested,
    payg,
  };
}

function summarizeProviderUsage(provider, model) {
  const key = normalizeUsageProvider(provider, model);
  const now = Date.now();
  const minuteAgo = now - MINUTE_MS;
  const hourAgo = now - HOUR_MS;
  const dayAgo = now - DAY_MS;
  const entries = ensureProviderUsageBucket(key);
  const filterByModel = !!(model && providerLimits.limits[key] && providerLimits.limits[key].scope === 'model');
  const result = {
    tokensLastMinute: 0,
    tokensLastHour: 0,
    tokensLastDay: 0,
    requestsLastMinute: 0,
    requestsLastHour: 0,
    requestsLastDay: 0,
    perModel: {},
  };

  entries.forEach((entry) => {
    if (!entry || typeof entry.ts !== 'number') return;
    const matchesModel = !filterByModel || (entry.model && model && entry.model === model);
    const targetKey = entry.model || 'unknown';
    const isMinute = entry.ts >= minuteAgo;
    const isHour = entry.ts >= hourAgo;
    const isDay = entry.ts >= dayAgo;
    if (isMinute && matchesModel) {
      result.tokensLastMinute += Number(entry.tokens || 0);
      result.requestsLastMinute += Number(entry.requests || 0);
    }
    if (isHour && matchesModel) {
      result.tokensLastHour += Number(entry.tokens || 0);
      result.requestsLastHour += Number(entry.requests || 0);
    }
    if (isDay && matchesModel) {
      result.tokensLastDay += Number(entry.tokens || 0);
      result.requestsLastDay += Number(entry.requests || 0);
    }
    if (!result.perModel[targetKey]) {
      result.perModel[targetKey] = { tokensLastMinute: 0, tokensLastHour: 0, tokensLastDay: 0, requestsLastMinute: 0, requestsLastHour: 0, requestsLastDay: 0 };
    }
    if (isMinute) {
      result.perModel[targetKey].tokensLastMinute += Number(entry.tokens || 0);
      result.perModel[targetKey].requestsLastMinute += Number(entry.requests || 0);
    }
    if (isHour) {
      result.perModel[targetKey].tokensLastHour += Number(entry.tokens || 0);
      result.perModel[targetKey].requestsLastHour += Number(entry.requests || 0);
    }
    if (isDay) {
      result.perModel[targetKey].tokensLastDay += Number(entry.tokens || 0);
      result.perModel[targetKey].requestsLastDay += Number(entry.requests || 0);
    }
  });

  return result;
}

function isProviderLimited(provider, model) {
  const key = normalizeUsageProvider(provider, model);
  const cfg = ensureProviderLimitDefaults(key);
  const usage = summarizeProviderUsage(key, model);
  const modelCfg = (cfg.scope === 'model' && model && cfg.perModel[model]) ? cfg.perModel[model] : cfg;
  const checks = [
    ['tokensPerMinute', usage.tokensLastMinute, 'minute tokens'],
    ['tokensPerHour', usage.tokensLastHour, 'hourly tokens'],
    ['tokensPerDay', usage.tokensLastDay, 'daily tokens'],
    ['requestsPerMinute', usage.requestsLastMinute, 'minute requests'],
    ['requestsPerHour', usage.requestsLastHour, 'hourly requests'],
    ['requestsPerDay', usage.requestsLastDay, 'daily requests'],
  ];
  for (const [field, used, label] of checks) {
    const limit = sanitizeLimitNumber(modelCfg[field]);
    if (limit > 0 && used >= limit) {
      return { limited: true, reason: `${label} limit reached`, field, used, limit, usage, scope: cfg.scope };
    }
  }
  return { limited: false, usage, scope: cfg.scope };
}

function recordProviderUsage(provider, model, tokens = 0, requests = 1) {
  const key = normalizeUsageProvider(provider, model);
  const isNew = !providerLimits.limits[key];
  ensureProviderLimitDefaults(key);
  if (isNew) {
    if (pendingProviderPersistTimer) clearTimeout(pendingProviderPersistTimer);
    const timer = setTimeout(() => {
      if (pendingProviderPersistTimer !== timer) return;
      pendingProviderPersistTimer = null;
      persistProviderLimits().catch((err) => log('Failed to persist new provider limit defaults', { provider: key, error: String(err) }));
    }, PROVIDER_PERSIST_DEBOUNCE_MS);
    pendingProviderPersistTimer = timer;
  }
  const bucket = ensureProviderUsageBucket(key);
  bucket.push({
    ts: Date.now(),
    tokens: Math.max(0, Math.round(tokens || 0)),
    requests: Math.max(0, Math.round(requests || 0)),
    model: model || '',
  });
  pruneProviderUsage();
  persistProviderUsage().catch((err) => log('Failed to persist provider usage (async)', { error: String(err) }));
}

/**
 * Validates that a token count is reasonable
 * @param {number} tokens - The token count to validate
 * @param {object} context - Context for validation (contentLength, source, etc.)
 * @returns {{valid: boolean, reason?: string, adjustedTokens?: number}}
 */
function validateTokenCount(tokens, context = {}) {
  const { contentLength = 0, source = 'unknown' } = context;
  
  // Must be a positive number
  if (typeof tokens !== 'number' || !isFinite(tokens)) {
    return { valid: false, reason: `tokens is not a finite number (type: ${typeof tokens}, value: ${tokens})` };
  }
  
  if (tokens <= 0) {
    return { valid: false, reason: `tokens is not positive (value: ${tokens})` };
  }
  
  // Reasonable maximum: 1M tokens (most models have much lower limits)
  const MAX_REASONABLE_TOKENS = 1000000;
  if (tokens > MAX_REASONABLE_TOKENS) {
    log('⚠️  validateTokenCount: Token count exceeds reasonable maximum', { tokens, max: MAX_REASONABLE_TOKENS, source, contentLength });
    return { valid: false, reason: `tokens exceeds reasonable maximum of ${MAX_REASONABLE_TOKENS}` };
  }
  
  // If we have content length, verify tokens are roughly consistent
  // Minimum chars per token is typically 1 (for very dense text), max is typically 10
  if (contentLength > 0) {
    const charsPerToken = contentLength / tokens;
    const MIN_CHARS_PER_TOKEN = 0.5; // Very conservative lower bound
    const MAX_CHARS_PER_TOKEN = 15;  // Very conservative upper bound
    
    if (charsPerToken < MIN_CHARS_PER_TOKEN) {
      log('⚠️  validateTokenCount: Chars per token too low (suspicious)', { 
        tokens, 
        contentLength, 
        charsPerToken, 
        min: MIN_CHARS_PER_TOKEN,
        source 
      });
      return { 
        valid: false, 
        reason: `chars per token (${charsPerToken.toFixed(2)}) is suspiciously low (< ${MIN_CHARS_PER_TOKEN})` 
      };
    }
    
    if (charsPerToken > MAX_CHARS_PER_TOKEN) {
      log('⚠️  validateTokenCount: Chars per token too high (suspicious)', { 
        tokens, 
        contentLength, 
        charsPerToken, 
        max: MAX_CHARS_PER_TOKEN,
        source 
      });
      return { 
        valid: false, 
        reason: `chars per token (${charsPerToken.toFixed(2)}) is suspiciously high (> ${MAX_CHARS_PER_TOKEN})` 
      };
    }
  }
  
  return { valid: true };
}

function normalizeTokenNumber(value) {
  if (typeof value === 'number' && isFinite(value)) return value;
  if (typeof value === 'string' && value.trim()) {
    const n = Number(value);
    if (isFinite(n)) return n;
  }
  return null;
}

function extractTokenUsage(parsed) {
  if (!parsed || typeof parsed !== 'object') return null;

  const directCandidates = [
    ['tokensUsed', parsed.tokensUsed],
    ['tokens', parsed.tokens],
    ['totalTokens', parsed.totalTokens],
    ['total_tokens', parsed.total_tokens],
    ['totalTokenCount', parsed.totalTokenCount],
    ['tokenCount', parsed.tokenCount],
    ['token_count', parsed.token_count],
    ['usage.total_tokens', parsed.usage?.total_tokens],
    ['usage.totalTokens', parsed.usage?.totalTokens],
    ['usage.totalTokenCount', parsed.usage?.totalTokenCount],
    ['usage.total', parsed.usage?.total],
    ['usage.totalTokensUsed', parsed.usage?.totalTokensUsed],
    ['usageMetadata.totalTokenCount', parsed.usageMetadata?.totalTokenCount],
    ['tokenUsage.total_tokens', parsed.tokenUsage?.total_tokens],
    ['tokenUsage.totalTokens', parsed.tokenUsage?.totalTokens],
    ['tokenUsage.totalTokenCount', parsed.tokenUsage?.totalTokenCount],
  ];

  for (const [source, value] of directCandidates) {
    const n = normalizeTokenNumber(value);
    if (n && n > 0) return { tokens: n, source };
  }

  const usage = parsed.usage || parsed.tokenUsage || parsed.usageMetadata || parsed.usage_metadata || null;
  if (usage) {
    const prompt = normalizeTokenNumber(usage.prompt_tokens ?? usage.promptTokens ?? usage.input_tokens ?? usage.inputTokens ?? usage.inputTokenCount);
    const completion = normalizeTokenNumber(usage.completion_tokens ?? usage.completionTokens ?? usage.output_tokens ?? usage.outputTokens ?? usage.outputTokenCount);
    if ((prompt || 0) + (completion || 0) > 0) {
      return { tokens: (prompt || 0) + (completion || 0), source: 'usage.sum' };
    }
  }

  const inputTokens = normalizeTokenNumber(parsed.inputTokens || parsed.input_tokens || parsed.prompt_tokens || parsed.promptTokens);
  const outputTokens = normalizeTokenNumber(parsed.outputTokens || parsed.output_tokens || parsed.completion_tokens || parsed.completionTokens);
  if ((inputTokens || 0) + (outputTokens || 0) > 0) {
    return { tokens: (inputTokens || 0) + (outputTokens || 0), source: 'input+output' };
  }

  return null;
}

function estimateTokensFromText(text) {
  if (!text) return 0;
  const str = typeof text === 'string' ? text : JSON.stringify(text);
  return Math.max(1, Math.ceil(str.length / AVG_CHARS_PER_TOKEN));
}

function estimateTokensFromMessages(messages = [], reply = '') {
  const combined = []
    .concat((messages || []).map((m) => {
      if (typeof m === 'string') return m;
      if (m && typeof m.content === 'string') return m.content;
      if (m && Array.isArray(m.content)) {
        return m.content.map(part => (part && part.text) || '').join(' ');
      }
      return '';
    }))
    .concat(reply || '')
    .filter(Boolean)
    .join(' ');
  const tokens = estimateTokensFromText(combined);
  console.log(`[USAGE] Estimated tokens: ${tokens} from combined length: ${combined.length}`);
  return tokens;
}

function extractTokenUsageFromResult(result, messages, options = {}) {
  const { allowEstimate = true } = options || {};
  console.log('[USAGE] extractTokenUsageFromResult called with:', { 
    resultType: typeof result,
    hasRaw: !!(result && result.raw),
    hasUsage: !!(result && (result.usage || (result.raw && result.raw.usage))),
    hasReply: !!(result && result.reply),
    hasMessages: !!messages && messages.length,
    allowEstimate
  });

  // 1. Try OpenAI/Standard format
  if (result && (result.usage || (result.raw && result.raw.usage))) {
    const usage = result.usage || result.raw.usage;
    const total = usage.total_tokens || usage.total || (usage.prompt_tokens || 0) + (usage.completion_tokens || 0);
    if (total) {
      console.log('[USAGE] ✅ Extracted tokens from OpenAI format:', total);
      return total;
    }
  }
  
  // 2. Try Google/Gemini format
  if (result && (result.usageMetadata || (result.raw && result.raw.usageMetadata))) {
    const meta = result.usageMetadata || result.raw.usageMetadata;
    const total = meta.totalTokenCount || (meta.promptTokenCount || 0) + (meta.candidatesTokenCount || 0);
    if (total) {
      console.log('[USAGE] ✅ Extracted tokens from Google format:', total);
      return total;
    }
  }

  // 3. Fallback to estimation - handle different result structures (OpenCode returns { reply, model, attempts, provider })
  if (!allowEstimate) {
    console.log('[USAGE] ⚠️ Token usage missing and estimation disabled.');
    return 0;
  }
  const replyText = (typeof result === 'string' ? result : (result && typeof result.reply === 'string' ? result.reply : ''));
  const estimated = estimateTokensFromMessages(messages || [], replyText);
  console.log('[USAGE] ⚠️ Using estimation fallback:', { replyLength: replyText.length, estimatedTokens: estimated });
  return estimated;
}

async function getOpencodeSessionTokenUsage(sessionId, cwd) {
  if (!sessionId || !cwd) {
    log('⚠️  getOpencodeSessionTokenUsage: Missing required parameters', { hasSessionId: !!sessionId, hasCwd: !!cwd });
    return 0;
  }

  const cliCommand = resolveCliCommand('opencode');
  const candidates = [
    ['session', 'info', '--id', sessionId, '--json'],
    ['sessions', 'info', '--id', sessionId, '--json'],
    ['session', 'info', sessionId, '--json'],
    ['session', 'usage', '--id', sessionId, '--json'],
    ['session', 'show', '--id', sessionId, '--json'],
  ];

  log('🔍 getOpencodeSessionTokenUsage: Starting session token query', {
    sessionId,
    cwd,
    cliCommand,
    candidateCount: candidates.length,
    candidates: candidates.map(c => c.join(' '))
  });

  const attemptResults = [];

  for (const args of candidates) {
    const cmdStr = args.join(' ');
    try {
      log(`  → Trying: ${cliCommand} ${cmdStr}`, { sessionId });
      const { stdout, stderr } = await runCommand(cliCommand, args, { timeout: 10000, cwd });

      const hasStdout = stdout && stdout.trim();
      const hasStderr = stderr && stderr.trim();

      log(`  ← Response received`, {
        args: cmdStr,
        hasStdout,
        hasStderr,
        stdoutLength: stdout?.length || 0,
        stderrLength: stderr?.length || 0,
        stdoutSample: stdout?.substring(0, 300),
        stderrSample: stderr?.substring(0, 200)
      });

      if (hasStdout) {
        // Try JSON parsing first
        try {
          const parsed = JSON.parse(stdout);
          log('  ✓ JSON parse successful', {
            args: cmdStr,
            parsedKeys: Object.keys(parsed),
            hasUsage: !!parsed.usage,
            hasTokens: !!parsed.tokens,
            hasTokensUsed: !!parsed.tokensUsed,
            hasSession: !!parsed.session
          });

          const extracted = extractTokenUsage(parsed) || extractTokenUsage(parsed.session) || null;
          const tokens = extracted?.tokens || 0;

          if (typeof tokens === 'number' && tokens > 0) {
            log('✅ getOpencodeSessionTokenUsage: Successfully extracted tokens from JSON', {
              sessionId,
              tokens,
              command: cmdStr,
              extractionPath: extracted?.source || 'unknown'
            });
            attemptResults.push({ command: cmdStr, success: true, tokens, source: 'json' });
            return tokens;
          } else {
            const reason = typeof tokens !== 'number' ? `tokens is ${typeof tokens}, not number` : 'tokens is 0 or negative';
            log('  ✗ JSON parsed but no valid token count', { args: cmdStr, tokens, reason });
            attemptResults.push({ command: cmdStr, success: false, reason, parsedTokens: tokens, source: 'json' });
          }
        } catch (jsonErr) {
          log('  ✗ JSON parse failed, trying text parse', {
            args: cmdStr,
            error: jsonErr.message,
            stdoutSample: stdout.substring(0, 200)
          });

          // Try to parse token count from text output
          const tokenMatch = stdout.match(/total[_\s-]?tokens?\s*[:=]?\s*(\d+)/i) ||
                             stdout.match(/tokens?\s*[:=]?\s*(\d+)/i) ||
                             stdout.match(/token\s*count\s*[:=]?\s*(\d+)/i);
          if (tokenMatch) {
            const tokens = parseInt(tokenMatch[1], 10);
            if (tokens > 0) {
              log('✅ getOpencodeSessionTokenUsage: Successfully extracted tokens from text', {
                sessionId,
                tokens,
                command: cmdStr,
                pattern: tokenMatch[0]
              });
              attemptResults.push({ command: cmdStr, success: true, tokens, source: 'text', pattern: tokenMatch[0] });
              return tokens;
            } else {
              log('  ✗ Text pattern matched but tokens <= 0', { args: cmdStr, tokens, pattern: tokenMatch[0] });
              attemptResults.push({ command: cmdStr, success: false, reason: 'matched text pattern but tokens <= 0', parsedTokens: tokens, source: 'text' });
            }
          } else {
            log('  ✗ No text patterns matched', { args: cmdStr, stdoutSample: stdout.substring(0, 200) });
            attemptResults.push({ command: cmdStr, success: false, reason: 'no text patterns matched', source: 'text' });
          }
        }
      } else {
        const reason = !stdout ? 'no stdout' : 'stdout is empty';
        log('  ✗ No stdout to parse', { args: cmdStr, reason, hasStderr });
        attemptResults.push({ command: cmdStr, success: false, reason, stderr: stderr?.substring(0, 200) });
      }
    } catch (err) {
      const errorDetails = {
        message: err.message,
        stderr: err.stderr?.substring(0, 200),
        stdout: err.stdout?.substring(0, 200),
        code: err.code
      };
      log('  ✗ Command execution failed', { args: cmdStr, error: errorDetails });
      attemptResults.push({ command: cmdStr, success: false, error: errorDetails });
    }
  }

  log('❌ getOpencodeSessionTokenUsage: All commands failed', {
    sessionId,
    totalAttempts: attemptResults.length,
    attemptResults
  });
  return 0;
}

function getProviderUsageSnapshot(providerList = null) {
  const providers = (providerList && providerList.length)
    ? providerList.map((p) => normalizeProviderName(p))
    : Object.keys(providerLimits.limits || {});
  return providers.map((provider) => {
    const cfg = ensureProviderLimitDefaults(provider);
    const usage = summarizeProviderUsage(provider);
    return {
      provider,
      scope: cfg.scope,
      limits: {
        tokensPerMinute: cfg.tokensPerMinute,
        tokensPerDay: cfg.tokensPerDay,
        requestsPerMinute: cfg.requestsPerMinute,
        requestsPerDay: cfg.requestsPerDay,
      },
      perModelLimits: cfg.perModel || {},
      usage,
    };
  });
}

function serializeSession(session) {
  // Accuracy fix: detect if 'running' messages have actually stopped
  if (Array.isArray(session.messages)) {
    const now = Date.now();
    session.messages.forEach(msg => {
      if (msg.status === 'running') {
        const isActuallyRunning = runningProcesses.has(msg.id);
        const startedAt = msg.startedAt ? new Date(msg.startedAt).getTime() : 0;
        const runningDuration = now - startedAt;

        // Only mark as error if there's evidence of an actual error from the opencode server
        // (non-zero exit code or existing error message). Don't mark as error just because
        // it's been running for a while - the process may still be working.
        const hasActualError = (msg.opencodeExitCode && msg.opencodeExitCode !== 0) ||
                               (msg.error && msg.error.length > 0);
        if (!isActuallyRunning && startedAt > 0 && runningDuration > 30000 && hasActualError) {
          log('Detecting failed running message in serializeSession', {
            sessionId: session.id,
            messageId: msg.id,
            duration: runningDuration,
            exitCode: msg.opencodeExitCode,
            error: msg.error
          });
          msg.status = 'error';
          if (!msg.error || !msg.error.length) {
            msg.error = 'Message processing seems to have stalled or was interrupted.';
          }
          msg.finishedAt = new Date().toISOString();
          session.updatedAt = msg.finishedAt;
          // Note: we don't call persistState() here to keep this fast,
          // but the next state change will save it.
        }
      }
    });
  }
  return {
    id: session.id,
    title: session.title,
    model: session.model,
    cli: session.cli || 'opencode',
    opencodeSessionId: session.opencodeSessionId,
    createdAt: session.createdAt,
    updatedAt: session.updatedAt,
    messages: session.messages,
    pending: session.pending || 0,
    userId: session.userId,
    appId: session.appId,
    entryMode: session.entryMode || 'plan',
    source: session.source || 'builder',
    planSummary: session.planSummary,
    planUserRequest: session.planUserRequest,
    planApproved: !!session.planApproved,
    pluginVersion: session.pluginVersion,
    pluginVersionHistory: session.pluginVersionHistory || [],
    lastVersionBumpType: session.lastVersionBumpType,
  };
}

async function pathExists(targetPath) {
  try {
    await fs.access(targetPath);
    return true;
  } catch (_) {
    return false;
  }
}

async function safeMkdir(targetPath) {
  await fs.mkdir(targetPath, { recursive: true });
}

async function normalizeIconPath(iconPath) {
  if (!iconPath) return '';
  await ensureAssetsDir();
  const trimmed = iconPath.trim().replace(/^https?:\/\/[^/]+/, '');
  const withoutLeadingSlash = trimmed.replace(/^\/+/, '');
  const relative = withoutLeadingSlash.startsWith('assets/') ? withoutLeadingSlash : `assets/${withoutLeadingSlash}`;
  const resolved = path.resolve(STATIC_ROOT, relative);
  const assetsRoot = path.resolve(ASSETS_DIR);
  if (!resolved.startsWith(assetsRoot)) return '';
  if (!(await pathExists(resolved))) throw new Error('Icon not found in assets folder');
  return `/${relative.replace(/\\/g, '/')}`;
}

async function listAdminIcons() {
  await ensureAssetsDir();
  try {
    const entries = await fs.readdir(ASSETS_DIR, { withFileTypes: true });
    return entries
      .filter((e) => e.isFile())
      .map((e) => e.name)
      .filter((name) => name.match(/\.(png|jpg|jpeg|gif|svg|webp)$/i))
      .map((name) => `/assets/${name}`);
  } catch (error) {
    log('Failed to list admin icons', { error: String(error) });
    return [];
  }
}
async function mergeCopyPath(srcPath, destPath, tag) {
  const srcStat = await fs.stat(srcPath);
  if (srcStat.isDirectory()) {
    await safeMkdir(destPath);
    const entries = await fs.readdir(srcPath);
    for (const entry of entries) {
      await mergeCopyPath(path.join(srcPath, entry), path.join(destPath, entry), tag);
    }
    return;
  }

  // File: only copy if destination doesn't exist; otherwise preserve both by writing a suffixed copy.
  if (!(await pathExists(destPath))) {
    await safeMkdir(path.dirname(destPath));
    await fs.copyFile(srcPath, destPath);
    return;
  }

  const ext = path.extname(destPath);
  const base = destPath.slice(0, destPath.length - ext.length);
  const suffixed = `${base}.migrated-${tag}${ext || ''}`;
  await safeMkdir(path.dirname(suffixed));
  await fs.copyFile(srcPath, suffixed);
}

async function migrateUserSessions(fromUserId, toUserId) {
  const from = sanitizeSegment(fromUserId || '', '');
  const to = sanitizeSegment(toUserId || '', '');
  if (!from || !to || from === to) return { moved: 0, skipped: 0 };

  const tag = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
  const sessionsToMove = state.sessions.filter((s) => s.userId === from);
  let moved = 0;
  let skipped = 0;

  for (const session of sessionsToMove) {
    const oldWorkspaceDir = session.workspaceDir;
    const oldUploadsDir = session.uploadsDir;

    const targetPaths = buildWorkspacePaths({ userId: to, appId: session.appId || session.id, id: session.id });
    const newWorkspaceDir = targetPaths.workspaceDir;
    const newUploadsDir = targetPaths.uploadsDir;

    try {
      await safeMkdir(newWorkspaceDir);
      await safeMkdir(newUploadsDir);

      // Merge-copy old workspace into new workspace, without deleting old.
      if (oldWorkspaceDir && oldWorkspaceDir !== newWorkspaceDir && (await pathExists(oldWorkspaceDir))) {
        await mergeCopyPath(oldWorkspaceDir, newWorkspaceDir, tag);
      }
      if (oldUploadsDir && oldUploadsDir !== newUploadsDir && (await pathExists(oldUploadsDir))) {
        await mergeCopyPath(oldUploadsDir, newUploadsDir, tag);
      }

      // Only after the disk merge succeeded do we flip ownership.
      session.userId = to;
      session.workspaceDir = newWorkspaceDir;
      session.uploadsDir = newUploadsDir;
      session.attachmentKey = session.attachmentKey || randomUUID();

      moved += 1;
    } catch (err) {
      skipped += 1;
      log('migration copy failed', { sessionId: session.id, err: String(err), oldWorkspaceDir, newWorkspaceDir });
    }
  }

  await persistState();
  return { moved, skipped };
}

function getSession(sessionId, userId) {
  const session = state.sessions.find((s) => s.id === sessionId);
  if (!session) return null;
  if (userId && session.userId && session.userId !== userId) return null;
  return session;
}
async function createSession(payload = {}, userId, appId) {
  const ownerPlan = resolveUserPlan(userId);
  const model = resolvePlanModel(ownerPlan, payload.model || 'default');
  const cli = normalizeCli(payload.cli);
  const entryMode = payload.entryMode === 'opencode' ? 'opencode' : 'plan';
  const source = payload.source || 'builder';
  const planApproved = payload.planApproved === true;
  const now = new Date().toISOString();
  const sessionId = randomUUID();
  const rawAppId = appId || payload.appId || payload.app;
  const reuseAppId = payload.reuseAppId === true || payload.reuseApp === true;
  const sanitizedAppId = sanitizeSegment(rawAppId || '', '');
  const ownerId = sanitizeSegment(userId || 'anonymous', 'anonymous');
  // Default to the unique session id when no app identifier is provided
  let resolvedAppId = sanitizedAppId || sessionId;
  
  // When reusing an existing appId, look up the existing session to preserve its title
  let existingSession = null;
  let sessionTitle = payload.title?.trim() || 'New Chat';
  
  if (sanitizedAppId && reuseAppId) {
    existingSession = state.sessions.find((s) => s.userId === ownerId && s.appId === resolvedAppId);
    if (existingSession && existingSession.title) {
      // Preserve the existing app's title
      sessionTitle = existingSession.title;
    }
  }
  
  if (sanitizedAppId) {
    const collision = state.sessions.some((s) => s.userId === ownerId && s.appId === resolvedAppId);
    if (collision && !reuseAppId) resolvedAppId = `${resolvedAppId}-${sessionId.slice(0, 8)}`;
  }
  const appLimit = getPlanAppLimit(ownerPlan);
  const existingAppIds = new Set(state.sessions.filter((s) => s.userId === ownerId).map((s) => s.appId));
  const isNewApp = !existingAppIds.has(resolvedAppId);
  const currentAppCount = existingAppIds.size;
  if (isNewApp && Number.isFinite(appLimit) && currentAppCount >= appLimit) {
    const err = new Error(`You have reached the app limit (${appLimit} apps, currently ${currentAppCount}) for your plan. Upgrade to create more apps.`);
    err.statusCode = 403;
    throw err;
  }
  const session = {
    id: sessionId,
    title: sessionTitle,
    model,
    cli,
    userId: ownerId,
    appId: resolvedAppId,
    attachmentKey: randomUUID(),
    opencodeSessionId: null, // Will be initialized when first message is sent
    initialOpencodeSessionId: null, // Will be locked to the first session created for continuity
    entryMode,
    source,
    planApproved,
    createdAt: now,
    updatedAt: now,
    messages: [],
    pending: 0,
    // Version tracking for WordPress plugins
    pluginVersion: null, // Will be detected from plugin file on first export
    pluginVersionHistory: [], // Track version changes over time
    lastVersionBumpType: null // 'major', 'minor', 'patch', or 'keep'
  };

  // WordPress identifies plugins by their folder + main file (plugin basename).
  // If we generate a new slug per session/export, WP treats it as a different plugin and won't upgrade cleanly.
  // Keep the slug stable per appId (and independent of title), so repeated exports upgrade the same plugin.
  const baseSlug = sanitizeSegment(resolvedAppId || payload.title || 'plugin', 'plugin');
  session.pluginSlug = baseSlug.startsWith('pc-') ? baseSlug : `pc-${baseSlug}`;
  // Plugin Name can change safely; keep it stable-ish for UX.
  session.pluginName = payload.title?.trim() ? `Plugin Compass ${payload.title.trim()}` : 'Plugin Compass Plugin';

  await ensureSessionPaths(session);
  state.sessions.unshift(session);

  // Track new session/project creation
  trackUserSession(userId, 'project_created', { 
    sessionId: session.id, 
    appId: session.appId, 
    plan: ownerPlan,
    isNewApp: isNewApp
  });
  trackFeatureUsage('project_creation', userId, ownerPlan);
  trackConversionFunnel('app_creation', 'project_created', userId, {
    appId: session.appId,
    plan: ownerPlan,
    isNewApp: isNewApp
  });

  log('session created', { id: session.id, opencodeSessionId: 'pending', model: session.model, cli: session.cli, userId: session.userId, appId: session.appId, pluginSlug: session.pluginSlug });
  return session;
}
function updatePending(sessionId, delta, userId) { const session = getSession(sessionId, userId); if (!session) return; session.pending = Math.max(0, (session.pending || 0) + delta); session.updatedAt = new Date().toISOString(); }

async function parseJsonBody(req, maxBytes = MAX_JSON_BODY_SIZE) { return new Promise((resolve, reject) => { let body = ''; req.on('data', (chunk) => { body += chunk; if (body.length > maxBytes) { req.socket.destroy(); reject(new Error('Payload too large')); } }); req.on('end', () => { if (!body) return resolve({}); try { const parsed = JSON.parse(body); resolve(parsed); } catch (error) { reject(error); } }); }); }

function decodeBase64Payload(raw) {
  if (!raw) return Buffer.alloc(0);
  const trimmed = String(raw).trim();
  const base64 = trimmed.includes(',') ? trimmed.slice(trimmed.indexOf(',') + 1) : trimmed;
  return Buffer.from(base64, 'base64');
}

async function loadExternalTestingSpec(workspaceDir) {
  if (!workspaceDir) return null;
  const specPath = path.join(workspaceDir, 'external-wp-tests.json');
  try {
    const raw = await fs.readFile(specPath, 'utf8');
    const parsed = JSON.parse(raw);
    if (!parsed || typeof parsed !== 'object') return null;
    return parsed;
  } catch (_) {
    return null;
  }
}

async function resolvePluginRoot(workspaceDir) {
  if (!workspaceDir) return null;
  const validFiles = [];
  await collectValidFiles(workspaceDir, workspaceDir, validFiles, [
    'node_modules',
    '.git',
    '.data',
    'uploads',
    '*.zip',
    '*.log'
  ]);

  for (const fileInfo of validFiles) {
    if (!fileInfo.fullPath.endsWith('.php')) continue;
    try {
      const content = await fs.readFile(fileInfo.fullPath, 'utf8');
      if (content.includes('Plugin Name:') && content.includes('Plugin URI:')) {
        return path.dirname(fileInfo.fullPath);
      }
    } catch (_) {
      // skip unreadable files
    }
  }
  return workspaceDir;
}

// Basic ZIP signature check: PK\x03\x04 (local header) or PK\x05\x06 (empty archives)
function isLikelyZip(buffer) {
  if (!buffer || buffer.length < 4) return false;
  const matchesSig = (sig) => buffer.subarray(0, 4).equals(Buffer.from(sig));
  return matchesSig(ZIP_LOCAL_HEADER_SIG) || matchesSig(ZIP_EOCD_EMPTY_SIG);
}

function findCommonRoot(entries) {
  if (!entries || entries.length === 0) return null;
  
  let commonPrefix = null;
  
  for (const entry of entries) {
    const rawName = (entry.entryName || '').replace(/\\/g, '/');
    const cleaned = rawName.replace(/^\/+/, '');
    
    if (!cleaned) continue;
    
    const parts = cleaned.split('/');
    const topDir = parts[0];
    
    if (!topDir) continue;
    
    if (commonPrefix === null) {
      commonPrefix = topDir;
    } else if (commonPrefix !== topDir) {
      return null;
    }
  }
  
  return commonPrefix;
}

async function extractZipToWorkspace(buffer, workspaceDir) {
  const zip = new AdmZip(buffer);
  const entries = zip.getEntries() || [];
  let fileCount = 0;
  const root = path.resolve(workspaceDir);
  
  const commonRoot = findCommonRoot(entries);

  for (const entry of entries) {
    const rawName = (entry.entryName || '').replace(/\\/g, '/');
    const cleaned = rawName.replace(/^\/+/, '');
    
    let entryPath = cleaned;
    if (commonRoot && cleaned.startsWith(commonRoot)) {
      entryPath = cleaned.slice(commonRoot.length).replace(/^\/+/, '');
    }
    
    const decoded = (() => { try { return decodeURIComponent(entryPath); } catch (_) { return entryPath; } })();
    const normalized = path.normalize(decoded);
    if (!entryPath || normalized.startsWith('..') || normalized.includes(`..${path.sep}`)) continue;
    if (path.isAbsolute(normalized)) continue;
    if (BLOCKED_PATH_PATTERN.test(normalized)) continue;

    const targetPath = path.join(workspaceDir, normalized);
    const resolved = path.resolve(targetPath);
    if (!resolved.startsWith(root)) continue;

    if (entry.isDirectory) {
      await fs.mkdir(resolved, { recursive: true });
      continue;
    }

    await fs.mkdir(path.dirname(resolved), { recursive: true });
    const data = entry.getData();
    await fs.writeFile(resolved, data);
    fileCount += 1;
  }

  if (fileCount === 0) {
    throw new Error('ZIP archive contained no valid files (entries may have been blocked)');
  }

  return fileCount;
}


function sendJson(res, statusCode, payload) { 
  // CORS headers are already set in route(), but ensure they're preserved
  const headers = { 
    'Content-Type': 'application/json'
  };
  res.writeHead(statusCode, headers); 
  res.end(JSON.stringify(payload)); 
}
function serveFile(res, filePath, contentType = 'text/html') { return fs.readFile(filePath).then((content) => { res.writeHead(200, { 'Content-Type': contentType }); res.end(content); }).catch(() => { res.writeHead(404); res.end('Not found'); }); }
function guessContentType(filePath) { const ext = path.extname(filePath); switch (ext) { case '.css': return 'text/css'; case '.js': return 'application/javascript'; case '.svg': return 'image/svg+xml'; case '.json': return 'application/json'; case '.html': return 'text/html'; default: return 'text/plain'; } }
function guessContentTypeFromExt(ext) { switch (ext) { case '.css': return 'text/css'; case '.js': return 'application/javascript'; case '.svg': return 'image/svg+xml'; case '.json': return 'application/json'; case '.png': return 'image/png'; case '.jpg': case '.jpeg': return 'image/jpeg'; case '.gif': return 'image/gif'; default: return 'application/octet-stream'; } }
function safeStaticPath(relativePath) { const clean = relativePath.replace(/^\/+/, '').replace(/\.\.+/g, ''); const target = path.resolve(STATIC_ROOT, clean || 'index.html'); const root = path.resolve(STATIC_ROOT); if (!target.startsWith(root)) throw new Error('Invalid path'); return target; }

function runCommand(command, args, options = {}) {
  return new Promise((resolve, reject) => {
    const spawnOpts = { ...options, stdio: ['ignore', 'pipe', 'pipe'] };
    const child = spawn(command, args, spawnOpts);
    const processId = randomUUID();
    let stdout = '';
    let stderr = '';
    let finished = false;
    
    // Track this child process for cleanup
    if (child.pid) {
      registerChildProcess(processId, child.pid, options.sessionId || '', options.messageId || '');
    }
    
    const cleanup = () => {
      unregisterChildProcess(processId);
    };
    
    const timer = options.timeout
      ? setTimeout(() => {
        finished = true;
        cleanup();
        try {
          child.kill('SIGTERM');
          // Force kill after 5 seconds if still running
          setTimeout(() => {
            try { child.kill('SIGKILL'); } catch (_) {}
          }, 5000);
        } catch (ignored) { }
        const err = new Error(`Command timed out after ${options.timeout}ms`);
        err.code = 'TIMEOUT';
        err.stdout = stdout;
        err.stderr = stderr;
        reject(err);
      }, options.timeout)
      : null;

    child.stdout.on('data', (data) => {
      const chunk = data.toString();
      stdout += chunk;
      if (options.onData) options.onData('stdout', chunk);
    });
    child.stderr.on('data', (data) => {
      const chunk = data.toString();
      stderr += chunk;
      if (options.onData) options.onData('stderr', chunk);
    });
    child.on('error', (error) => {
      if (finished) return;
      if (timer) clearTimeout(timer);
      finished = true;
      cleanup();
      const err = new Error(String(error) || 'Failed to spawn process');
      err.code = error.code || 'spawn_error';
      err.stdout = stdout;
      err.stderr = stderr;
      reject(err);
    });
    child.on('close', (code) => {
      if (finished) return;
      if (timer) clearTimeout(timer);
      finished = true;
      cleanup();
      if (code === 0) return resolve({ stdout, stderr, code });
      const err = new Error(`Command exited with code ${code}${stderr ? `: ${stderr.trim()}` : ''}`);
      err.code = code;
      err.stdout = stdout;
      err.stderr = stderr;
      reject(err);
    });
  });
}


// Fetch models from OpenRouter API when API key is configured
async function fetchOpenRouterModels() {
  // Try paid API key first, then fall back to regular key
  const apiKey = OPENROUTER_PAID_API_KEY || OPENROUTER_API_KEY;
  if (!apiKey) return [];
  try {
    const res = await fetch('https://openrouter.ai/api/v1/models', {
      headers: { 'Authorization': `Bearer ${apiKey}` }
    });
    if (!res.ok) {
      log('OpenRouter models fetch failed', { status: res.status });
      return [];
    }
    const data = await res.json();
    const models = Array.isArray(data?.data) ? data.data : [];
    return models.map((m) => ({
      name: m.id || m.name,
      label: `${m.id || m.name} (OpenRouter)`,
      provider: 'openrouter'
    })).filter((m) => m.name);
  } catch (error) {
    log('OpenRouter models fetch error', { error: String(error) });
    return [];
  }
}

// Fetch models from Mistral API when API key is configured
async function fetchMistralModels() {
  if (!MISTRAL_API_KEY) return [];
  try {
    const res = await fetch('https://api.mistral.ai/v1/models', {
      headers: { 'Authorization': `Bearer ${MISTRAL_API_KEY}` }
    });
    if (!res.ok) {
      log('Mistral models fetch failed', { status: res.status });
      return [];
    }
    const data = await res.json();
    const models = Array.isArray(data?.data) ? data.data : [];
    return models.map((m) => ({
      name: m.id || m.name,
      label: `${m.id || m.name} (Mistral)`,
      provider: 'mistral'
    })).filter((m) => m.name);
  } catch (error) {
    log('Mistral models fetch error', { error: String(error) });
    return [];
  }
}

// Fetch models from Groq API when API key is configured
async function fetchGroqModels() {
  if (!GROQ_API_KEY) return [];
  try {
    const res = await fetch('https://api.groq.com/openai/v1/models', {
      headers: { 'Authorization': `Bearer ${GROQ_API_KEY}` }
    });
    if (!res.ok) {
      log('Groq models fetch failed', { status: res.status });
      return [];
    }
    const data = await res.json();
    const models = Array.isArray(data?.data) ? data.data : [];
    return models.map((m) => ({
      name: m.id || m.name,
      label: `${m.id || m.name} (Groq)`,
      provider: 'groq'
    })).filter((m) => m.name);
  } catch (error) {
    log('Groq models fetch error', { error: String(error) });
    return [];
  }
}

// Fetch models from Google Gemini API when API key is configured
async function fetchGoogleModels() {
  if (!GOOGLE_API_KEY) return [];
  try {
    const res = await fetch(`https://generativelanguage.googleapis.com/v1beta/models?key=${GOOGLE_API_KEY}`);
    if (!res.ok) {
      log('Google models fetch failed', { status: res.status });
      return [];
    }
    const data = await res.json();
    const models = Array.isArray(data?.models) ? data.models : [];
    return models.map((m) => ({
      name: m.name || m.id,
      label: `${m.name || m.id} (Google)`,
      provider: 'google'
    })).filter((m) => m.name);
  } catch (error) {
    log('Google models fetch error', { error: String(error) });
    return [];
  }
}

// Fetch models from NVIDIA API when API key is configured
async function fetchNvidiaModels() {
  if (!NVIDIA_API_KEY) return [];
  try {
    const res = await fetch('https://integrate.api.nvidia.com/v1/models', {
      headers: { 'Authorization': `Bearer ${NVIDIA_API_KEY}` }
    });
    if (!res.ok) {
      log('NVIDIA models fetch failed', { status: res.status });
      return [];
    }
    const data = await res.json();
    const models = Array.isArray(data?.data) ? data.data : [];
    return models.map((m) => ({
      name: m.id || m.name,
      label: `${m.id || m.name} (NVIDIA)`,
      provider: 'nvidia'
    })).filter((m) => m.name);
  } catch (error) {
    log('NVIDIA models fetch error', { error: String(error) });
    return [];
  }
}

// Fetch models from Chutes API when API key is configured
async function fetchChutesModels() {
  if (!CHUTES_API_KEY) {
    log('Chutes API key not configured');
    return [];
  }
  try {
    log('Fetching Chutes models', { 
      url: `${CHUTES_API_URL}/models`,
      keyPrefix: CHUTES_API_KEY.substring(0, 8) + '...'
    });
    const res = await fetch(`${CHUTES_API_URL}/models`, {
      headers: { 'Authorization': `Bearer ${CHUTES_API_KEY}` }
    });
    if (!res.ok) {
      const errorText = await res.text().catch(() => '');
      log('Chutes models fetch failed', { 
        status: res.status, 
        statusText: res.statusText,
        error: errorText.substring(0, 200),
        keyPrefix: CHUTES_API_KEY.substring(0, 8) + '...'
      });
      return [];
    }
    const data = await res.json();
    const models = Array.isArray(data?.data) ? data.data : [];
    log('Chutes models fetched successfully', { count: models.length });
    return models.map((m) => ({
      name: m.id || m.name,
      label: `${m.id || m.name} (Chutes)`,
      provider: 'chutes'
    })).filter((m) => m.name);
  } catch (error) {
    log('Chutes models fetch error', { error: String(error) });
    return [];
  }
}

// Fetch all models for a specific provider from models.dev API
async function fetchModelsDevProviderModels(providerId) {
  const now = Date.now();
  const cacheKey = providerId.toLowerCase();
  
  // Check cache
  if (cachedModelsDevProviders.has(cacheKey)) {
    const cachedAt = cachedModelsDevProvidersAt.get(cacheKey) || 0;
    if (now - cachedAt < MODELS_DEV_CACHE_TTL) {
      log('Using cached models.dev data', { provider: providerId });
      return cachedModelsDevProviders.get(cacheKey);
    }
  }
  
  try {
    log('Fetching models from models.dev', { provider: providerId });
    const res = await fetch('https://models.dev/api.json', { timeout: 10000 });
    if (!res.ok) {
      log('models.dev fetch failed', { status: res.status, provider: providerId });
      return [];
    }
    
    const data = await res.json();
    const providerData = data[providerId.toLowerCase()];
    
    if (!providerData || !providerData.models) {
      log('No models found for provider', { provider: providerId });
      return [];
    }
    
    // Convert models.dev format to opencode format
    const models = {};
    for (const [modelId, modelInfo] of Object.entries(providerData.models)) {
      models[modelId] = {
        id: modelId,
        name: modelInfo.name || modelId,
        tool_call: modelInfo.tool_call ?? true,
        temperature: modelInfo.temperature ?? true
      };
    }
    
    // Cache the results
    cachedModelsDevProviders.set(cacheKey, models);
    cachedModelsDevProvidersAt.set(cacheKey, now);
    
    log('Fetched models from models.dev', { 
      provider: providerId, 
      count: Object.keys(models).length 
    });
    
    return models;
  } catch (error) {
    log('models.dev fetch error', { error: String(error), provider: providerId });
    return [];
  }
}

async function listModels(cliName = 'opencode') {
  const now = Date.now();
  const normalizedCli = normalizeCli(cliName);
  const cacheKey = normalizedCli;
  if (cachedModels.has(cacheKey) && now - (cachedModelsAt.get(cacheKey) || 0) < 60_000) return cachedModels.get(cacheKey);

  const collected = [];
  function addModel(m) {
    if (!m) return;
    if (typeof m === 'string') collected.push({ name: m, label: m });
    else if (m.name) collected.push({ name: m.name, label: m.label || m.name });
    else if (m.label) collected.push({ name: m.label, label: m.label });
  }

  const cliCommand = resolveCliCommand(normalizedCli);

  // Try CLI models (text output format: provider/model per line)
  try {
    const { stdout } = await runCommand(cliCommand, ['models'], { timeout: 15000 });
    const lines = stdout.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
    const modelLines = lines.filter(l => !l.match(/^(Usage:|Options:|Commands:|Examples?:|Description:|\s*-)/i));
    modelLines.forEach((name) => {
      if (name && name.length > 0 && name.length < 100) {
        addModel({ name, label: name });
      }
    });
  } catch (error) { log('Unable to read models', { cli: normalizedCli, error: String(error) }); }

  // Try provider and connection commands in parallel
  const providerCmds = [
    [cliCommand, ['providers', '--json']],
    [cliCommand, ['providers', 'list', '--json']],
    [cliCommand, ['connections', '--json']],
    [cliCommand, ['connections', 'list', '--json']],
    [cliCommand, ['providers']],
    [cliCommand, ['connections']],
  ];

  const providerPromises = providerCmds.map(([cmd, args]) =>
    runCommand(cmd, args, { timeout: 8000 })
      .then(({ stdout }) => ({ stdout, cmd, args }))
      .catch(err => ({ err, cmd, args }))
  );

  const results = await Promise.allSettled(providerPromises);

  for (const result of results) {
    if (result.status === 'fulfilled' && !result.value.err) {
      const { stdout } = result.value;
      try {
        const parsed = JSON.parse(stdout);
        const arr = Array.isArray(parsed) ? parsed : parsed.providers || parsed.connections || [];
        arr.forEach((p) => {
          if (!p) return;
          const providerName = p.name || p.id || p.provider || normalizedCli;
          if (Array.isArray(p.models) && p.models.length) p.models.forEach((m) => addModel({ name: `${providerName}/${m}`, label: `${providerName}/${m}` }));
          else if (Array.isArray(p.availableModels) && p.availableModels.length) p.availableModels.forEach((m) => addModel({ name: `${providerName}/${m}`, label: `${providerName}/${m}` }));
          else if (p.defaultModel) addModel({ name: `${providerName}/${p.defaultModel}`, label: `${providerName}/${p.defaultModel}` });
          else if (p.model) addModel({ name: `${providerName}/${p.model}`, label: `${providerName}/${p.model}` });
        });
      } catch (parseError) {
        // Fallback for text output
        const lines = stdout.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
        const contentLines = lines.filter(l => !l.match(/^(Usage:|Options:|Commands:|Examples?:|Description:|\s*-)/i));
        contentLines.forEach((line) => {
          const m = line.split(/[\s:\t-]+/).pop();
          if (m && m.length > 0 && m.length < 100) addModel({ name: m, label: `${m} (connected)` });
        });
      }
    }
  }

  // Fetch models from configured external providers
  const externalProviderFetches = [
    fetchOpenRouterModels(),
    fetchMistralModels(),
    fetchGroqModels(),
    fetchGoogleModels(),
    fetchNvidiaModels(),
    fetchChutesModels()
  ];

  const externalResults = await Promise.allSettled(externalProviderFetches);
  for (const result of externalResults) {
    if (result.status === 'fulfilled' && Array.isArray(result.value)) {
      result.value.forEach((m) => addModel(m));
    }
  }

  // Add fallback models per CLI
  const fallbackModels = {
    opencode: [],
  };

  // Add models from OPENCODE_EXTRA_MODELS env var
  if (process.env.OPENCODE_EXTRA_MODELS) {
    const extras = process.env.OPENCODE_EXTRA_MODELS.split(',').map((s) => s.trim()).filter(Boolean);
    extras.forEach((name) => addModel({ name, label: name }));
  }

  const unique = new Map();
  for (const m of collected) {
    const key = `${normalizedCli}:${encodeURIComponent((m.name || m.label || '').toLowerCase())}`;
    if (!unique.has(key)) unique.set(key, { name: m.name, label: m.label || m.name });
  }
  let result = Array.from(unique.values());
  if (!result.length) result = [{ name: 'default', label: 'default' }];
  cachedModels.set(cacheKey, result);
  cachedModelsAt.set(cacheKey, now);
  return result;
}

// Filter out opencode status messages that aren't meant for the user
function filterOpencodeStatusMessages(text) {
  if (!text) return '';
  const lines = text.split('\n');
  const filtered = lines.filter(line => {
    const lower = line.toLowerCase();
    // Filter out common status messages
    if (lower.includes('you need to open a file first')) return false;
    if (lower.includes('no file is currently open')) return false;
    if (lower.includes('use /open') && lower.includes('to open a file')) return false;
    if (lower.includes('session created') && lower.includes('ses-')) return false;
    if (lower.match(/^session:\s*ses-/i)) return false;
    if (lower.match(/^model:\s*\w+/i) && line.length < 50) return false;
    return true;
  });
  return filtered.join('\n').trim();
}

// Strip ANSI color codes and stray bracket-only codes and leading pipe prefixes
function stripAnsiAndPrefixes(text) {
  if (!text) return '';
  // Remove standard ANSI escape sequences like \x1b[93m
  let t = text.replace(/\x1b\[[0-9;]*m/g, '');
  // Remove stray bracket-only sequences like [93m or [0m that may appear when ESC is stripped
  t = t.replace(/\[\d+(?:;\d+)*m/g, '');
  // Remove any remaining ESC characters
  t = t.replace(/\u001b/g, '');
  // Remove leading pipe prefixes on each line (some CLIs prefix lines with "| ")
  t = t.split('\n').map(l => l.replace(/^\s*\|\s*/, '')).join('\n');
  return t;
}

// Detect if output looks like terminal/command output
function detectOutputType(text) {
  if (!text) return 'text';
  const lines = text.split('\n');

  // Check for terminal indicators
  const hasCommandPrompt = lines.some(l => l.match(/^\$\s+/) || l.match(/^>\s+/) || l.match(/^#\s+/));
  const hasExitCode = text.match(/exit\s+code:\s*\d+/i);
  const hasShellOutput = lines.some(l => l.match(/^\w+:\s*command not found/i) || l.match(/^\/[\w\/]+/));

  if (hasCommandPrompt || hasExitCode || hasShellOutput) {
    return 'terminal';
  }

  // Check for code blocks
  const hasCodeFence = text.includes('```');
  if (hasCodeFence) {
    return 'code';
  }

  return 'text';
}

function normalizeModels(list) {
  return (list || []).map((m) => (m || '').trim()).filter(Boolean);
}

function resolveOpenRouterModel() {
  const candidates = normalizeModels([
    openrouterSettings.primaryModel,
    openrouterSettings.backupModel1,
    openrouterSettings.backupModel2,
    openrouterSettings.backupModel3,
    ...OPENROUTER_FALLBACK_MODELS,
    ...OPENROUTER_STATIC_FALLBACK_MODELS,
  ]);
  return candidates[0] || OPENROUTER_DEFAULT_MODEL;
}

function resolveMistralModel() {
  const candidates = normalizeModels([
    mistralSettings.primaryModel,
    mistralSettings.backupModel1,
    mistralSettings.backupModel2,
    mistralSettings.backupModel3,
  ]);
  return candidates[0] || MISTRAL_DEFAULT_MODEL;
}

function uniqueStrings(values = []) {
  const seen = new Set();
  const result = [];
  (values || []).forEach((val) => {
    const trimmed = (val || '').trim();
    if (trimmed && !seen.has(trimmed)) {
      seen.add(trimmed);
      result.push(trimmed);
    }
  });
  return result;
}

function buildOpenRouterPlanChain() {
  return uniqueStrings([
    openrouterSettings.primaryModel,
    openrouterSettings.backupModel1,
    openrouterSettings.backupModel2,
    openrouterSettings.backupModel3,
    ...OPENROUTER_FALLBACK_MODELS,
    ...OPENROUTER_STATIC_FALLBACK_MODELS,
    OPENROUTER_DEFAULT_MODEL,
  ]);
}

function buildMistralPlanChain() {
  return uniqueStrings([
    mistralSettings.primaryModel,
    mistralSettings.backupModel1,
    mistralSettings.backupModel2,
    mistralSettings.backupModel3,
    MISTRAL_DEFAULT_MODEL,
  ]);
}

function buildGroqPlanChain() {
  // Groq uses fast models like Llama 3.3 70B and Mixtral
  return uniqueStrings([
    'llama-3.3-70b-versatile',
    'mixtral-8x7b-32768',
    'llama-3.1-70b-versatile',
  ]);
}

function buildGooglePlanChain() {
  // Google Gemini models
  return uniqueStrings([
    'gemini-1.5-flash',
    'gemini-1.5-pro',
    'gemini-pro',
  ]);
}

function buildNvidiaPlanChain() {
  // NVIDIA NIM models
  return uniqueStrings([
    'meta/llama-3.1-70b-instruct',
    'meta/llama-3.1-8b-instruct',
  ]);
}

function parseModelString(modelString) {
  // Parse a model string like "groq/compound-mini" or "compound-mini" into { provider, model }
  if (!modelString || typeof modelString !== 'string') {
    return { provider: null, model: '' };
  }

  const trimmed = modelString.trim();
  const parts = trimmed.split('/');

  if (parts.length > 1 && parts[0] && PLANNING_PROVIDERS.includes(normalizeProviderName(parts[0]))) {
    // Format: "provider/model"
    return {
      provider: normalizeProviderName(parts[0]),
      model: parts.slice(1).join('/')
    };
  }

  // No provider prefix, return just the model
  return { provider: null, model: trimmed };
}

function buildPlanModelChain() {
  const chain = normalizePlanningChain(planSettings.planningChain);
  if (chain.length) return chain;

  // Check if freePlanModel has a provider prefix (e.g., "groq/compound-mini")
  const freePlanModel = (planSettings.freePlanModel || '').trim();
  if (freePlanModel) {
    const parsed = parseModelString(freePlanModel);
    if (parsed.provider) {
      // User specified a provider prefix, use it directly
      return [{ provider: parsed.provider, model: parsed.model }];
    }
  }

  return defaultPlanningChainFromSettings(planSettings.provider);
}

function isPlanProviderConfigured(providerName) {
  const normalized = normalizeProviderName(providerName);
  if (normalized === 'openrouter') return !!OPENROUTER_API_KEY;
  if (normalized === 'mistral') return !!MISTRAL_API_KEY;
  if (normalized === 'google') return !!GOOGLE_API_KEY;
  if (normalized === 'groq') return !!GROQ_API_KEY;
  if (normalized === 'nvidia') return !!NVIDIA_API_KEY;
  if (normalized === 'ollama') return !!(OLLAMA_API_URL || OLLAMA_API_KEY);
  return false;
}

function parseProviderErrorDetail(detailText) {
  if (!detailText) return { text: '', code: '' };
  try {
    const parsed = JSON.parse(detailText);
    const text = parsed?.error?.message || parsed?.message || parsed?.error || detailText;
    const code = parsed?.error?.code || parsed?.code || parsed?.error?.type || '';
    return { text: typeof text === 'string' ? text : JSON.stringify(text), code: typeof code === 'string' ? code : '' };
  } catch (_) {
    return { text: detailText, code: '' };
  }
}

function buildProviderError(provider, status, detailText, codeHint = '') {
  const { text, code } = parseProviderErrorDetail(detailText || '');
  const lower = (text || '').toLowerCase();
  const err = new Error(`${provider} request failed (${status || 'error'}): ${text || 'Unknown error'}`);
  err.provider = provider;
  err.status = status;
  err.detail = text ? text.slice(0, 600) : '';
  err.code = codeHint || code || '';
  err.isAuthError = status === 401 || lower.includes('unauthorized') || lower.includes('api key') || lower.includes('invalid token') || lower.includes('invalid auth');
  err.isBillingError = status === 402 || lower.includes('payment required') || lower.includes('insufficient credit') || lower.includes('insufficient quota') || lower.includes('billing');
  err.isRateLimit = status === 429 || lower.includes('rate limit') || lower.includes('too many requests') || lower.includes('tokens per minute') || lower.includes('tpm');
  err.isTokenLimit = lower.includes('request too large') || lower.includes('token limit') || lower.includes('context length exceeded') || lower.includes('maximum context') || lower.includes('max tokens') || lower.includes('reduce your message size');
  err.isModelMissing = status === 404 || lower.includes('model not found') || lower.includes('unknown model') || lower.includes('does not exist');
  err.isServerError = typeof status === 'number' && status >= 500;
  err.shouldFallback = err.isModelMissing || err.isRateLimit || err.isTokenLimit || err.isServerError || err.isBillingError || err.isAuthError;
  err.rawDetail = detailText;
  return err;
}

function shouldFallbackProviderError(err) {
  return !!(err && err.shouldFallback);
}

async function loadOpenRouterPlanPrompt(userRequest) {
  const sanitizedRequest = sanitizePromptInput(userRequest);
  const fallback = `You are the planning specialist for the WordPress Plugin Builder. Stay in PLAN mode only and never write code.

User request:
{{USER_REQUEST}}

Create a concise, actionable plan: key features, WordPress hooks/APIs, data models, UI, security/GDPR, and a numbered roadmap. Ask for approval or changes.`;
  try {
    const prompt = await fs.readFile(OPENROUTER_PLAN_PROMPT_PATH, 'utf8');
    const trimmed = prompt?.trim();
    if (trimmed) {
      // Use a placeholder to prevent template injection
      return trimmed.replace('{{USER_REQUEST}}', sanitizedRequest);
    }
  } catch (err) {
    log('Failed to load OpenRouter plan prompt file', { path: OPENROUTER_PLAN_PROMPT_PATH, err: String(err) });
  }
  return fallback.replace('{{USER_REQUEST}}', sanitizedRequest);
}

async function sendOpenRouterChat({ messages, model }) {
  // Determine which API key to use based on model
  // Premium/paid models use OPENROUTER_PAID_API_KEY
  const isPremiumModel = model && (
    model.includes('gpt-4') ||
    model.includes('claude-3-opus') ||
    model.includes('claude-3-5-sonnet') ||
    model.includes('gemini-1.5-pro') ||
    model.includes('llama-3.1-405b')
  );
  
  const apiKey = isPremiumModel && OPENROUTER_PAID_API_KEY 
    ? OPENROUTER_PAID_API_KEY 
    : OPENROUTER_API_KEY;
  
  if (!apiKey) {
    log('OpenRouter API key missing, cannot fulfill planning request');
    throw new Error('OpenRouter API key is not configured');
  }
  if (!process.env.OPENROUTER_API_URL && !warnedOpenRouterApiUrl) {
    log('OPENROUTER_API_URL not set; using default OpenRouter endpoint', { url: DEFAULT_OPENROUTER_API_URL });
    warnedOpenRouterApiUrl = true;
  }

  const safeMessages = Array.isArray(messages) ? messages : [];
  if (!safeMessages.length) throw new Error('OpenRouter messages must be a non-empty array');

  const payload = { model: model || resolveOpenRouterModel(), messages: safeMessages };
  const headers = {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${apiKey}`,
  };
  if (OPENROUTER_SITE_URL) headers['HTTP-Referer'] = OPENROUTER_SITE_URL;
  headers['X-Title'] = OPENROUTER_APP_NAME;
  const res = await fetch(OPENROUTER_API_URL, { method: 'POST', headers, body: JSON.stringify(payload) });
  if (!res.ok) {
    let detail = '';
    try { detail = await res.text(); } catch (err) {
      log('OpenRouter error body read failed', { status: res.status, err: String(err) });
    }
    const err = buildProviderError('OpenRouter', res.status, detail || res.statusText);
    log('OpenRouter request failed', { status: res.status, detail: err.detail || res.statusText });
    throw err;
  }
  const data = await res.json();
  const reply = data?.choices?.[0]?.message?.content || '';
  return { reply: reply ? String(reply).trim() : '', raw: data };
}

async function sendMistralChat({ messages, model }) {
  if (!MISTRAL_API_KEY) {
    console.error('[MISTRAL] API key missing');
    log('Mistral API key missing, cannot fulfill planning request');
    throw new Error('Mistral API key is not configured');
  }

  const safeMessages = Array.isArray(messages) ? messages : [];
  if (!safeMessages.length) {
    console.error('[MISTRAL] Empty messages array');
    throw new Error('Mistral messages must be a non-empty array');
  }

  const resolvedModel = model || resolveMistralModel();
  const payload = { model: resolvedModel, messages: safeMessages };

  console.log('[MISTRAL] Starting API request', {
    url: MISTRAL_API_URL,
    model: resolvedModel,
    messageCount: safeMessages.length,
    hasApiKey: !!MISTRAL_API_KEY,
    apiKeyPrefix: MISTRAL_API_KEY ? MISTRAL_API_KEY.substring(0, 8) + '...' : 'none'
  });

  console.log('[MISTRAL] Request payload:', {
    model: payload.model,
    messagesCount: payload.messages.length,
    firstMessage: payload.messages[0] ? {
      role: payload.messages[0].role,
      contentLength: payload.messages[0].content?.length || 0,
      contentPreview: payload.messages[0].content?.substring(0, 100)
    } : null,
    lastMessage: payload.messages[payload.messages.length - 1] ? {
      role: payload.messages[payload.messages.length - 1].role,
      contentLength: payload.messages[payload.messages.length - 1].content?.length || 0,
      contentPreview: payload.messages[payload.messages.length - 1].content?.substring(0, 100)
    } : null
  });

  const headers = {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${MISTRAL_API_KEY}`,
  };

  try {
    const res = await fetch(MISTRAL_API_URL, { method: 'POST', headers, body: JSON.stringify(payload) });

    console.log('[MISTRAL] Response received', {
      status: res.status,
      statusText: res.statusText,
      ok: res.ok,
      headers: Object.fromEntries(res.headers.entries())
    });

    if (!res.ok) {
      let detail = '';
      try {
        detail = await res.text();
        console.error('[MISTRAL] Error response body:', detail);
      } catch (err) {
        console.error('[MISTRAL] Failed to read error body', String(err));
        log('Mistral error body read failed', { status: res.status, err: String(err) });
      }
      const err = buildProviderError('Mistral', res.status, detail || res.statusText);
      console.error('[MISTRAL] Request failed', { status: res.status, detail: err.detail });
      log('Mistral request failed', { status: res.status, detail: err.detail || res.statusText });
      throw err;
    }

    const data = await res.json();

    // Log the FULL raw response for debugging
    console.log('[MISTRAL] Full API response:', JSON.stringify(data, null, 2));

    // Log the response data structure analysis
    console.log('[MISTRAL] Response data structure:', {
      hasChoices: !!data?.choices,
      choicesLength: data?.choices?.length || 0,
      firstChoiceKeys: data?.choices?.[0] ? Object.keys(data.choices[0]) : [],
      hasMessage: !!data?.choices?.[0]?.message,
      messageKeys: data?.choices?.[0]?.message ? Object.keys(data.choices[0].message) : [],
      hasContent: !!data?.choices?.[0]?.message?.content,
      contentLength: data?.choices?.[0]?.message?.content?.length || 0,
      rawDataKeys: Object.keys(data || {})
    });

    // Log each step of content extraction
    console.log('[MISTRAL] Choices array:', data?.choices);
    console.log('[MISTRAL] First choice:', data?.choices?.[0]);
    console.log('[MISTRAL] Message object:', data?.choices?.[0]?.message);
    console.log('[MISTRAL] Content value:', data?.choices?.[0]?.message?.content);
    console.log('[MISTRAL] Content type:', typeof data?.choices?.[0]?.message?.content);

    const reply = data?.choices?.[0]?.message?.content || '';

    console.log('[MISTRAL] Extracted reply:', {
      reply: reply,
      replyType: typeof reply,
      replyLength: reply?.length || 0,
      isEmpty: reply === '',
      isNull: reply === null,
      isUndefined: reply === undefined,
      isFalsy: !reply
    });

    if (!reply) {
      console.error('[MISTRAL] No content in response!', {
        fullData: JSON.stringify(data, null, 2),
        extractedReply: reply,
        replyType: typeof reply
      });
    } else {
      console.log('[MISTRAL] Successfully extracted reply', {
        replyLength: reply.length,
        replyPreview: reply.substring(0, 200)
      });
    }

    log('Mistral request succeeded', { model: resolvedModel, replyLength: reply.length });
    return { reply: reply ? String(reply).trim() : '', model: resolvedModel, raw: data };
  } catch (fetchErr) {
    console.error('[MISTRAL] Fetch error:', {
      error: String(fetchErr),
      message: fetchErr.message,
      stack: fetchErr.stack
    });
    throw fetchErr;
  }
}

async function sendOpenRouterPlanWithFallback(messages, preferredModel) {
  const chain = preferredModel ? uniqueStrings([preferredModel, ...buildOpenRouterPlanChain()]) : buildOpenRouterPlanChain();
  const attempts = [];
  let lastError = null;
  for (const candidate of chain) {
    try {
      const result = await sendOpenRouterChat({ messages, model: candidate });
      if (attempts.length) {
        log('OpenRouter plan succeeded after fallback', { attempts, model: candidate });
      }
      return { ...result, model: candidate, attempts };
    } catch (err) {
      lastError = err;
      attempts.push({ model: candidate, error: err.message || String(err), status: err.status || null });
      if (!shouldFallbackProviderError(err)) break;
    }
  }
  const err = new Error('OpenRouter plan failed after trying all configured models');
  err.attempts = attempts;
  err.cause = lastError;
  throw err;
}

// Direct Google/Groq/NVIDIA handlers (use provider-specific API if configured; otherwise fall back to OpenRouter)
async function sendGoogleChat({ messages, model }) {
  if (!GOOGLE_API_KEY) throw new Error('Google API key is not configured');
  const safeMessages = Array.isArray(messages) ? messages : [];
  if (!safeMessages.length) throw new Error('Google messages must be a non-empty array');
  // Attempt typical Gemini-like endpoint; callers can override via GOOGLE_API_URL
  const targetModel = model || 'gemini-alpha';
  const url = `${GOOGLE_API_URL}/models/${encodeURIComponent(targetModel)}:generateMessage`;
  const headers = { 'Content-Type': 'application/json', 'Authorization': `Bearer ${GOOGLE_API_KEY}` };
  const payload = { messages: safeMessages };
  const res = await fetch(url, { method: 'POST', headers, body: JSON.stringify(payload) });
  if (!res.ok) {
    let detail = '';
    try { detail = await res.text(); } catch (_) { }
    const err = buildProviderError('Google', res.status, detail || res.statusText);
    log('Google request failed', { status: res.status, detail: err.detail || res.statusText });
    throw err;
  }
  const data = await res.json();
  const reply = data?.candidates?.[0]?.content || data?.output?.[0]?.content || '';
  return { reply: reply ? String(reply).trim() : '', model: targetModel, raw: data };
}

async function sendGroqChat({ messages, model }) {
  if (!GROQ_API_KEY) throw new Error('Groq API key is not configured');
  const safeMessages = Array.isArray(messages) ? messages : [];
  if (!safeMessages.length) throw new Error('Groq messages must be a non-empty array');
  // Use a valid Groq model - llama-3.3-70b-versatile is a good default
  const targetModel = model || 'llama-3.3-70b-versatile';
  const headers = {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${GROQ_API_KEY}`
  };
  // Groq uses OpenAI-compatible API format
  const payload = {
    model: targetModel,
    messages: safeMessages
  };

  console.log('[GROQ] Starting API request', {
    url: GROQ_API_URL,
    model: targetModel,
    messageCount: safeMessages.length
  });

  const res = await fetch(GROQ_API_URL, { method: 'POST', headers, body: JSON.stringify(payload) });

  console.log('[GROQ] Response received', {
    status: res.status,
    ok: res.ok
  });

  if (!res.ok) {
    let detail = '';
    try { detail = await res.text(); } catch (_) { }
    const err = buildProviderError('Groq', res.status, detail || res.statusText);
    log('Groq request failed', { status: res.status, detail: err.detail || res.statusText });
    console.error('[GROQ] Request failed', { status: res.status, detail });
    throw err;
  }
  const data = await res.json();

  console.log('[GROQ] Response data:', {
    hasChoices: !!data?.choices,
    choicesLength: data?.choices?.length || 0,
    model: data?.model
  });

  // Extract reply from OpenAI-compatible response format with validation
  if (!data?.choices || !Array.isArray(data.choices) || data.choices.length === 0) {
    console.error('[GROQ] Invalid response structure - no choices array', {
      dataKeys: Object.keys(data || {}),
      hasChoices: !!data?.choices,
      choicesType: typeof data?.choices
    });
    throw new Error('Groq API returned invalid response structure - missing choices array');
  }

  const reply = data.choices[0]?.message?.content || '';

  if (!reply) {
    console.error('[GROQ] No content in response', {
      firstChoice: data.choices[0],
      hasMessage: !!data.choices[0]?.message,
      messageKeys: data.choices[0]?.message ? Object.keys(data.choices[0].message) : []
    });
  }

  console.log('[GROQ] Extracted reply:', {
    replyLength: reply?.length || 0,
    replyPreview: reply ? reply.substring(0, 150) : 'empty'
  });

  return { reply: reply ? String(reply).trim() : '', model: data?.model || targetModel, raw: data };
}

async function sendNvidiaChat({ messages, model }) {
  if (!NVIDIA_API_KEY) throw new Error('NVIDIA API key is not configured');
  const safeMessages = Array.isArray(messages) ? messages : [];
  if (!safeMessages.length) throw new Error('NVIDIA messages must be a non-empty array');
  const targetModel = model || 'nvidia-model';
  const url = `${NVIDIA_API_URL}/models/${encodeURIComponent(targetModel)}/generate`;
  const headers = { 'Content-Type': 'application/json', 'Authorization': `Bearer ${NVIDIA_API_KEY}` };
  const payload = { messages: safeMessages };
  const res = await fetch(url, { method: 'POST', headers, body: JSON.stringify(payload) });
  if (!res.ok) {
    let detail = '';
    try { detail = await res.text(); } catch (_) { }
    const err = buildProviderError('NVIDIA', res.status, detail || res.statusText);
    log('NVIDIA request failed', { status: res.status, detail: err.detail || res.statusText });
    throw err;
  }
  const data = await res.json();
  const reply = data?.output?.text || '';
  return { reply: reply ? String(reply).trim() : '', model: targetModel, raw: data };
}

async function sendOllamaChat({ messages, model }) {
  const urlBase = OLLAMA_API_URL;
  if (!urlBase) throw new Error('Ollama API URL is not configured');
  const safeMessages = Array.isArray(messages) ? messages : [];
  if (!safeMessages.length) throw new Error('Ollama messages must be a non-empty array');

  // Build a simple prompt by joining roles - Ollama expects a text prompt by default
  const prompt = safeMessages.map((m) => `${m.role.toUpperCase()}: ${m.content}`).join('\n\n');
  const targetModel = model || OLLAMA_DEFAULT_MODEL || '';
  const endpoint = `${String(urlBase).replace(/\/$/, '')}/api/generate`;
  const headers = { 'Content-Type': 'application/json' };
  
  // Always include Authorization header if API key is configured
  // Some Ollama servers require authentication even if the key is empty
  if (OLLAMA_API_KEY) {
    headers['Authorization'] = `Bearer ${OLLAMA_API_KEY}`;
  } else {
    console.log('[OLLAMA] Warning: No API key configured. Server may reject request.');
  }
  
  const payload = { model: targetModel, prompt };

  const res = await fetch(endpoint, { 
    method: 'POST', 
    headers, 
    body: JSON.stringify(payload),
    signal: AbortSignal.timeout(30000) // 30 second timeout to prevent 504 gateway timeouts
  });
  if (!res.ok) {
    let detail = '';
    try { detail = await res.text(); } catch (_) { }
    const err = buildProviderError('Ollama', res.status, detail || res.statusText);
    log('Ollama request failed', { status: res.status, detail: err.detail || res.statusText });
    throw err;
  }

  let data;
  try { data = await res.json(); } catch (_) { data = {}; }

  // Try common fields returned by Ollama
  const reply = data?.text || data?.generated_text || data?.result || (data?.response?.[0]?.content) || '';
  return { reply: reply ? String(reply).trim() : '', model: targetModel || data?.model || '', raw: data };
}

async function sendMistralPlanWithFallback(messages, preferredModel) {
  const chain = preferredModel ? uniqueStrings([preferredModel, ...buildMistralPlanChain()]) : buildMistralPlanChain();
  console.log('[MISTRAL] Starting fallback chain', {
    preferredModel,
    chainLength: chain.length,
    models: chain
  });

  const attempts = [];
  let lastError = null;
  for (const candidate of chain) {
    console.log('[MISTRAL] Trying model:', candidate);
    try {
      const result = await sendMistralChat({ messages, model: candidate });
      if (attempts.length) {
        console.log('[MISTRAL] Plan succeeded after fallback', { attempts, model: candidate });
        log('Mistral plan succeeded after fallback', { attempts, model: candidate });
      } else {
        console.log('[MISTRAL] Plan succeeded on first try', { model: candidate });
      }
      return { ...result, model: candidate, attempts };
    } catch (err) {
      lastError = err;
      console.error('[MISTRAL] Model failed:', {
        model: candidate,
        error: err.message || String(err),
        status: err.status || null,
        shouldFallback: shouldFallbackProviderError(err)
      });
      attempts.push({ model: candidate, error: err.message || String(err), status: err.status || null });
      if (!shouldFallbackProviderError(err)) {
        console.error('[MISTRAL] Breaking fallback chain - error not fallback-eligible');
        break;
      }
    }
  }
  console.error('[MISTRAL] All models failed', {
    attempts,
    lastError: lastError ? String(lastError) : 'none'
  });
  const err = new Error('Mistral plan failed after trying all configured models');
  err.attempts = attempts;
  err.cause = lastError;
  throw err;
}


async function handlePlanMessage(req, res, userId) {
  let body;
  try {
    body = await parseJsonBody(req);
  } catch (error) {
    return sendJson(res, 400, { error: 'Invalid JSON body' });
  }
  const sessionId = body.sessionId || body.session || body.session_id;
  if (!sessionId) return sendJson(res, 400, { error: 'sessionId is required' });
  const session = getSession(sessionId, userId);
  if (!session) return sendJson(res, 404, { error: 'Session not found' });

  // Helper function to strip markdown formatting from display content
  function stripMarkdownFromDisplay(text) {
    if (!text || typeof text !== 'string') return text;
    let cleaned = text;
    cleaned = cleaned.replace(/\*\*([^*]+)\*\*/g, '$1');
    cleaned = cleaned.replace(/\*\*/g, '');
    cleaned = cleaned.replace(/^##\s+/gm, '');
    cleaned = cleaned.replace(/##/g, '');
    return cleaned;
  }

  // Sanitize user input to prevent prompt injection
  const content = sanitizePromptInput(body.content || '');
  const displayContent = typeof body.displayContent === 'string' && body.displayContent.trim() ? 
    stripMarkdownFromDisplay(sanitizePromptInput(body.displayContent)) : 
    stripMarkdownFromDisplay(content);
  if (!content) return sendJson(res, 400, { error: 'Message is required' });
  const userPlan = resolveUserPlan(session.userId);
  const allowance = canConsumeTokens(session.userId, userPlan, estimateTokensFromText(content) + TOKEN_ESTIMATION_BUFFER);
  if (!allowance.allowed) {
    return sendJson(res, 402, { error: 'You have reached your token allowance. Upgrade or add a boost.', allowance });
  }

  try {
    await ensureSessionPaths(session);
    const planRoot = sanitizePromptInput(session.planUserRequest || content);
    const systemPrompt = await loadOpenRouterPlanPrompt(planRoot);
    // Replace plugin slug and name placeholders if present in the planning prompt
    const safePluginSlugRaw = session.pluginSlug || session.title || 'plugin';
    const safePluginSlugBase = sanitizeSegment(safePluginSlugRaw, 'plugin');
    const safePluginSlug = safePluginSlugBase.startsWith('pc-') ? safePluginSlugBase : `pc-${safePluginSlugBase}`;
    const safePluginName = session.pluginName || `Plugin Compass ${session.title || 'Plugin'}`;
    let finalSystemPrompt = (systemPrompt || '').replace('{{PLUGIN_SLUG}}', safePluginSlug);
    finalSystemPrompt = finalSystemPrompt.replace('{{PLUGIN_NAME}}', safePluginName);
    const historyMessages = (session.messages || []).filter((m) => m.phase === 'plan');
    const messages = [{ role: 'system', content: finalSystemPrompt }];
    historyMessages.forEach((m) => {
      if (m.content) messages.push({ role: 'user', content: sanitizePromptInput(m.content) });
      if (m.reply) messages.push({ role: 'assistant', content: sanitizeAiOutput(m.reply) });
    });
    messages.push({ role: 'user', content });

    let model;
    let reply;
    let cli;
    let failoverAttempts = [];
    let providerUsed = null;
    // Build the normalized plan chain and filter out providers that are not configured
    const normalizedChain = buildPlanModelChain();
    const planChain = normalizedChain.filter((entry) => isPlanProviderConfigured(entry.provider));

    if (!planChain.length) {
      // If the normalized chain requested providers that are not configured, report which ones
      const requestedProviders = Array.from(new Set((normalizedChain || []).map((e) => normalizeProviderName(e.provider))));
      const missing = requestedProviders.filter((p) => !isPlanProviderConfigured(p));
      console.log('[PLAN] No configured providers found for plan chain', { requestedProviders, missing });
      const msg = missing.length
        ? `No configured planning providers available. Missing provider API keys: ${missing.join(', ')}.`
        : 'Planning is not configured. Please configure a planning provider first.';
      return sendJson(res, 503, { error: msg, missingProviders: missing });
    }

    console.log('[PLAN] Starting plan message handling', {
      sessionId: session.id,
      planChainLength: planChain.length,
      providers: planChain.map(e => `${e.provider}:${e.model || 'default'}`)
    });

    for (const entry of planChain) {
      const providerName = normalizeProviderName(entry.provider);
      const modelHint = entry.raw || entry.model || '';
      console.log('[PLAN] Trying provider', { provider: providerName, model: modelHint });

      const limitState = isProviderLimited(providerName, modelHint || 'plan');
      if (limitState.limited) {
        console.log('[PLAN] Provider limited, skipping', { provider: providerName, reason: limitState.reason });
        failoverAttempts.push({ provider: providerName, model: modelHint, error: `limit: ${limitState.reason}` });
        continue;
      }
      try {
        let result;
        if (providerName === 'mistral') {
          console.log('[PLAN] Using Mistral provider', { modelHint, messagesCount: messages.length });
          result = await sendMistralPlanWithFallback(messages, modelHint);
          cli = 'mistral';
          console.log('[PLAN] Mistral result received (raw):', {
            hasResult: !!result,
            resultKeys: result ? Object.keys(result) : [],
            hasReply: !!result?.reply,
            replyValue: result?.reply,
            replyType: typeof result?.reply,
            replyLength: result?.reply?.length || 0,
            model: result?.model,
            fullResult: JSON.stringify(result, null, 2)
          });
        } else if (providerName === 'google') {
          // Google is a separate provider and requires GOOGLE_API_KEY to be configured
          console.log('[PLAN] Using Google provider', { modelHint });
          result = await sendGoogleChat({ messages, model: modelHint });
          cli = 'google';
        } else if (providerName === 'groq') {
          console.log('[PLAN] Using Groq provider', { modelHint });
          result = await sendGroqChat({ messages, model: modelHint });
          cli = 'groq';
        } else if (providerName === 'nvidia') {
          console.log('[PLAN] Using NVIDIA provider', { modelHint });
          result = await sendNvidiaChat({ messages, model: modelHint });
          cli = 'nvidia';
        } else if (providerName === 'ollama') {
          console.log('[PLAN] Using Ollama provider', { modelHint });
          result = await sendOllamaChat({ messages, model: modelHint });
          cli = 'ollama';
        } else {
          // default to OpenRouter if configured (explicit choice)
          console.log('[PLAN] Using OpenRouter provider', { modelHint });
          result = await sendOpenRouterPlanWithFallback(messages, modelHint);
          cli = 'openrouter';
        }

        model = result.model || modelHint;

        console.log('[PLAN] Before sanitization:', {
          provider: providerName,
          rawReply: result.reply,
          rawReplyType: typeof result.reply,
          rawReplyLength: result.reply?.length || 0
        });

        reply = sanitizeAiOutput(result.reply);

        console.log('[PLAN] After sanitization:', {
          provider: providerName,
          sanitizedReply: reply,
          sanitizedReplyType: typeof reply,
          sanitizedReplyLength: reply?.length || 0,
          wasModified: result.reply !== reply
        });

        console.log('[PLAN] Provider succeeded', {
          provider: providerName,
          model,
          replyLength: reply?.length || 0,
          replyPreview: reply ? reply.substring(0, 150) : 'no reply'
        });

        failoverAttempts = failoverAttempts.concat(result.attempts || []);
        providerUsed = providerName;
        const tokensUsed = extractTokenUsageFromResult(result, messages);
        // Ensure we log provider usage
        recordProviderUsage(providerName, model || modelHint || providerName, tokensUsed, 1);

        const recordId = userId || session.userId;
        if (recordId) {
          console.log(`[PLAN] Recording tokens: user=${recordId} tokens=${tokensUsed} provider=${providerName}`);
          // Await this to ensure file is written before we return response to client
          await recordUserTokens(recordId, tokensUsed);
        } else {
          console.error('[PLAN] Cannot record tokens: no userId available');
        }
        break;
      } catch (err) {
        console.error('[PLAN] Provider failed', {
          provider: providerName,
          model: modelHint,
          error: err.message || String(err),
          status: err.status || null,
          shouldFallback: shouldFallbackProviderError(err)
        });
        failoverAttempts.push({ provider: providerName, model: modelHint, error: err.message || String(err), status: err.status || null });
        if (!shouldFallbackProviderError(err)) {
          console.error('[PLAN] Breaking provider chain - error not fallback-eligible');
          break;
        }
      }
    }

    if (!providerUsed) {
      console.error('[PLAN] No provider succeeded', { failoverAttempts });
      return sendJson(res, 429, { error: 'All planning providers are rate limited or unavailable' });
    }

    const now = new Date().toISOString();

    console.log('[PLAN] Creating message object with reply:', {
      reply: reply,
      replyType: typeof reply,
      replyLength: reply?.length || 0,
      replyPreview: reply ? reply.substring(0, 200) : 'empty'
    });

    const message = {
      id: randomUUID(),
      role: 'user',
      content,
      displayContent,
      model,
      cli,
      status: 'done',
      createdAt: now,
      updatedAt: now,
      finishedAt: now,
      reply,
      phase: 'plan',
    };

    if (failoverAttempts.length) message.failoverAttempts = failoverAttempts;

    if (!session.planUserRequest) session.planUserRequest = planRoot;
    const cleanReply = reply && reply.trim ? reply.trim() : reply;

    console.log('[PLAN] Final message details:', {
      messageId: message.id,
      messageReply: message.reply,
      messageReplyLength: message.reply?.length || 0,
      cleanReply: cleanReply,
      cleanReplyLength: cleanReply?.length || 0,
      planSummary: session.planSummary,
      planSummaryLength: session.planSummary?.length || 0
    });

    session.planSummary = cleanReply || session.planSummary;
    session.planApproved = false;
    session.messages.push(message);
    session.updatedAt = now;
    await persistState();

    console.log('[PLAN] Plan message completed successfully', {
      sessionId: session.id,
      provider: providerUsed,
      model,
      replyLength: cleanReply?.length || 0,
      hasFailovers: failoverAttempts.length > 0
    });

    return sendJson(res, 200, { message, model, planSummary: session.planSummary });
  } catch (error) {
    console.error('[PLAN] Plan message handler error', {
      error: error.message || String(error),
      stack: error.stack,
      attempts: error.attempts
    });
    const attemptInfo = Array.isArray(error.attempts)
      ? error.attempts.map((a) => `${a.model || 'unknown'}: ${a.error || 'error'}`).join(' | ')
      : '';
    const message = error.message || 'Failed to process plan message';
    const composed = attemptInfo ? `${message} (${attemptInfo})` : message;
    return sendJson(res, 500, { error: composed });
  }
}

async function sendToOpencode({ session, model, content, message, cli, streamCallback, opencodeSessionId }) {
  const clean = sanitizeMessage(content);
  
  // Validate content is properly sanitized
  if (clean === null || clean === undefined) {
    throw new Error('Message content failed sanitization');
  }
  
  // Convert to string if needed and ensure it's not empty after trimming
  const contentStr = String(clean).trim();
  if (!contentStr) {
    throw new Error('Message cannot be empty after sanitization');
  }
  if (!session) throw new Error('Session is required for OpenCode commands');
  await ensureSessionPaths(session);
  if (!session.workspaceDir) throw new Error('Session workspace directory not initialized');
  const workspaceDir = session.workspaceDir;
  const cliName = normalizeCli(cli || session?.cli);
  const cliCommand = resolveCliCommand(cliName);
  verifyOpencodeSetup(cliCommand);
  
  // Ensure model is properly resolved
  const resolvedModel = model || session.model;
  if (!resolvedModel) {
    throw new Error('Model is required for OpenCode commands');
  }
  
  const args = ['run', '--format', 'json', '--model', resolvedModel];

  // Add session ID if available
  if (opencodeSessionId) {
    args.push('--session', opencodeSessionId);
  }

  // Configure WP CLI Testing MCP server if feature is enabled
  const wpCliMcpEnabled = message?.externalTestingEnabled === true;
  log('WP CLI Testing MCP server configuration', { 
    enabled: wpCliMcpEnabled, 
    messageId: message?.id,
    sessionId: session.id 
  });

  // Ensure content is properly passed as the final argument
  if (typeof clean !== 'string' || clean.length === 0) {
    throw new Error('Message content is invalid or empty after sanitization');
  }

  args.push(clean);
  
  log('Preparing OpenCode CLI command', { 
    cli: cliName, 
    cmd: cliCommand, 
    model: resolvedModel,
    sessionId: session.id,
    messageId: message?.id,
    contentLength: clean.length,
    contentPreview: clean.substring(0, 100).replace(/\n/g, '\\n')
  });
  let partialOutput = '';
  let lastStreamTime = Date.now();
  const messageKey = message?.id;
  let sessionsBefore = null;

  try {
    log('Running CLI', { cli: cliName, cmd: cliCommand, args: args.slice(0, -1).concat(['[content]']), messageId: messageKey, workspaceDir, opencodeSessionId });

  if (!opencodeSessionId) {
    try {
      sessionsBefore = await listOpencodeSessions(workspaceDir);
    } catch (err) {
      log('Failed to list opencode sessions before run', { error: String(err) });
    }
  }

  // Verify CLI command exists before attempting to run
  try {
    fsSync.accessSync(cliCommand, fsSync.constants.X_OK);
    log('OpenCode CLI verified', { cliCommand });
  } catch (cliError) {
    log('OpenCode CLI not found or not executable', { cliCommand, error: String(cliError) });
    throw new Error(`OpenCode CLI not found or not executable: ${cliCommand}. Please ensure OpenCode is properly installed.`);
  }

  // Log the full command that will be executed
  const fullCommandStr = `${cliCommand} ${args.map(arg => {
    const str = String(arg);
    if (str.includes(' ') || str.includes('"') || str.includes("'") || str.includes('\\')) {
      return `"${str.replace(/"/g, '\\"')}"`;
    }
    return str;
  }).join(' ')}`;
  
  log('Executing OpenCode command', {
    command: fullCommandStr,
    workspaceDir,
    messageId: messageKey
  });

  // Mark process as running - this allows tracking even if SSE stream closes
  // The process will continue running independently of the HTTP connection
  if (messageKey) {
    runningProcesses.set(messageKey, { started: Date.now(), cli: cliName, model });
  }

    let capturedSessionId = null;

    // Use the OpenCode process manager for execution
    // This ensures all sessions share the same OpenCode instance when possible
    
    // Build environment variables including WP CLI MCP server config
    const executionEnv = {
      ...process.env,
      OPENAI_API_KEY: OPENCODE_OLLAMA_API_KEY,
      CHUTES_API_KEY: CHUTES_API_KEY,
      GROQ_API_KEY: GROQ_API_KEY,
      GOOGLE_API_KEY: GOOGLE_API_KEY,
      NVIDIA_API_KEY: NVIDIA_API_KEY,
    };
    
    // Add WP CLI Testing MCP server to OpenCode config if enabled
    if (wpCliMcpEnabled) {
      const wpMcpServerPath = path.resolve(__dirname, '../opencode/mcp-servers/wp-cli-testing/index.js');
      log('Enabling WP CLI Testing MCP server', { path: wpMcpServerPath, messageId: message?.id });
      
      // Configure OpenCode to include the WP CLI testing MCP server
      // We pass this via environment variable that OpenCode can read
      executionEnv.OPENCODE_EXTRA_MCP_SERVERS = JSON.stringify([
        {
          name: 'wp-cli-testing',
          command: 'node',
          args: [wpMcpServerPath],
          disabled: false
        }
      ]);
    } else {
      // Safety: ensure the tools are not injected when the builder toggle is off,
      // even if the parent process environment happens to have the variable set.
      if (executionEnv.OPENCODE_EXTRA_MCP_SERVERS) {
        delete executionEnv.OPENCODE_EXTRA_MCP_SERVERS;
      }
    }
    
    const { stdout, stderr } = await opencodeManager.executeInSession(
      session?.id || 'standalone',
      workspaceDir,
      cliCommand,
      args,
      {
        timeout: 600000, // 10 minute timeout to prevent stuck processes
        env: executionEnv,
        onData: (type, chunk) => {
        try {
          const now = Date.now();
          const chunkStr = chunk.toString();

          // Split chunk into lines (since output is newline-delimited JSON)
          const lines = chunkStr.split('\n');

          for (const line of lines) {
            if (!line.trim()) continue;

            // Parse JSON line
            try {
              const event = JSON.parse(line);

              // Capture session ID from any event that has it
              // CRITICAL FIX: Only update session ID if we don't already have an explicit one
              // This prevents overwriting the passed opencodeSessionId with a new auto-generated one
              if (!capturedSessionId && event.sessionID) {
                capturedSessionId = event.sessionID;
                log('Captured session ID from JSON event', { capturedSessionId, messageType: event.type, messageId: messageKey, existingOpencodeSessionId: opencodeSessionId });
                if (session) {
                  // Only update session.opencodeSessionId if no explicit session was passed
                  // This ensures we don't overwrite the intended session ID with an auto-generated one
                  if (!opencodeSessionId || !session.opencodeSessionId) {
                    session.opencodeSessionId = capturedSessionId;
                    // Lock this as the initial session if not already set
                    if (!session.initialOpencodeSessionId) {
                      session.initialOpencodeSessionId = capturedSessionId;
                      log('Locked initial opencode session', { initialOpencodeSessionId: capturedSessionId, sessionId: session.id });
                    }
                    persistState().catch(() => { });
                  } else {
                    log('Preserving existing opencode session ID (not overwriting with captured)', {
                      existing: session.opencodeSessionId || opencodeSessionId,
                      captured: capturedSessionId
                    });
                  }
                }
              }

              // Extract token usage from step_finish events
              if (event.type === 'step_finish' && event.part?.tokens) {
                const inputTokens = event.part.tokens.input || 0;
                const outputTokens = event.part.tokens.output || 0;
                const reasoningTokens = event.part.tokens.reasoning || 0;
                const totalTokens = inputTokens + outputTokens + reasoningTokens;

                if (totalTokens > 0) {
                  // Accumulate tokens if we've already captured some
                  if (message.opencodeTokensUsed) {
                    message.opencodeTokensUsed += totalTokens;
                  } else {
                    message.opencodeTokensUsed = totalTokens;
                  }
                  log('Captured token usage from step_finish event', {
                    inputTokens,
                    outputTokens,
                    reasoningTokens,
                    totalTokens,
                    accumulatedTokens: message.opencodeTokensUsed,
                    messageId: messageKey
                  });
                }
              }

               // Capture todo tool events
              if (event.type === 'tool' && event.tool === 'todowrite' && event.input?.todos) {
                const todos = event.input.todos;
                if (message) {
                  message.todos = todos;
                  log('Captured todos from todowrite tool', {
                    messageId: messageKey,
                    todoCount: todos.length,
                    todos: todos.map(t => ({ id: t.id, content: t.content?.substring(0, 50), status: t.status }))
                  });

                  // Broadcast todos to SSE clients
                  if (messageKey && activeStreams.has(messageKey)) {
                    const streams = activeStreams.get(messageKey);
                    const data = JSON.stringify({
                      type: 'todos',
                      todos: todos,
                      timestamp: new Date().toISOString()
                    });
                    streams.forEach(res => {
                      try {
                        res.write(`data: ${data}\n\n`);
                      } catch (err) {
                        log('SSE todos write error', { err: String(err) });
                      }
                    });
                  }
                  persistState().catch(() => { });
                }
              }

              if (event.type === 'todo.updated' && event.properties?.todos) {
                const todos = event.properties.todos;
                if (message) {
                  message.todos = todos;
                  log('Captured todos from todo.updated event', {
                    messageId: messageKey,
                    todoCount: todos.length,
                    todos: todos.map(t => ({ id: t.id, content: t.content?.substring(0, 50), status: t.status }))
                  });

                  if (messageKey && activeStreams.has(messageKey)) {
                    const streams = activeStreams.get(messageKey);
                    const data = JSON.stringify({
                      type: 'todos',
                      todos: todos,
                      timestamp: new Date().toISOString()
                    });
                    streams.forEach(res => {
                      try {
                        res.write(`data: ${data}\n\n`);
                      } catch (err) {
                        log('SSE todos write error', { err: String(err) });
                      }
                    });
                  }
                  persistState().catch(() => { });
                }
              }

              // Extract text from text events
              if (event.type === 'text' && event.part?.text) {
                partialOutput += event.part.text;

                // Filter out status messages
                const filtered = filterOpencodeStatusMessages(partialOutput);

                if (message) {
                  message.partialOutput = filtered;
                  message.outputType = detectOutputType(filtered);
                  message.partialUpdatedAt = new Date().toISOString();

                  // Only persist every 500ms to avoid excessive writes
                  if (now - lastStreamTime > 500) {
                    persistState().catch(() => { });
                    lastStreamTime = now;
                  }
                }

                // Stream to clients immediately
                const cleanChunk = event.part.text;
                if (streamCallback) {
                  streamCallback({
                    type: 'chunk',
                    content: cleanChunk,
                    filtered: filtered,
                    outputType: message?.outputType,
                    timestamp: new Date().toISOString()
                  });
                }

                // Broadcast to SSE clients
                if (messageKey && activeStreams.has(messageKey)) {
                  const streams = activeStreams.get(messageKey);
                  const data = JSON.stringify({
                    type: 'chunk',
                    content: cleanChunk,
                    filtered: filtered,
                    outputType: message?.outputType,
                    partialOutput: filtered,
                    timestamp: new Date().toISOString()
                  });
                  streams.forEach(res => {
                    try {
                      res.write(`data: ${data}\n\n`);
                    } catch (err) {
                      log('SSE write error', { err: String(err) });
                    }
                  });
                }

                log('cli chunk', { cli: cliName, type: 'text', messageId: messageKey, length: cleanChunk.length, filtered: filtered.slice(0, 100) });
              }
            } catch (jsonErr) {
              // Line is not valid JSON - might be partial line or error output
              log('Failed to parse JSON line', { line: line.substring(0, 200), error: String(jsonErr), messageId: messageKey });
            }
          }
        } catch (err) {
          log('onData handler error', { err: String(err) });
        }
      }
    });

    // Process complete
    if (messageKey) {
      runningProcesses.delete(messageKey);
    }

    // Use accumulated text output (partialOutput) since we're using --format json
    const finalOutput = filterOpencodeStatusMessages(partialOutput || '');

    // Check for completion signal and process it
    const completionMatch = finalOutput.match(/\s*[COMPLETE]\s*$/i);
    let processedOutput = finalOutput;
    if (completionMatch) {
      // Remove the completion signal from the output
      processedOutput = finalOutput.slice(0, completionMatch.index).trim();
      log('OpenCode completion signal detected', {
        messageId: messageKey,
        hadCompletionSignal: true,
        originalLength: finalOutput.length,
        processedLength: processedOutput.length
      });
    }

    // Mark message as done to prevent false "stalled" detection
    if (message) {
      message.status = 'done';
      message.finishedAt = new Date().toISOString();
      if (session) {
        session.updatedAt = message.finishedAt;
      }
      if (message.todos && Array.isArray(message.todos) && message.todos.length) {
        message.todos = [];
      }
    }

    // The reply is the final accumulated output
    let reply = processedOutput;

    if (message) {
      message.partialOutput = finalOutput;
      message.reply = reply; // Set reply on message object before sending completion event
      message.outputType = detectOutputType(finalOutput);
      message.partialUpdatedAt = new Date().toISOString();
      message.opencodeExitCode = 0;
      message.opencodeSummary = finalOutput.slice(0, 800) || `No output from ${cliName} (exit 0)`;
      persistState().catch(() => { });
    }

    // Send completion event to SSE clients with processed reply
    if (messageKey && activeStreams.has(messageKey)) {
      const streams = activeStreams.get(messageKey);
      const data = JSON.stringify({
        type: 'complete',
        content: reply, // Send the processed reply instead of raw finalOutput
        outputType: message?.outputType,
        exitCode: 0,
        timestamp: new Date().toISOString()
      });
      streams.forEach(res => {
        try {
          res.write(`data: ${data}\n\n`);
          res.end();
        } catch (err) {
          log('SSE completion error', { err: String(err) });
        }
      });
      activeStreams.delete(messageKey);
    }

    log('cli finished', { cli: cliName, messageId: messageKey, outputLength: finalOutput.length, replyLength: reply.length });

    if (!opencodeSessionId && session && !session.opencodeSessionId) {
      try {
        const sessionsAfter = await listOpencodeSessions(workspaceDir);
        const beforeIds = new Set((sessionsBefore || []).map((s) => s.id));
        const newSessions = sessionsAfter.filter((s) => s.id && !beforeIds.has(s.id));
        const candidates = newSessions.length ? newSessions : sessionsAfter;

        if (candidates.length) {
          const sorted = candidates.slice().sort((a, b) => {
            const aTime = Date.parse(a.updatedAt || a.createdAt || '') || 0;
            const bTime = Date.parse(b.updatedAt || b.createdAt || '') || 0;
            return bTime - aTime;
          });
          const detected = sorted[0]?.id || null;
          if (detected) {
            session.opencodeSessionId = detected;
            // CRITICAL: Only set initialOpencodeSessionId if not already set
            // This prevents overwriting an existing initial session with a new one
            if (!session.initialOpencodeSessionId) {
              session.initialOpencodeSessionId = detected;
              log('Recovered and locked opencode session ID from session list', { detected, messageId: messageKey });
            } else {
              log('Recovered opencode session ID but preserving existing initial session', {
                detected,
                initialSessionId: session.initialOpencodeSessionId,
                messageId: messageKey
              });
            }
            opencodeSessionId = session.initialOpencodeSessionId || detected;
            persistState().catch(() => { });
          }
        }
      } catch (err) {
        log('Failed to recover opencode session ID from session list', { error: String(err), messageId: messageKey });
      }
    } else if (!opencodeSessionId && session && session.opencodeSessionId) {
      // No explicit session was passed but session has one - log for continuity verification
      log('Using stored opencode session ID (no explicit session in request)', {
        sessionId: session.id,
        opencodeSessionId: session.opencodeSessionId,
        messageId: messageKey
      });
    }

    // Extract token usage from the parsed response if available
    let tokensUsed = 0;
    let tokenSource = 'none';
    const tokenExtractionLog = [];

    // First, check if we captured token usage from the stream (JSON events)
    if (message && message.opencodeTokensUsed) {
      const candidateTokens = message.opencodeTokensUsed;
      const validation = validateTokenCount(candidateTokens, {
        contentLength: finalOutput?.length || 0,
        source: 'stream'
      });

      if (validation.valid) {
        tokensUsed = candidateTokens;
        tokenSource = 'stream';
        tokenExtractionLog.push({ method: 'stream', success: true, value: tokensUsed, validation: 'passed' });
        log('✓ Token extraction: Using token usage captured from stream', { tokensUsed, messageId: messageKey });
      } else {
        tokenExtractionLog.push({
          method: 'stream',
          success: false,
          value: candidateTokens,
          validation: 'failed',
          validationReason: validation.reason
        });
        log('✗ Token extraction: Stream tokens failed validation', {
          tokens: candidateTokens,
          reason: validation.reason,
          messageId: messageKey
        });
      }
    } else if (message) {
      tokenExtractionLog.push({ method: 'stream', success: false, reason: 'message.opencodeTokensUsed not set during streaming' });
    }

    // If no tokens found in response, try to get from session
    if (!tokensUsed && capturedSessionId && workspaceDir) {
      try {
        tokenExtractionLog.push({ method: 'session_query', attempt: 'starting', sessionId: capturedSessionId });
        const sessionTokens = await getOpencodeSessionTokenUsage(capturedSessionId, workspaceDir);
        if (sessionTokens > 0) {
          const validation = validateTokenCount(sessionTokens, {
            contentLength: finalOutput?.length || 0,
            source: 'session'
          });

          if (validation.valid) {
            tokensUsed = sessionTokens;
            tokenSource = 'session';
            tokenExtractionLog.push({ method: 'session_query', success: true, value: tokensUsed, validation: 'passed' });
            log('✓ Token extraction: Got tokens from session info', { opencodeSessionId: capturedSessionId, tokensUsed, messageId: messageKey });
          } else {
            tokenExtractionLog.push({
              method: 'session_query',
              success: false,
              value: sessionTokens,
              validation: 'failed',
              validationReason: validation.reason
            });
            log('✗ Token extraction: Session tokens failed validation', {
              opencodeSessionId: capturedSessionId,
              tokens: sessionTokens,
              reason: validation.reason,
              messageId: messageKey
            });
          }
        } else {
          tokenExtractionLog.push({ method: 'session_query', success: false, reason: 'session query returned 0 tokens' });
          log('✗ Token extraction: Session query returned 0 tokens', { opencodeSessionId: capturedSessionId, messageId: messageKey });
        }
      } catch (sessionErr) {
        tokenExtractionLog.push({ method: 'session_query', success: false, error: String(sessionErr) });
        log('✗ Token extraction: Failed to get session token usage', { opencodeSessionId: capturedSessionId, error: String(sessionErr), messageId: messageKey });
      }
    } else if (!tokensUsed) {
      const reason = !capturedSessionId ? 'no capturedSessionId' : !workspaceDir ? 'no workspaceDir' : 'unknown';
      tokenExtractionLog.push({ method: 'session_query', success: false, reason });
    }

    // Log full extraction summary
    if (!tokensUsed) {
      log('⚠️  Token extraction: All methods failed, will fall back to estimation', {
        messageId: messageKey,
        extractionLog: tokenExtractionLog,
        hadStream: !!message?.opencodeTokensUsed,
        hadOutput: !!finalOutput,
        hadSessionId: !!capturedSessionId
      });
    } else {
      log('✅ Token extraction successful', {
        messageId: messageKey,
        tokensUsed,
        tokenSource,
        extractionLog: tokenExtractionLog
      });
    }

    return { reply, raw: null, tokensUsed, tokenSource, tokenExtractionLog };
  } catch (error) {
    // Process failed
    if (messageKey) {
      runningProcesses.delete(messageKey);
    }

    // Try to parse error from JSON format
    let errorOutput = '';
    const errorStr = error.stderr || error.stdout || '';
    const errorLines = errorStr.split('\n');

    for (const line of errorLines) {
      if (!line.trim()) continue;
      try {
        const event = JSON.parse(line);
        if (event.type === 'error' && event.error) {
          errorOutput += event.error.data?.message || event.error.message || JSON.stringify(event.error);
        } else if (event.type === 'text' && event.part?.text) {
          errorOutput += event.part.text;
        }
      } catch (_) {
        // Not JSON, include as-is
        errorOutput += line + '\n';
      }
    }

    if (!errorOutput) {
      errorOutput = filterOpencodeStatusMessages(stripAnsiAndPrefixes(errorStr));
    }

    const msg = `${cliName} failed: ${String(error.message || error)}${errorOutput ? `\n${errorOutput}` : ''}`;
    log('cli error', {
      cli: cliName,
      cmd: `${cliCommand} ${args.join(' ')}`,
      code: error.code || 'error',
      messageId: messageKey,
      errorDetails: errorOutput.substring(0, 200)
    });

    const err = new Error(msg);
    err.code = error.code || 'error';
    err.stderr = error.stderr;
    err.stdout = error.stdout;

    const isEarlyTerminated = isEarlyTerminationError(error, error.stderr, error.stdout);
    if (isEarlyTerminated) {
      err.shouldFallback = true;
      err.earlyTermination = true;
      log('Early session termination detected', {
        error: error.message,
        pattern: 'early termination indicator',
        sessionId: opencodeSessionId,
        messageId: messageKey
      });
    }

    if (message) {
      message.opencodeExitCode = err.code;
      message.opencodeSummary = errorOutput.slice(0, 800) || String(error.message || error).slice(0, 800);
      message.outputType = detectOutputType(errorOutput);
      message.partialUpdatedAt = new Date().toISOString();
      persistState().catch(() => { });
    }

    // Send error event to SSE clients
    if (messageKey && activeStreams.has(messageKey)) {
      const streams = activeStreams.get(messageKey);
      const data = JSON.stringify({
        type: 'error',
        error: msg,
        content: errorOutput || '', // Include error output as content for the client
        code: err.code,
        exitCode: err.code,
        outputType: message?.outputType,
        timestamp: new Date().toISOString()
      });
      streams.forEach(res => {
        try {
          res.write(`data: ${data}\n\n`);
          res.end();
        } catch (err) {
          log('SSE error event error', { err: String(err) });
        }
      });
      activeStreams.delete(messageKey);
    }

    throw err;
  }
}

function isEarlyTerminationError(error, stderr, stdout) {
  const errorOutput = (stderr || '').toLowerCase();
  const combinedOutput = `${errorOutput} ${(stdout || '').toLowerCase()}`;

  // Exclude warnings and info messages - these are not termination failures
  const isWarningOrInfo = /(warn|warning|info|notice)/i.test(errorOutput);
  if (isWarningOrInfo) return false;

  // Only trigger on specific error patterns that indicate actual early termination
  // These must be explicit errors, not warnings or informational messages
  const terminationPatterns = [
    /error:.*proper prefixing/i,
    /error:.*tool.*call.*format/i,
    /error:.*tool.?call.*prefix/i,
    /error:.*session terminated/i,
    /error:.*unexpected end/i,
    /error:.*premature exit/i,
    /error:.*incomplete output/i,
    /error:.*bracket prefix/i,
    /error:.*xml tag.*missing/i,
    /error:.*function call/i,
    /error:.*invalid tool call/i,
    /error:.*stream.*closed/i,
    /error:.*connection.*lost/i,
    /error:.*process.*exited/i
  ];

  return terminationPatterns.some(pattern => pattern.test(errorOutput));
}

function isSuccessfulCompletion(stderr, stdout, exitCode) {
  const errorOutput = (stderr || '').toLowerCase();
  const normalOutput = (stdout || '').toLowerCase();

  // If there's substantial output, check if it indicates successful completion
  if (normalOutput.length > 200) {
    // Check for success indicators in output
    const successIndicators = [
      /completed successfully/i,
      /done\.$/i,
      /finished/i,
      /output generated/i,
      /response complete/i,
      /task completed/i
    ];

    // Also check for absence of critical error patterns
    const criticalErrors = [
      /error:/i,
      /failed/i,
      /exception/i,
      /crashed/i,
      /terminated abnormally/i
    ];

    const hasSuccessIndicator = successIndicators.some(ind => ind.test(normalOutput));
    const hasCriticalError = criticalErrors.some(err => err.test(errorOutput) || err.test(normalOutput));

    return hasSuccessIndicator && !hasCriticalError;
  }

  return false;
}

function resetMessageStreamingFields(message) {
  if (!message) return;
  delete message.partialOutput;
  delete message.partialUpdatedAt;
  delete message.opencodeExitCode;
  delete message.opencodeSummary;
  delete message.outputType;
}

function resolveModelProviders(modelName) {
  const target = getAdminModelByIdOrName(modelName);
  if (target && Array.isArray(target.providers) && target.providers.length) {
    return target.providers.map((p, idx) => ({
      provider: normalizeProviderName(p.provider || p.name || 'opencode'),
      model: (p.model || p.name || modelName || '').trim() || modelName,
      primary: typeof p.primary === 'boolean' ? p.primary : idx === 0,
      cli: target.cli || 'opencode',
    }));
  }
  return [{
    provider: 'opencode',
    model: modelName,
    primary: true,
    cli: 'opencode',
  }];
}

function buildOpencodeAttemptChain(cli, preferredModel) {
  const chain = [];
  const seen = new Set();
  
  // Build chain from opencodeModels order
  // The order in the list determines fallback priority
  opencodeModels.forEach((m, idx) => {
    const modelToUse = idx === 0 && preferredModel ? preferredModel : m.name;
    const key = `${modelToUse}`;
    if (seen.has(key)) return;
    seen.add(key);
    chain.push({
      provider: 'opencode',
      model: modelToUse,
      primary: idx === 0,
      cli: normalizeCli(cli || 'opencode'),
      sourceModel: modelToUse,
    });
  });
  
  // If no models configured, fall back to default
  if (chain.length === 0) {
    chain.push({
      provider: 'opencode',
      model: preferredModel || 'default',
      primary: true,
      cli: normalizeCli(cli || 'opencode'),
      sourceModel: preferredModel || 'default',
    });
  }
  
  // Log the built chain for debugging
  log('Built model attempt chain', {
    cli,
    preferredModel: (typeof preferredModel === 'string' && preferredModel.trim()) ? preferredModel : '(none)',
    chainLength: chain.length,
    models: chain.map(c => c.model).slice(0, 5)
  });
  
  return chain;
}

function buildCliFallbackModels(cli, preferredModel) {
  const chain = [];
  const add = (name) => {
    const trimmed = (name || '').trim();
    if (trimmed && !chain.includes(trimmed)) chain.push(trimmed);
  };
  add(preferredModel);
  getConfiguredModels(cli).forEach((m) => add(m.name));
  add('default');
  return chain;
}

function classifyProviderError(error, provider) {
  const statusCode = error.statusCode || error.code;
  const errorMessage = (error.message || '').toLowerCase();
  
  const providerPatterns = {
    openai: {
      transient: [500, 502, 503, 504, 529],
      rateLimit: 429,
      auth: [401, 402],
      permission: 403,
      userError: [400],
      notFound: 404,
      timeout: 408
    },
    anthropic: {
      transient: [500, 529],
      rateLimit: 429,
      auth: 401,
      permission: 403,
      userError: [400, 413],
      notFound: 404
    },
    openrouter: {
      transient: [502, 503],
      rateLimit: 429,
      auth: [401, 402],
      permission: 403,
      userError: [400],
      timeout: 408,
      notFound: 404
    },
    chutes: {
      transient: [500, 502, 503],
      rateLimit: 429,
      auth: 401,
      permission: 403,
      userError: [400, 413],
      notFound: 404
    },
    nvidia: {
      transient: [500, 502, 503],
      rateLimit: 429,
      auth: 401,
      permission: 403,
      userError: [400],
      notFound: 404
    },
    together: {
      transient: [500, 502, 503],
      rateLimit: 429,
      auth: [401, 402],
      permission: 403,
      userError: [400],
      notFound: 404
    },
    fireworks: {
      transient: [500, 502, 503],
      rateLimit: 429,
      auth: 401,
      userError: [400],
      notFound: 404
    },
    mistral: {
      transient: [500, 502, 503],
      rateLimit: 429,
      auth: 401,
      permission: 403,
      userError: [400],
      notFound: 404
    },
    groq: {
      transient: [500, 502, 503],
      rateLimit: 429,
      auth: [401, 402],
      permission: 403,
      userError: [400, 413],
      notFound: 404
    },
    google: {
      transient: [500, 502, 503],
      rateLimit: 429,
      auth: 401,
      permission: 403,
      userError: [400, 413],
      notFound: 404
    },
    ollama: {
      transient: [500, 502, 503, 504, 529],
      rateLimit: 429,
      auth: 401,
      permission: 403,
      userError: [400],
      notFound: 404
    },
    default: {
      transient: [500, 502, 503, 529],
      rateLimit: 429,
      auth: [401, 402],
      permission: 403,
      userError: [400, 413],
      notFound: 404
    }
  };
  
  const patterns = providerPatterns[provider] || providerPatterns.default;
  
  if (error.isToolError) {
    return { category: 'toolError', action: 'return', waitTime: 0 };
  }
  
  if (patterns.transient?.includes(statusCode)) {
    return { category: 'transient', action: 'wait', waitTime: 30000 };
  }
  if (statusCode === patterns.rateLimit) {
    return { category: 'rateLimit', action: 'wait', waitTime: 30000 };
  }
  if (patterns.auth?.includes(statusCode)) {
    return { category: 'auth', action: 'switch', waitTime: 0 };
  }
  if (statusCode === patterns.permission) {
    return { category: 'permission', action: 'return', waitTime: 0 };
  }
  if (patterns.userError?.includes(statusCode)) {
    return { category: 'userError', action: 'return', waitTime: 0 };
  }
  if (statusCode === patterns.timeout) {
    return { category: 'timeout', action: 'wait', waitTime: 30000 };
  }
  if (statusCode === patterns.notFound) {
    return { category: 'notFound', action: 'wait', waitTime: 30000 };
  }
  
  if (statusCode >= 500) {
    return { category: 'serverError', action: 'wait', waitTime: 30000 };
  }
  
  if (errorMessage.includes('model not found') || errorMessage.includes('unknown model')) {
    return { category: 'modelNotFound', action: 'wait', waitTime: 30000 };
  }
  if (errorMessage.includes('insufficient credit') || errorMessage.includes('insufficient quota') || errorMessage.includes('payment required')) {
    return { category: 'billing', action: 'switch', waitTime: 0 };
  }
  if (errorMessage.includes('context length exceeded') || errorMessage.includes('token limit exceeded') || errorMessage.includes('request too large')) {
    return { category: 'userError', action: 'return', waitTime: 0 };
  }
  
  return { category: 'unknown', action: 'switch', waitTime: 0 };
}

function shouldFallbackCliError(err, message) {
  if (!err) return false;

  if (err.isToolError) {
    log('Tool error detected - no fallback needed', {
      error: err.message,
      toolError: true
    });
    return false;
  }

  // First, check if this was actually a successful completion despite an error being thrown
  // This can happen if the model completed but the process had a non-zero exit code
  if (message && message.partialOutput && message.partialOutput.length > 200) {
    // Check for success indicators in the output
    const successIndicators = [
      /completed successfully/i,
      /finished successfully/i,
      /done\.$/i,
      /task completed/i,
      /output generated/i,
      /response complete/i,
      /✔|✓|success/i
    ];

    const hasSuccessIndicator = successIndicators.some(ind => ind.test(message.partialOutput));
    const hasCriticalError = /error:|failed|exception|crashed/i.test(message.partialOutput);

    if (hasSuccessIndicator && !hasCriticalError) {
      log('Blocking fallback - model completed successfully', {
        messageId: message.id,
        partialOutputLength: message.partialOutput.length,
        successIndicator: true
      });
      return false;
    }
  }

  // Check if message has substantial output - if so, don't fallback even on errors
  // This prevents fallback when model was working fine but hit a minor issue
  if (message && message.partialOutput && message.partialOutput.length > 500) {
    log('Blocking fallback - message has substantial output', {
      messageId: message.id,
      partialOutputLength: message.partialOutput.length
    });
    return false;
  }

  // Only check stderr and error message for actual errors, not stdout which may contain informational logs
  const errorMessage = (err.message || '').toLowerCase();
  const stderr = (err.stderr || '').toLowerCase();
  const combinedOutput = `${errorMessage} ${stderr}`;

  // Exclude warnings and info messages - these are not failures requiring fallback
  const isWarningOrInfo = /(warn|warning|info|notice)/i.test(combinedOutput);
  if (isWarningOrInfo) {
    log('Blocking fallback - only warning/info present', {
      hasWarning: true,
      combinedOutput: combinedOutput.substring(0, 200)
    });
    return false;
  }

  // Use more specific patterns that indicate actual API/model failures requiring fallback
  // These require explicit "error:" prefix or very specific failure indicators
  const apiFailurePatterns = [
    // Model availability errors - these are clear failures requiring fallback
    /error:.*model not found/i,
    /error:.*unknown model/i,
    /error:.*invalid model/i,
    /error:.*unsupported model/i,
    // Authentication errors - clear API failures
    /error:.*api key/i,
    /error:.*unauthorized/i,
    /error:.*forbidden/i,
    // Quota/credit errors - clear service failures
    /error:.*insufficient credit/i,
    /error:.*insufficient quota/i,
    /error:.*no credit/i,
    /error:.*payment required/i,
    // Rate limiting - these require fallback
    /error:.*rate limit exceeded/i,
    /error:.*too many requests/i,
    /error:.*rate limited/gi,
    // Size limits - clear model constraints
    /error:.*request too large/i,
    /error:.*token limit exceeded/i,
    /error:.*context length exceeded/i,
    /error:.*maximum context length/i,
    /error:.*max tokens exceeded/i,
    /error:.*reduce your message size/i,
    // Connection and server errors
    /error:.*connection.*refused/i,
    /error:.*connection.*timeout/i,
    /error:.*server.*error/i,
    /error:.*internal server error/i,
    /error:.*service unavailable/i,
    /error:.*gateway.*timeout/i
  ];

  // Check if any API failure pattern is present in error output
  const hasApiFailure = apiFailurePatterns.some((pattern) => pattern.test(combinedOutput));

  if (hasApiFailure) {
    log('Allowing fallback - API failure detected', {
      pattern: 'api_failure',
      combinedOutput: combinedOutput.substring(0, 200)
    });
    return true;
  }

  // Additional check: if error mentions a provider with explicit error indicator
  // Only trigger for actual errors, not provider mentions in normal logging
  if (combinedOutput.includes('openrouter') ||
      combinedOutput.includes('mistral') ||
      combinedOutput.includes('nvidia') ||
      combinedOutput.includes('groq')) {

    // Require explicit error indicators for provider mentions
    const errorIndicators = [
      /^error:/i,
      / failed/i,
      / failure/i,
      / exception/i,
      / rejected/i,
      / denied/i
    ];

    const hasErrorIndicator = errorIndicators.some((ind) => ind.test(combinedOutput));

    if (hasErrorIndicator) {
      log('Allowing fallback - provider error detected', {
        provider: 'provider_mentioned',
        combinedOutput: combinedOutput.substring(0, 200)
      });
      return true;
    }
  }

  // Additional check: HTTP status codes that indicate actual failures
  const httpStatusPatterns = [
    /error.*401/i,
    /error.*402/i,
    /error.*404/i,
    /error.*429/i,
    /error.*5\d{2}/
  ];

  const hasHttpError = httpStatusPatterns.some((pattern) => pattern.test(combinedOutput));
  if (hasHttpError) {
    log('Allowing fallback - HTTP error status detected', {
      pattern: 'http_error',
      combinedOutput: combinedOutput.substring(0, 200)
    });
    return true;
  }

  log('Blocking fallback - no eligible error pattern detected', {
    combinedOutput: combinedOutput.substring(0, 200)
  });
  return false;
}

async function sendToOpencodeWithFallback({ session, model, content, message, cli, streamCallback, opencodeSessionId, plan }) {
  const cliName = normalizeCli(cli || session?.cli);
  const preferredModel = model || session?.model;
  const chain = buildOpencodeAttemptChain(cliName, preferredModel);
  const tried = new Set();
  const attempts = [];
  let lastError = null;
  let switchedToBackup = false;
  
  const continueAttempts = new Map();
  const MAX_CONTINUE_ATTEMPTS = 3;
  const CONTINUE_MESSAGE = '[CONTINUE] Please continue from where you left off.';
  const lastErrorTypes = new Map();

  log('Fallback sequence initiated', {
    sessionId: session?.id,
    messageId: message?.id,
    primaryModel: preferredModel,
    cliName,
    chainLength: chain.length,
    timestamp: new Date().toISOString()
  });

  const tryOption = async (option, isBackup = false) => {
    const key = `${option.provider}:${option.model}`;
    if (tried.has(key)) return null;
    tried.add(key);
    const limit = isProviderLimited(option.provider, option.model);
    if (limit.limited) {
      attempts.push({ 
        model: option.model, 
        provider: option.provider, 
        error: `limit: ${limit.reason}`,
        classification: 'rateLimit'
      });
      return null;
    }
    try {
      resetMessageStreamingFields(message);
      
      let messageContent = content;
      const modelKey = `${option.provider}:${option.model}`;
      const continueCount = continueAttempts.get(modelKey) || 0;
      
      if (continueCount > 0 && continueCount <= MAX_CONTINUE_ATTEMPTS) {
        messageContent = `${CONTINUE_MESSAGE}\n\n${content}`;
        log('Sending continue message', {
          model: option.model,
          provider: option.provider,
          attempt: continueCount,
          modelKey
        });
      }
      
      // When switching to backup model, preserve session and keep original content
      if (isBackup && !switchedToBackup && attempts.length > 0) {
        switchedToBackup = true;
        log('Switching to backup model with session continuity', {
          model: option.model,
          provider: option.provider,
          preservedSession: opencodeSessionId,
          attempts: attempts.length
        });
      }
      const result = await sendToOpencode({ session, model: option.model, content: messageContent, message, cli: cliName, streamCallback, opencodeSessionId });
      const normalizedResult = (result && typeof result === 'object') ? result : { reply: result };
      
      let tokensUsed = 0;
      let tokenSource = 'none';
      let tokenExtractionLog = [];
      
      // First try to use tokens from sendToOpencode result if available
      if (result && typeof result === 'object' && result.tokensUsed > 0) {
        tokensUsed = result.tokensUsed;
        tokenSource = result.tokenSource || 'result';
        tokenExtractionLog = result.tokenExtractionLog || [];
        log('✓ sendToOpencodeWithFallback: Using tokens from sendToOpencode result', { tokensUsed, tokenSource, messageId: message?.id });
      } else {
        // Fallback to extractTokenUsageFromResult
        tokensUsed = extractTokenUsageFromResult(normalizedResult, [messageContent], { allowEstimate: false });
        if (tokensUsed > 0) {
          tokenSource = 'response-extracted';
          tokenExtractionLog.push({ method: 'extractTokenUsageFromResult', success: true, value: tokensUsed });
          log('✓ sendToOpencodeWithFallback: Extracted tokens from result', { tokensUsed, messageId: message?.id });
        } else {
          tokenExtractionLog.push({ method: 'extractTokenUsageFromResult', success: false, reason: 'returned 0 tokens' });
        }
      }

      // Check if token usage was captured during streaming
      if (!tokensUsed && message.opencodeTokensUsed) {
        tokensUsed = message.opencodeTokensUsed;
        tokenSource = 'stream';
        tokenExtractionLog.push({ method: 'stream', success: true, value: tokensUsed, context: 'sendToOpencodeWithFallback' });
        log('✓ sendToOpencodeWithFallback: Using token usage captured during streaming', { tokensUsed, messageId: message?.id });
      }

      // Use estimation as last resort
      if (!tokensUsed) {
        const inputTokens = estimateTokensFromMessages([messageContent], '');
        const outputTokens = estimateTokensFromMessages([], normalizedResult.reply || '');
        tokensUsed = Math.max(inputTokens, outputTokens) + Math.ceil(Math.min(inputTokens, outputTokens) * 0.5);
        tokensUsed = Math.max(tokensUsed, 50);
        tokenSource = 'estimate-improved';
        
        const estimationDetails = {
          inputTokens,
          outputTokens,
          inputLength: (messageContent || '').length,
          outputLength: (normalizedResult.reply || '').length,
          calculatedTokens: Math.max(inputTokens, outputTokens) + Math.ceil(Math.min(inputTokens, outputTokens) * 0.5),
          finalTokens: tokensUsed,
          reason: 'All token extraction methods failed in sendToOpencodeWithFallback'
        };
        
        tokenExtractionLog.push({ method: 'estimation', success: true, value: tokensUsed, details: estimationDetails });
        
        log('⚠️  sendToOpencodeWithFallback: Using estimation', {
          messageId: message?.id,
          sessionId: session?.id,
          model: option.model,
          provider: option.provider,
          ...estimationDetails
        });
        
        console.warn('[TOKEN_TRACKING] ⚠️  ESTIMATION USED in sendToOpencodeWithFallback:', JSON.stringify({
          messageId: message?.id,
          sessionId: session?.id,
          model: option.model,
          provider: option.provider,
          estimatedTokens: tokensUsed,
          ...estimationDetails
        }, null, 2));
      }
      
      log('✅ Token usage determined in sendToOpencodeWithFallback', { 
        tokenSource, 
        tokensUsed, 
        sessionId: session?.id, 
        messageId: message?.id,
        extractionLog: tokenExtractionLog
      });
      
      recordProviderUsage(option.provider, option.model, tokensUsed, 1);
      
      // Reset counters on success
      continueAttempts.delete(modelKey);
      lastErrorTypes.delete(modelKey);
      
      if (attempts.length) {
        log('opencode succeeded after fallback', { attempts, model: option.model, provider: option.provider, cli: cliName, backup: isBackup });
      }
      
      return { 
        reply: normalizedResult.reply, 
        model: option.model, 
        attempts, 
        provider: option.provider, 
        raw: normalizedResult.raw, 
        tokensUsed,
        tokenSource,
        tokenExtractionLog
      };
    } catch (err) {
      lastError = err;
      
      const errorData = {
        model: option.model,
        provider: option.provider,
        error: err.message || String(err),
        code: err.code || null,
        timestamp: new Date().toISOString()
      };
      
      if (err.earlyTermination) {
        const partialOutputLength = (message?.partialOutput || '').length;
        const hasSubstantialOutput = partialOutputLength > 500;
        
        if (hasSubstantialOutput) {
          log('Blocking fallback - model has substantial output despite early termination', {
            model: option.model,
            provider: option.provider,
            error: err.message,
            partialOutputLength
          });
          return err;
        }
        
        const modelKey = `${option.provider}:${option.model}`;
        const currentCount = continueAttempts.get(modelKey) || 0;
        continueAttempts.set(modelKey, currentCount + 1);
        
        log('Early termination detected', {
          model: option.model,
          provider: option.provider,
          continueAttempt: currentCount + 1,
          maxAttempts: MAX_CONTINUE_ATTEMPTS
        });
        
        if (currentCount + 1 < MAX_CONTINUE_ATTEMPTS) {
          errorData.earlyTermination = true;
          errorData.continueAttempt = currentCount + 1;
          errorData.willContinue = true;
          attempts.push(errorData);
          
          tried.delete(key);
          return null;
        }
        
        log('Max continue attempts reached, switching model', {
          model: option.model,
          provider: option.provider,
          totalAttempts: MAX_CONTINUE_ATTEMPTS
        });
        
        attempts.push(errorData);
        return null;
      }
      
      const classification = classifyProviderError(err, option.provider);
      errorData.classification = classification.category;
      
      const modelKey = `${option.provider}:${option.model}`;
      const lastErrorType = lastErrorTypes.get(modelKey);
      
      if (lastErrorType === classification.category && 
          classification.category !== 'unknown') {
        log('Repeated error type detected', {
          model: option.model,
          provider: option.provider,
          errorType: classification.category
        });
        lastErrorTypes.set(modelKey, classification.category);
      }
      
      if (classification.action === 'return') {
        log('User/permission error - returning to user', {
          category: classification.category,
          model: option.model,
          provider: option.provider
        });
        err.willNotFallback = true;
        attempts.push(errorData);
        return err;
      }
      
      if (classification.action === 'wait') {
        log(`Provider error (${classification.category}) - waiting ${classification.waitTime}ms`, {
          model: option.model,
          provider: option.provider,
          category: classification.category,
          waitTime: classification.waitTime
        });
        
        errorData.willWait = true;
        errorData.waitTime = classification.waitTime;
        attempts.push(errorData);
        
        await new Promise(resolve => setTimeout(resolve, classification.waitTime));
        
        return null;
      }
      
      errorData.immediateSwitch = true;
      attempts.push(errorData);
      
      return null;
    }
  };

  for (const option of chain) {
    const result = await tryOption(option);
    if (result instanceof Error) break;
    if (result) return result;
  }

  const backupModel = (providerLimits.opencodeBackupModel || '').trim();
  if (backupModel) {
    const backupChain = buildOpencodeAttemptChain(cliName, backupModel);
    for (const option of backupChain) {
      const result = await tryOption(option, true);
      if (result instanceof Error) break;
      if (result) return result;
    }
  }

  const MAX_EARLY_TERMINATIONS = 2;
  const recentEarlyTerminations = attempts.filter(
    a => a.earlyTermination &&
    Date.now() - new Date(a.timestamp || 0).getTime() < 60000
  ).length;

  if (recentEarlyTerminations >= MAX_EARLY_TERMINATIONS) {
    log('Too many early terminations, giving up', {
      sessionId: session?.id,
      earlyTerminations: recentEarlyTerminations,
      attempts: attempts.length
    });
  }

  const err = new Error(`All ${cliName.toUpperCase()} models failed`);
  err.attempts = attempts;
  err.cause = lastError;
  throw err;
}

async function queueMessage(sessionId, message) { 
  traceMessageLifecycle('queued', sessionId, message); 
  const prev = sessionQueues.get(sessionId) || Promise.resolve(); 
  const next = prev.then(async () => {
    try {
      await processMessage(sessionId, message);
    } catch (error) {
      // Enhanced error handling to ensure resource cleanup
      log('Queue processing error', { sessionId, messageId: message.id, error: String(error) });
      
      // Try to find the session and message to mark as error if not already handled
      try {
        const session = getSession(sessionId);
        if (session) {
          const msg = session.messages.find(m => m.id === message.id);
          if (msg && msg.status !== 'error' && msg.status !== 'completed' && msg.status !== 'skipped') {
            msg.status = 'error';
            msg.error = `Queue processing failed: ${String(error)}`;
            msg.finishedAt = new Date().toISOString();
            session.updatedAt = msg.finishedAt;
            updatePending(sessionId, -1, session.userId);
            await persistState();
          }
        }
      } catch (cleanupError) {
        log('Failed to cleanup after queue error', { sessionId, error: String(cleanupError) });
      }
      
      // Re-throw to maintain the promise chain behavior
      throw error;
    }
  }); 
  sessionQueues.set(sessionId, next); 
  return next; 
}

function formatExternalTestingSummary(result) {
  if (!result) return '';
  if (result.skipped) {
    const limit = Number.isFinite(result.summary?.limit) ? result.summary.limit : 'unlimited';
    return `\n\n---\nExternal WP CLI Testing\nStatus: Skipped\nReason: ${result.reason || 'Not available'}\nUsage: ${result.summary?.used || 0} / ${limit}`;
  }
  if (!result.ok) {
    const errorText = Array.isArray(result.errors) && result.errors.length
      ? result.errors.join(' | ')
      : 'Unknown error';
    return `\n\n---\nExternal WP CLI Testing\nStatus: Failed\nErrors: ${errorText}`;
  }
  const cli = result.test_results?.cli_tests || { passed: 0, failed: 0 };
  const usage = result.usageSummary;
  const limit = Number.isFinite(usage?.limit) ? usage.limit : 'unlimited';
  return `\n\n---\nExternal WP CLI Testing\nStatus: ${cli.failed === 0 ? 'Passed' : 'Failed'}\nCLI Tests: ${cli.passed} passed, ${cli.failed} failed\nSubsite: ${result.subsite_url || 'n/a'}\nUsage: ${usage?.used || 0} / ${limit}`;
}

async function runExternalTestingForSession(session, message, plan) {
  if (!message?.externalTestingEnabled) return null;
  if (!session?.workspaceDir) return { ok: false, errors: ['Workspace directory not available'] };

  const user = findUserById(session.userId);
  const limitCheck = canUseExternalTesting(session.userId, plan, user?.unlimitedUsage === true);
  if (!limitCheck.allowed) {
    return { skipped: true, reason: 'External testing limit reached. Upgrade to continue.', summary: limitCheck.summary };
  }

  const pluginRoot = await resolvePluginRoot(session.workspaceDir);
  const spec = await loadExternalTestingSpec(session.workspaceDir);
  const testInput = {
    plugin_path: pluginRoot,
    plugin_slug: session.pluginSlug,
    test_mode: 'cli',
    required_plugins: spec?.required_plugins || [],
    test_scenarios: spec?.test_scenarios || [],
  };

  await recordExternalTestUsage(session.userId);
  trackFeatureUsage('external_wp_testing', session.userId, plan);

  const result = await externalWpTester.runTest(testInput, { configOverrides: {} });
  result.usageSummary = getExternalTestUsageSummary(session.userId, plan);
  return result;
}

async function processMessage(sessionId, message) {
  const session = getSession(sessionId);
  if (!session) return;

  // Track message processing start
  const startTime = Date.now();
  const userPlan = resolveUserPlan(session.userId);

  if (message.isContinuation && message.originalMessageId) {
    const originalMessage = session.messages?.find(m => m.id === message.originalMessageId);
    
    // Ensure we always preserve the session for continuations
    // Priority order: originalMessage.opencodeSessionId > session.opencodeSessionId > session.initialOpencodeSessionId
    if (originalMessage && originalMessage.opencodeSessionId) {
      message.opencodeSessionId = originalMessage.opencodeSessionId;
    } else if (session.opencodeSessionId) {
      message.opencodeSessionId = session.opencodeSessionId;
    } else if (session.initialOpencodeSessionId) {
      message.opencodeSessionId = session.initialOpencodeSessionId;
    }
    
    log('Processing continuation message', {
      sessionId,
      originalMessageId: message.originalMessageId,
      newMessageId: message.id,
      model: message.model,
      preservedSessionId: message.opencodeSessionId,
      initialSessionId: session.initialOpencodeSessionId,
      sessionOpencodeSessionId: session.opencodeSessionId,
      originalMessageSessionId: originalMessage?.opencodeSessionId
    });
  }

  message.status = 'running';
  message.startedAt = new Date().toISOString();
  updatePending(sessionId, 0, session.userId);
  await persistState();
  let releaseResources = null;
  try {
    traceMessageLifecycle('processing', sessionId, message);
    await ensureSessionPaths(session);
    const sessionPlan = resolveUserPlan(session.userId);
    await applyPlanPriorityDelay(sessionPlan);
    
    // Wait for resources - this will wait indefinitely until resources are available
    // Messages are never skipped, they stay in queue until they can be processed
    releaseResources = await waitForResources(message.id);
    
    const activeCli = normalizeCli(message.cli || session.cli);

    // Track model usage
    const modelUsed = message.model || session.model || 'default';
    trackModelUsage(modelUsed, session.userId, userPlan);
    trackFeatureUsage('ai_chat', session.userId, userPlan);

    // Image attachments: append image URLs to message content
    // Images are sent to the opencode session along with the message
    const imageAttachments = Array.isArray(message.attachments)
      ? message.attachments.filter((a) => a && isImageMime(a.type) && a.url)
      : [];
    if (imageAttachments.length) {
      const imageTags = imageAttachments.map(a => `@${a.name}`).join(' ');
      message.content = `${imageTags}\n\n${message.content}`;
    }

    // Ensure opencode session exists before processing
    // CRITICAL FIX: Use message.opencodeSessionId if explicitly provided in request
    // This ensures session continuity for continuations and retries
    let opencodeSessionId;
    if (message.opencodeSessionId) {
      log('Using explicit opencodeSessionId from message', {
        sessionId,
        messageOpencodeSessionId: message.opencodeSessionId,
        sessionOpencodeSessionId: session.opencodeSessionId
      });
      opencodeSessionId = message.opencodeSessionId;
      // Update session to use this session ID
      if (session.opencodeSessionId !== opencodeSessionId) {
        session.opencodeSessionId = opencodeSessionId;
        // Only set initialOpencodeSessionId if not already set to preserve session continuity
        if (!session.initialOpencodeSessionId) {
          session.initialOpencodeSessionId = opencodeSessionId;
        }
        await persistState();
      }
    } else {
      log('Ensuring opencode session (no explicit session ID in message)', {
        sessionId,
        activeCli,
        model: message.model,
        isProceedWithBuild: message.isProceedWithBuild
      });
      opencodeSessionId = await ensureOpencodeSession(session, message.model);
      if (opencodeSessionId && session.opencodeSessionId !== opencodeSessionId) {
        session.opencodeSessionId = opencodeSessionId;
        await persistState();
      }
    }
    // We allow null opencodeSessionId, which means we'll let the CLI create one
    log('opencode session ensured (or pending)', { sessionId, opencodeSessionId, model: message.model, workspaceDir: session.workspaceDir });

    const opencodeResult = await sendToOpencodeWithFallback({ session, model: message.model, content: message.content, message, cli: activeCli, opencodeSessionId, plan: sessionPlan });
    let reply = opencodeResult.reply;
    if (opencodeResult.model) {
      message.model = opencodeResult.model;
    }
    if (Array.isArray(opencodeResult.attempts) && opencodeResult.attempts.length) {
      message.failoverAttempts = opencodeResult.attempts;
    }
    
    // Track AI response time
    const responseTime = Date.now() - startTime;
    const provider = opencodeResult.provider || 'opencode';
    trackAIResponseTime(responseTime, provider, true, null);
    
    // Calculate tokens: prefer OpenCode-reported usage, then stream capture, then real session usage, then estimation
    let tokensUsed = 0;
    let tokenSource = 'none';
    let tokenExtractionLog = [];

    // First check if we got tokens from the result
    if (typeof opencodeResult.tokensUsed === 'number' && opencodeResult.tokensUsed > 0) {
      tokensUsed = opencodeResult.tokensUsed;
      tokenSource = opencodeResult.tokenSource || 'result';
      tokenExtractionLog = opencodeResult.tokenExtractionLog || [];
      log('✓ processMessage: Using tokens from opencodeResult', { tokensUsed, tokenSource, messageId: message.id });
    }

    // Check if token usage was captured during streaming (fallback if result didn't have it)
    if (!tokensUsed && message.opencodeTokensUsed) {
      tokensUsed = message.opencodeTokensUsed;
      tokenSource = 'stream';
      tokenExtractionLog.push({ method: 'stream', success: true, value: tokensUsed, context: 'processMessage fallback' });
      log('✓ processMessage: Using token usage captured during streaming', { tokensUsed, messageId: message.id });
    }

    // If still no tokens, use estimation with detailed logging
    if (!tokensUsed) {
      const inputTokens = estimateTokensFromMessages([message.content], '');
      const outputTokens = estimateTokensFromMessages([], reply || '');
      // Use a more accurate ratio for AI-generated code (typically ~3-4 chars per token for code)
      tokensUsed = Math.max(inputTokens, outputTokens) + Math.ceil(Math.min(inputTokens, outputTokens) * 0.5);
      // Ensure minimum token count for any AI interaction
      tokensUsed = Math.max(tokensUsed, 50);
      tokenSource = 'estimate-improved';

      const estimationDetails = {
        inputTokens,
        outputTokens,
        inputLength: (message.content || '').length,
        outputLength: (reply || '').length,
        calculatedTokens: Math.max(inputTokens, outputTokens) + Math.ceil(Math.min(inputTokens, outputTokens) * 0.5),
        finalTokens: tokensUsed,
        reason: 'All token extraction methods failed'
      };

      tokenExtractionLog.push({
        method: 'estimation',
        success: true,
        value: tokensUsed,
        details: estimationDetails,
        context: 'processMessage fallback'
      });

      log('⚠️  processMessage: Using estimation because all extraction methods failed', {
        messageId: message.id,
        sessionId: session.id,
        ...estimationDetails
      });

      console.warn('[TOKEN_TRACKING] ⚠️  ESTIMATION USED:', JSON.stringify({
        messageId: message.id,
        sessionId: session.id,
        model: message.model,
        provider,
        estimatedTokens: tokensUsed,
        ...estimationDetails
      }, null, 2));

      // Mark message as having token extraction failure for client-side error detection
      // This allows the client to detect server-side errors even when status is 'done'
      message.tokenExtractionFailed = true;
      message.tokenSource = tokenSource;
    }

      log('✅ Token usage determined for processMessage', {
        tokenSource,
        tokensUsed,
        sessionId,
        messageId: message.id,
        extractionLog: tokenExtractionLog
      });

      console.log(`[USAGE] processMessage: recording tokens user=${session.userId} tokens=${tokensUsed} model=${message.model} source=${tokenSource}`);

      if (session.userId) {
        await recordUserTokens(session.userId, tokensUsed);
      } else {
        console.error('[USAGE] ERROR: Cannot record tokens in processMessage because session.userId is missing', { sessionId, messageId: message.id });
      }

      // Check if message appears incomplete due to token extraction failure
      // If token extraction failed and output is suspiciously short, mark as incomplete
      const outputLength = (reply || '').length;
      const isSuspiciouslyShort = outputLength < 100 && tokenSource === 'estimate-improved';
      const isTokenExtractionFailed = tokenSource === 'estimate-improved';

      if (isTokenExtractionFailed && isSuspiciouslyShort && message.cli === 'opencode') {
        console.warn('[COMPLETION_DETECTION] Message appears incomplete due to token extraction failure', {
          messageId: message.id,
          sessionId: session.id,
          outputLength,
          tokenSource,
          isSuspiciouslyShort
        });
        // Mark message as potentially incomplete for client-side detection
        message.potentiallyIncomplete = true;
      }

      if (message.isProceedWithBuild && message.externalTestingEnabled) {
        try {
          const testingResult = await runExternalTestingForSession(session, message, sessionPlan);
          message.externalTesting = testingResult;
          reply = `${reply || ''}${formatExternalTestingSummary(testingResult)}`.trim();
        } catch (testErr) {
          message.externalTesting = { ok: false, errors: [testErr.message || String(testErr)] };
          reply = `${reply || ''}\n\n---\nExternal WP CLI Testing\nStatus: Failed\nErrors: ${testErr.message || String(testErr)}`.trim();
        }
      }

      message.status = 'done';
      message.reply = reply;
      if (message.todos && Array.isArray(message.todos) && message.todos.length) {
        message.todos = [];
      }
    message.finishedAt = new Date().toISOString();
    session.updatedAt = message.finishedAt;
    session.cli = activeCli;
    session.model = message.model; // Update session's active model after any fallback
    await persistState(); // Persist the updated model
    traceMessageLifecycle('done', sessionId, message);
  } catch (error) {
    // Provide helpful and parseable error details in the message
    message.status = 'error';
    if (message.todos && Array.isArray(message.todos) && message.todos.length) {
      message.todos = [];
    }
    const details = [];
    if (error.code) details.push(`code: ${error.code}`);
    if (error.stderr) details.push(`stderr: ${error.stderr.trim()}`);
    if (error.stdout) details.push(`stdout: ${error.stdout.trim()}`);
    if (Array.isArray(error.attempts) && error.attempts.length) {
      const attemptSummary = error.attempts.map((a) => `${a.model || 'unknown'}: ${a.error || 'error'}`).join(' | ');
      details.push(`attempts: ${attemptSummary}`);
      const lastAttempt = error.attempts[error.attempts.length - 1];
      if (lastAttempt?.model) message.model = lastAttempt.model;
    }
    const messageText = (error.message && error.message.length) ? String(error.message) : `${String(error)}`;
    message.error = `${messageText}${details.length ? ` -- ${details.join(' | ')}` : ''}`;
    
    // Track AI errors
    const errorType = error.code || 'processing_error';
    trackUserSession(session.userId, 'error', { 
      errorType: errorType,
      sessionId: sessionId,
      messageId: message.id 
    });
    trackAIResponseTime(Date.now() - startTime, 'opencode', false, errorType);
    
    log('message processing failed', { sessionId, messageId: message.id, error: message.error });
    message.finishedAt = new Date().toISOString();
    session.updatedAt = message.finishedAt;
  } finally {
    releaseResources?.();
    updatePending(sessionId, -1, session.userId);
    
    // Clean up any active streams for this message
    if (activeStreams.has(message.id)) {
      const streams = activeStreams.get(message.id);
      if (streams instanceof Set) {
        for (const stream of streams) {
          try {
            const finalData = JSON.stringify({
              type: message.status === 'error' ? 'error' : 'complete',
              content: message.reply || message.partialOutput || '',
              error: message.error,
              outputType: message.outputType,
              exitCode: message.opencodeExitCode,
              timestamp: message.finishedAt || new Date().toISOString()
            });
            stream.write(`data: ${finalData}\n\n`);
            stream.end();
          } catch (_) {}
        }
      }
      activeStreams.delete(message.id);
    }
    
    // Clean up process tracking
    if (runningProcesses.has(message.id)) {
      runningProcesses.delete(message.id);
    }
    
    // Trigger memory cleanup after processing completes
    const processTime = Date.now() - startTime;
    if (processTime > 60000) { // If processing took > 1 minute, trigger cleanup
      triggerMemoryCleanup('long_process_complete');
    }
    
    await persistState();
  }
}

function getConfiguredModels(cliParam = 'opencode') {
  const cli = normalizeCli(cliParam || 'opencode');
  // Return public models for user-facing dropdown selection
  // These are the models displayed to users in the builder
  const mapped = publicModels.map((m, idx) => ({
    id: m.id,
    name: m.name,
    label: m.label || m.name,
    icon: m.icon || '',
    cli: cli,
    providers: [],
    primaryProvider: 'opencode',
    tier: m.tier || 'free',
    multiplier: getTierMultiplier(m.tier || 'free'),
    supportsMedia: m.supportsMedia ?? false,
  }));
  return mapped;
}

function getOpencodeModelsForExecution() {
  // Return opencode models for internal fallback chain during execution
  // These models are used when rate limits are reached or errors occur
  return opencodeModels.map((m) => ({
    id: m.id,
    name: m.name,
    label: m.label || m.name,
    icon: m.icon || '',
    tier: m.tier || 'free',
    multiplier: getTierMultiplier(m.tier || 'free'),
    supportsMedia: m.supportsMedia ?? false,
  }));
}

async function handleModels(_req, res, cliParam = null) {
  try {
    const models = getConfiguredModels(cliParam || 'opencode');
    sendJson(res, 200, { models, empty: models.length === 0 });
  } catch (error) {
    sendJson(res, 500, { error: error.message || 'Failed to load models' });
  }
}

async function handleUserLogin(req, res) {
  try {
    const body = await parseJsonBody(req);
    const email = (body.email || body.username || body.user || '').trim().toLowerCase();
    const password = (body.password || body.pass || '').trim();
    const remember = body.remember === true || body.remember === 'true';
    const clientIp = req.socket?.remoteAddress || 'unknown';

    // Check honeypot
    if (checkHoneypot(body)) {
      log('user login honeypot triggered', { ip: clientIp });
      return sendJson(res, 400, { error: 'Invalid request' });
    }

    if (!email || !password) {
      return sendJson(res, 400, { error: 'Email and password are required' });
    }

    // Check rate limit
    const rateLimitKey = `${email}:${clientIp}`;
    const rateLimit = checkLoginRateLimit(rateLimitKey, USER_LOGIN_RATE_LIMIT, loginAttempts);
    if (rateLimit.blocked) {
      log('user login rate limited', { email, ip: clientIp, retryAfter: rateLimit.retryAfter });
      return sendJson(res, 429, {
        error: 'Too many login attempts. Please try again later.',
        retryAfter: rateLimit.retryAfter || 60
      });
    }

    const user = await verifyUserPassword(email, password);
    if (!user) {
      log('failed user login attempt', { email, ip: clientIp, reason: 'invalid_credentials' });
      return sendJson(res, 401, { error: 'Incorrect credentials' });
    }

    // Check account lockout
    if (user.lockedUntil && user.lockedUntil > Date.now()) {
      log('login attempt on locked account', { email, ip: clientIp, lockedUntil: user.lockedUntil });
      return sendJson(res, 429, {
        error: 'Account temporarily locked due to too many failed attempts.',
        retryAfter: Math.ceil((user.lockedUntil - Date.now()) / 1000)
      });
    }

    if (!user.emailVerified) {
      return sendJson(res, 403, { error: 'Please verify your email address before signing in.' });
    }

    // Clear failed attempts on success
    loginAttempts.delete(rateLimitKey);

    // Reset failed logins on successful login
    user.failedLogins = 0;
    user.lockedUntil = null;
    await persistUsersDb();

    const token = startUserSession(res, user.id, remember);
    const ttl = remember ? USER_SESSION_TTL_MS : USER_SESSION_SHORT_TTL_MS;
    const expiresAt = Date.now() + ttl;

    // Track user login
    trackUserSession(user.id, 'login', { plan: user.plan });
    trackConversionFunnel('signup_to_login', 'login', user.id, { plan: user.plan });

    log('successful user login', { userId: user.id, email: user.email, remember });
    sendJson(res, 200, {
      ok: true,
      user: { id: user.id, email: user.email, plan: user.plan },
      token,
      expiresAt
    });
  } catch (error) {
    sendJson(res, 400, { error: error.message || 'Unable to login' });
  }
}

async function subscribeToEmailMarketing(email) {
  try {
    await fetch('https://emailmarketing.modelrailway3d.co.uk/api/webhooks/incoming/wh_0Z49zi_DGj4-lKJMOPO8-g', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        email,
        source: 'plugin_compass_signup',
        timestamp: new Date().toISOString()
      })
    });
    log('email marketing subscription added', { email });
  } catch (error) {
    log('email marketing subscription failed', { email, error: error.message });
  }
}

async function handleUserRegister(req, res) {
  try {
    const body = await parseJsonBody(req);
    const email = (body.email || body.username || body.user || '').trim().toLowerCase();
    const password = (body.password || body.pass || '').trim();
    const clientIp = req.socket?.remoteAddress || 'unknown';

    // Check honeypot
    if (checkHoneypot(body)) {
      log('user registration honeypot triggered', { ip: clientIp });
      return sendJson(res, 400, { error: 'Invalid request' });
    }

    if (!email || !password) {
      return sendJson(res, 400, { error: 'Email and password are required' });
    }

    // Basic email validation
    if (!EMAIL_REGEX.test(email)) {
      return sendJson(res, 400, { error: 'Invalid email format' });
    }

    // Enhanced password strength validation
    const passwordValidation = validatePassword(password);
    if (!passwordValidation.valid) {
      return sendJson(res, 400, {
        error: 'Password does not meet requirements',
        requirements: passwordValidation.errors
      });
    }

    const affiliateCode = sanitizeAffiliateCode(body.affiliateCode || body.ref || readAffiliateReferralCode(req));
    const trackedAffiliate = affiliateCode && findAffiliateByCode(affiliateCode) ? affiliateCode : '';
    if (trackedAffiliate) {
      setAffiliateReferralCookie(res, trackedAffiliate);
    }

    const user = await createUser(email, password, { referredByAffiliateCode: trackedAffiliate });

    subscribeToEmailMarketing(user.email).catch(err => {
      log('background email marketing subscription failed', { userId: user.id, email: user.email });
    });

    // Track signup conversion and start conversion funnel
    trackConversion('signup', req);
    trackConversionFunnel('signup_process', 'signup_completed', user.id, { 
      source: body.source || 'direct',
      has_affiliate: !!trackedAffiliate 
    });

    // Send verification email in the background to avoid holding up the response
    sendVerificationEmail(user, resolveBaseUrl(req)).catch(err => {
      log('background verification email failed', { userId: user.id, email: user.email });
    });

    log('user registered successfully', { userId: user.id, email: user.email });
    sendJson(res, 200, {
      ok: true,
      user: { id: user.id, email: user.email, emailVerified: user.emailVerified },
      verificationRequired: true,
      message: 'Please check your email to verify your account.'
    });
  } catch (error) {
    if (error.message === 'User already exists with this email') {
      sendJson(res, 409, { error: error.message });
    } else {
      sendJson(res, 400, { error: error.message || 'Unable to register' });
    }
  }
}

async function handleAffiliateSignup(req, res) {
  try {
    const body = await parseJsonBody(req);
    const email = (body.email || body.username || '').trim().toLowerCase();
    const password = (body.password || '').trim();
    const name = (body.name || body.fullName || '').trim();
    if (!EMAIL_REGEX.test(email)) return sendJson(res, 400, { error: 'Email is invalid' });
    if (!password || password.length < 6) return sendJson(res, 400, { error: 'Password must be at least 6 characters long' });
    const affiliate = await registerAffiliate({ email, password, name });
    sendAffiliateVerificationEmail(affiliate, resolveBaseUrl(req)).catch(err => {
      log('background affiliate verification email failed', { affiliateId: affiliate.id, email: affiliate.email });
    });
    return sendJson(res, 201, { ok: true, verificationRequired: true, message: 'Please check your email to verify your account.' });
  } catch (error) {
    if (error.message && error.message.includes('already exists')) {
      return sendJson(res, 409, { error: error.message });
    }
    return sendJson(res, 400, { error: error.message || 'Unable to create affiliate' });
  }
}

async function handleAffiliateLogin(req, res) {
  try {
    const body = await parseJsonBody(req);
    const email = (body.email || '').trim().toLowerCase();
    const password = (body.password || '').trim();
    if (!email || !password) return sendJson(res, 400, { error: 'Email and password are required' });
    const affiliate = await verifyAffiliatePassword(email, password);
    if (!affiliate) return sendJson(res, 401, { error: 'Incorrect credentials' });
    if (!affiliate.emailVerified) {
      return sendJson(res, 403, { error: 'Please verify your email before logging in.', verificationRequired: true });
    }
    const token = startAffiliateSession(res, affiliate.id);
    return sendJson(res, 200, { ok: true, affiliate: summarizeAffiliate(affiliate), token });
  } catch (error) {
    return sendJson(res, 400, { error: error.message || 'Unable to login' });
  }
}

async function handleAffiliateVerifyEmailApi(req, res, url) {
  try {
    const tokenFromQuery = (url && url.searchParams && url.searchParams.get('token')) || '';
    const body = req.method === 'POST' ? await parseJsonBody(req).catch(() => ({})) : {};
    const token = (body.token || tokenFromQuery || '').trim();
    if (!token) return sendJson(res, 400, { error: 'Verification token is required' });

    const affiliate = affiliatesDb.find((a) => a.verificationToken === token);
    if (!affiliate) return sendJson(res, 400, { error: 'Verification link is invalid' });

    if (affiliate.verificationExpiresAt) {
      const expires = new Date(affiliate.verificationExpiresAt).getTime();
      if (Number.isFinite(expires) && expires < Date.now()) {
        return sendJson(res, 400, { error: 'Verification link has expired. Please request a new one.' });
      }
    }

    affiliate.emailVerified = true;
    affiliate.verificationToken = '';
    affiliate.verificationExpiresAt = null;
    await persistAffiliatesDb();
    const tokenValue = startAffiliateSession(res, affiliate.id);
    log('affiliate email verified', { affiliateId: affiliate.id, email: affiliate.email });

    sendJson(res, 200, {
      ok: true,
      affiliate: summarizeAffiliate(affiliate),
      token: tokenValue,
      expiresAt: Date.now() + AFFILIATE_SESSION_TTL_MS,
      message: 'Email verified successfully.',
      redirect: '/affiliate-dashboard',
    });
  } catch (error) {
    sendJson(res, 400, { error: error.message || 'Unable to verify email' });
  }
}

async function handleAffiliateLogout(_req, res) {
  clearAffiliateSession(res);
  sendJson(res, 200, { ok: true });
}

async function handleAffiliateMe(req, res, url) {
  const auth = requireAffiliateAuth(req, res);
  if (!auth) return;
  const affiliate = auth.affiliate;
  const summary = summarizeAffiliate(affiliate);
  const baseUrl = resolveBaseUrl(req, url);
  const firstLink = (summary.trackingLinks || [])[0];
  const firstCode = firstLink?.code || '';
  const firstPath = firstLink?.targetPath || '/pricing';
  const sampleLink = firstCode ? `${baseUrl}${firstPath}${firstPath.includes('?') ? '&' : '?'}aff=${firstCode}` : `${baseUrl}/pricing`;
  sendJson(res, 200, { ok: true, affiliate: summary, sampleLink });
}

async function handleAffiliateTransactions(req, res) {
  const auth = requireAffiliateAuth(req, res);
  if (!auth) return;
  const affiliate = auth.affiliate;
  const records = Array.isArray(affiliate.earnings) ? affiliate.earnings : [];
  // Sort by date descending
  const sorted = [...records].sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
  sendJson(res, 200, { ok: true, transactions: sorted });
}

async function handleInvoicesList(req, res) {
  const session = requireUserAuth(req, res);
  if (!session) return;
  const user = findUserById(session.userId);
  if (!user) return sendJson(res, 404, { error: 'User not found' });

  const invoices = getInvoicesByUserId(user.id);
  sendJson(res, 200, { ok: true, invoices });
}

async function handleInvoiceDownload(req, res, url, invoiceId) {
  const session = requireUserAuth(req, res);
  if (!session) return;
  const user = findUserById(session.userId);
  if (!user) return sendJson(res, 404, { error: 'User not found' });

  const invoice = invoicesDb.find(inv => inv.id === invoiceId);
  if (!invoice) return sendJson(res, 404, { error: 'Invoice not found' });
  if (invoice.userId !== user.id) return sendJson(res, 403, { error: 'Access denied' });

  const pdfPath = path.join(INVOICES_DIR, `${invoiceId}.pdf`);
  try {
    await fs.access(pdfPath);
    const fileStream = fsSync.createReadStream(pdfPath);
    res.setHeader('Content-Type', 'application/pdf');
    res.setHeader('Content-Disposition', `attachment; filename="${invoice.invoiceNumber}.pdf"`);
    fileStream.pipe(res);
  } catch (error) {
    log('Failed to read invoice PDF', { invoiceId, error: String(error) });
    return sendJson(res, 500, { error: 'Failed to download invoice' });
  }
}

async function handleAffiliateCreateLink(req, res) {
  const auth = requireAffiliateAuth(req, res);
  if (!auth) return;
  const affiliate = auth.affiliate;
  const body = await parseJsonBody(req).catch(() => ({}));
  const label = (body.label || body.name || 'New link').toString().trim() || 'New link';
  let targetPath = (body.targetPath || body.path || '/pricing').toString().trim();
  if (!targetPath.startsWith('/')) targetPath = '/' + targetPath;
  
  const code = generateTrackingCode(label);
  affiliate.codes = Array.isArray(affiliate.codes) ? affiliate.codes : [];
  affiliate.codes.push({ code, label, targetPath, createdAt: new Date().toISOString() });
  await persistAffiliatesDb();
  sendJson(res, 201, { ok: true, link: { code, label, targetPath }, links: affiliate.codes });
}

async function handleAffiliateCreateWithdrawal(req, res) {
  const auth = requireAffiliateAuth(req, res);
  if (!auth) return;
  const affiliate = auth.affiliate;
  const body = await parseJsonBody(req).catch(() => ({}));

  const paypalEmail = (body.paypalEmail || '').trim().toLowerCase();
  const currency = (body.currency || 'USD').toUpperCase();
  const amount = Number(body.amount || 0);

  if (!paypalEmail || !paypalEmail.includes('@')) {
    return sendJson(res, 400, { error: 'PayPal email is required' });
  }
  if (!['USD', 'EUR', 'GBP', 'CAD', 'AUD', 'JPY', 'CHF'].includes(currency)) {
    return sendJson(res, 400, { error: 'Invalid currency' });
  }
  if (amount <= 0) {
    return sendJson(res, 400, { error: 'Amount must be greater than 0' });
  }

  const earnings = Array.isArray(affiliate.earnings) ? affiliate.earnings : [];
  const totalEarnings = earnings.reduce((sum, e) => sum + Number(e.amount || 0), 0);
  if (amount > totalEarnings) {
    return sendJson(res, 400, { error: 'Amount exceeds available balance' });
  }

  const withdrawal = {
    id: randomUUID(),
    affiliateId: affiliate.id,
    affiliateEmail: affiliate.email,
    paypalEmail,
    currency,
    amount,
    status: 'pending',
    createdAt: new Date().toISOString(),
    processedAt: null,
  };

  withdrawalsDb.push(withdrawal);
  await persistWithdrawalsDb();
  sendJson(res, 201, { ok: true, withdrawal });
}

async function handleAdminWithdrawalsList(req, res) {
  const session = requireAdminAuth(req, res);
  if (!session) return;

  const sorted = [...withdrawalsDb].sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
  sendJson(res, 200, { withdrawals: sorted });
}

async function handleAdminWithdrawalUpdate(req, res) {
  const session = requireAdminAuth(req, res);
  if (!session) return;

  const body = await parseJsonBody(req).catch(() => ({}));
  const withdrawalId = body.withdrawalId;
  const newStatus = body.status;

  if (!withdrawalId) {
    return sendJson(res, 400, { error: 'Withdrawal ID is required' });
  }
  if (!['pending', 'done'].includes(newStatus)) {
    return sendJson(res, 400, { error: 'Invalid status' });
  }

  const withdrawal = withdrawalsDb.find(w => w.id === withdrawalId);
  if (!withdrawal) {
    return sendJson(res, 404, { error: 'Withdrawal not found' });
  }

  withdrawal.status = newStatus;
  if (newStatus === 'done') {
    withdrawal.processedAt = new Date().toISOString();
  }

  await persistWithdrawalsDb();
  sendJson(res, 200, { ok: true, withdrawal });
}

async function handleFeatureRequestsList(req, res) {
  const session = getUserSession(req);
  const userId = session?.userId || '';

  const sorted = [...featureRequestsDb].sort((a, b) => {
    if (b.votes !== a.votes) return b.votes - a.votes;
    return new Date(b.createdAt) - new Date(a.createdAt);
  });

  const result = sorted.map(fr => ({
    id: fr.id,
    title: fr.title,
    description: fr.description,
    votes: fr.votes,
    createdAt: fr.createdAt,
    status: fr.status || 'pending',
    adminReply: fr.adminReply || '',
    hasVoted: userId ? fr.upvoters.includes(userId) : false,
  }));

  sendJson(res, 200, { featureRequests: result });
}

async function handleFeatureRequestCreate(req, res) {
  const session = requireUserAuth(req, res);
  if (!session) return;

  const user = findUserById(session.userId);
  if (!user) return sendJson(res, 404, { error: 'User not found' });

  try {
    const body = await parseJsonBody(req);
    const title = (body.title || '').toString().trim();
    const description = (body.description || '').toString().trim();

    if (!title || title.length < 3) {
      return sendJson(res, 400, { error: 'Title must be at least 3 characters' });
    }
    if (!description || description.length < 10) {
      return sendJson(res, 400, { error: 'Description must be at least 10 characters' });
    }

    const featureRequest = {
      id: randomUUID(),
      title,
      description,
      votes: 1,
      upvoters: [session.userId],
      authorEmail: user.email || '',
      authorId: session.userId,
      status: 'pending',
      adminReply: '',
      createdAt: new Date().toISOString(),
      updatedAt: new Date().toISOString(),
    };

    featureRequestsDb.push(featureRequest);
    await persistFeatureRequestsDb();

    log('Feature request created', { id: featureRequest.id, title, userId: session.userId });
    sendJson(res, 201, {
      ok: true,
      featureRequest: {
        id: featureRequest.id,
        title: featureRequest.title,
        description: featureRequest.description,
        votes: featureRequest.votes,
        createdAt: featureRequest.createdAt,
        status: featureRequest.status,
        adminReply: featureRequest.adminReply,
        hasVoted: true,
      }
    });
  } catch (error) {
    sendJson(res, 400, { error: error.message || 'Unable to create feature request' });
  }
}

async function handleFeatureRequestUpvote(req, res, id) {
  const session = requireUserAuth(req, res);
  if (!session) return;

  const featureRequest = featureRequestsDb.find(fr => fr.id === id);
  if (!featureRequest) return sendJson(res, 404, { error: 'Feature request not found' });

  const userId = session.userId;
  const hasUpvoted = featureRequest.upvoters.includes(userId);

  if (hasUpvoted) {
    featureRequest.upvoters = featureRequest.upvoters.filter(uid => uid !== userId);
    featureRequest.votes = Math.max(0, featureRequest.votes - 1);
  } else {
    featureRequest.upvoters.push(userId);
    featureRequest.votes = (featureRequest.votes || 0) + 1;
  }

  await persistFeatureRequestsDb();

  sendJson(res, 200, {
    ok: true,
    votes: featureRequest.votes,
    hasVoted: !hasUpvoted,
  });
}

async function handleAdminFeatureRequestsList(req, res) {
  const session = getAdminSession(req);
  if (!session) {
    return sendJson(res, 403, { error: 'Admin access required' });
  }

  const sorted = [...featureRequestsDb].sort((a, b) => {
    if (b.votes !== a.votes) return b.votes - a.votes;
    return new Date(b.createdAt) - new Date(a.createdAt);
  });

  const result = sorted.map(fr => ({
    id: fr.id,
    title: fr.title,
    description: fr.description,
    votes: fr.votes,
    createdAt: fr.createdAt,
    updatedAt: fr.updatedAt,
    authorEmail: fr.authorEmail,
    authorId: fr.authorId,
    status: fr.status || 'pending',
    adminReply: fr.adminReply || '',
    upvoters: fr.upvoters || [],
  }));

  sendJson(res, 200, { featureRequests: result });
}

async function handleFeatureRequestReply(req, res, id) {
  const session = getAdminSession(req);
  if (!session) {
    return sendJson(res, 403, { error: 'Admin access required' });
  }

  const featureRequest = featureRequestsDb.find(fr => fr.id === id);
  if (!featureRequest) {
    return sendJson(res, 404, { error: 'Feature request not found' });
  }

  try {
    const body = await parseJsonBody(req);
    const reply = (body.reply || '').toString().trim();

    featureRequest.adminReply = reply;
    featureRequest.updatedAt = new Date().toISOString();
    await persistFeatureRequestsDb();

    log('Feature request reply added', { id, adminId: session.userId });
    sendJson(res, 200, { ok: true, adminReply: reply });
  } catch (error) {
    sendJson(res, 400, { error: error.message || 'Unable to add reply' });
  }
}

async function handleFeatureRequestUpdateStatus(req, res, id) {
  const session = getAdminSession(req);
  if (!session) {
    return sendJson(res, 403, { error: 'Admin access required' });
  }

  const featureRequest = featureRequestsDb.find(fr => fr.id === id);
  if (!featureRequest) {
    return sendJson(res, 404, { error: 'Feature request not found' });
  }

  try {
    const body = await parseJsonBody(req);
    const status = (body.status || '').toString().trim();
    const validStatuses = ['pending', 'planned', 'in-progress', 'completed', 'declined'];

    if (!validStatuses.includes(status)) {
      return sendJson(res, 400, { error: 'Invalid status. Must be one of: ' + validStatuses.join(', ') });
    }

    featureRequest.status = status;
    featureRequest.updatedAt = new Date().toISOString();
    await persistFeatureRequestsDb();

    log('Feature request status updated', { id, status, adminId: session.userId });
    sendJson(res, 200, { ok: true, status });
  } catch (error) {
    sendJson(res, 400, { error: error.message || 'Unable to update status' });
  }
}

async function handleFeatureRequestDelete(req, res, id) {
  const session = getAdminSession(req);
  if (!session) {
    return sendJson(res, 403, { error: 'Admin access required' });
  }

  const index = featureRequestsDb.findIndex(fr => fr.id === id);
  if (index === -1) {
    return sendJson(res, 404, { error: 'Feature request not found' });
  }

  featureRequestsDb.splice(index, 1);
  await persistFeatureRequestsDb();

  log('Feature request deleted', { id, adminId: session.userId });
  sendJson(res, 200, { ok: true });
}

async function handleContactMessagesList(req, res) {
  const session = getAdminSession(req);

  if (!session) {
    return sendJson(res, 403, { error: 'Admin access required' });
  }

  const sorted = [...contactMessagesDb].sort((a, b) => {
    return new Date(b.createdAt) - new Date(a.createdAt);
  });

  const result = sorted.map(msg => ({
    id: msg.id,
    name: msg.name,
    email: msg.email,
    subject: msg.subject,
    message: msg.message,
    createdAt: msg.createdAt,
    read: msg.read || false,
  }));

  sendJson(res, 200, { messages: result });
}

async function handleContactMessageCreate(req, res) {
  try {
    const body = await parseJsonBody(req);
    const name = (body.name || '').toString().trim();
    const email = (body.email || '').toString().trim();
    const subject = (body.subject || '').toString().trim();
    const message = (body.message || '').toString().trim();

    if (!name || name.length < 2) {
      return sendJson(res, 400, { error: 'Name must be at least 2 characters' });
    }
    if (!email || !EMAIL_REGEX.test(email)) {
      return sendJson(res, 400, { error: 'Please provide a valid email address' });
    }
    if (!subject || subject.length < 3) {
      return sendJson(res, 400, { error: 'Subject must be at least 3 characters' });
    }
    if (!message || message.length < 10) {
      return sendJson(res, 400, { error: 'Message must be at least 10 characters' });
    }

    const contactMessage = {
      id: randomUUID(),
      name,
      email,
      subject,
      message,
      read: false,
      createdAt: new Date().toISOString(),
      ip: req.socket?.remoteAddress || '',
    };

    contactMessagesDb.push(contactMessage);
    await persistContactMessagesDb();

    log('Contact message received', { id: contactMessage.id, email });
    sendJson(res, 201, { ok: true, id: contactMessage.id });
  } catch (error) {
    log('Contact form error', { error: String(error) });
    sendJson(res, 400, { error: error.message || 'Unable to process your message' });
  }
}

async function handleContactMessageMarkRead(req, res, id) {
  const session = getAdminSession(req);

  if (!session) {
    return sendJson(res, 403, { error: 'Admin access required' });
  }

  const message = contactMessagesDb.find(msg => msg.id === id);
  if (!message) return sendJson(res, 404, { error: 'Message not found' });

  message.read = true;
  await persistContactMessagesDb();

  sendJson(res, 200, { ok: true });
}

async function handleContactMessageDelete(req, res, id) {
  const session = getAdminSession(req);

  if (!session) {
    return sendJson(res, 403, { error: 'Admin access required' });
  }

  const index = contactMessagesDb.findIndex(msg => msg.id === id);
  if (index === -1) return sendJson(res, 404, { error: 'Message not found' });

  contactMessagesDb.splice(index, 1);
  await persistContactMessagesDb();

  sendJson(res, 200, { ok: true });
}

// Blog API Handlers
async function handleBlogsList(req, res, url) {
  try {
    const type = url.searchParams.get('type');
    const category = url.searchParams.get('category');
    const limit = parseInt(url.searchParams.get('limit')) || 10;
    const offset = parseInt(url.searchParams.get('offset')) || 0;

    const blogs = await blogSystem.getAllBlogs({
      type,
      category,
      status: 'published',
      limit,
      offset
    });

    // Sanitize response (don't send full content in list)
    const sanitized = blogs.map(b => ({
      id: b.id,
      slug: b.slug,
      type: b.type,
      title: b.title,
      excerpt: b.excerpt,
      author: b.author,
      featured_image: b.featured_image,
      category: b.category,
      tags: b.tags,
      published_at: b.published_at,
      updated_at: b.updated_at
    }));

    sendJson(res, 200, { posts: sanitized, total: blogs.length });
  } catch (error) {
    log('Failed to list blogs', { error: String(error) });
    sendJson(res, 500, { error: 'Failed to list blogs' });
  }
}

async function handleBlogGet(req, res, slug) {
  try {
    const blog = await blogSystem.getBlogBySlug(slug);
    if (!blog) {
      return sendJson(res, 404, { error: 'Blog post not found' });
    }

    // Convert blocks to HTML for easier consumption
    const html = blogSystem.blocksToHtml(blog.content?.blocks || []);

    sendJson(res, 200, {
      post: {
        ...blog,
        html
      }
    });
  } catch (error) {
    log('Failed to get blog', { error: String(error), slug });
    sendJson(res, 500, { error: 'Failed to get blog post' });
  }
}

async function handleBlogCategoriesList(req, res) {
  try {
    const categories = await blogSystem.getCategories();
    sendJson(res, 200, { categories });
  } catch (error) {
    log('Failed to list blog categories', { error: String(error) });
    sendJson(res, 500, { error: 'Failed to list categories' });
  }
}

async function handleAdminBlogsList(req, res, url) {
  try {
    const type = url.searchParams.get('type');
    const status = url.searchParams.get('status');
    const source = url.searchParams.get('source');
    const category = url.searchParams.get('category');

    const blogs = await blogSystem.getAllBlogs({ type, status, source, category });
    const categories = await blogSystem.getCategories();

    sendJson(res, 200, { posts: blogs, categories });
  } catch (error) {
    log('Failed to list admin blogs', { error: String(error) });
    sendJson(res, 500, { error: 'Failed to list blogs' });
  }
}

async function handleAdminBlogCreate(req, res) {
  try {
    const body = await readBodyJson(req);
    const { title, slug, type, content, excerpt, author, featured_image, meta_title, meta_description, category, tags, status } = body;

    if (!title || !slug || !type) {
      return sendJson(res, 400, { error: 'Title, slug, and type are required' });
    }

    const newPost = await blogSystem.createBlog({
      title,
      slug,
      type,
      content,
      excerpt,
      author: author || 'Admin',
      featured_image,
      meta_title,
      meta_description,
      category,
      tags: tags || [],
      status: status || 'draft'
    });

    sendJson(res, 201, { post: newPost });
  } catch (error) {
    log('Failed to create blog', { error: String(error) });
    sendJson(res, 500, { error: error.message || 'Failed to create blog post' });
  }
}

async function handleAdminBlogUpdate(req, res, id) {
  try {
    const body = await readBodyJson(req);
    const { title, slug, content, excerpt, author, featured_image, meta_title, meta_description, category, tags, status, published_at } = body;

    const updated = await blogSystem.updateBlog(id, {
      title,
      slug,
      content,
      excerpt,
      author,
      featured_image,
      meta_title,
      meta_description,
      category,
      tags,
      status,
      published_at
    });

    sendJson(res, 200, { post: updated });
  } catch (error) {
    log('Failed to update blog', { error: String(error), id });
    sendJson(res, 500, { error: error.message || 'Failed to update blog post' });
  }
}

async function handleAdminBlogDelete(req, res, id) {
  try {
    await blogSystem.deleteBlog(id);
    sendJson(res, 200, { ok: true });
  } catch (error) {
    log('Failed to delete blog', { error: String(error), id });
    sendJson(res, 500, { error: error.message || 'Failed to delete blog post' });
  }
}

async function handleAdminBlogCategoryCreate(req, res) {
  try {
    const body = await readBodyJson(req);
    const { name, slug, description } = body;

    if (!name || !slug) {
      return sendJson(res, 400, { error: 'Name and slug are required' });
    }

    const category = await blogSystem.createCategory({ name, slug, description });
    sendJson(res, 201, { category });
  } catch (error) {
    log('Failed to create blog category', { error: String(error) });
    sendJson(res, 500, { error: error.message || 'Failed to create category' });
  }
}

async function handleAdminBlogCategoryUpdate(req, res, id) {
  try {
    const body = await readBodyJson(req);
    const { name, slug, description } = body;

    const updated = await blogSystem.updateCategory(id, { name, slug, description });
    sendJson(res, 200, { category: updated });
  } catch (error) {
    log('Failed to update blog category', { error: String(error), id });
    sendJson(res, 500, { error: error.message || 'Failed to update category' });
  }
}

async function handleAdminBlogCategoryDelete(req, res, id) {
  try {
    await blogSystem.deleteCategory(id);
    sendJson(res, 200, { ok: true });
  } catch (error) {
    log('Failed to delete blog category', { error: String(error), id });
    sendJson(res, 500, { error: error.message || 'Failed to delete category' });
  }
}

async function handleAdminBlogImageUpload(req, res) {
  try {
    const contentType = req.headers['content-type'] || '';
    if (!contentType.includes('multipart/form-data')) {
      return sendJson(res, 400, { error: 'Expected multipart/form-data' });
    }

    // Parse multipart form data manually
    const chunks = [];
    for await (const chunk of req) {
      chunks.push(chunk);
    }
    const buffer = Buffer.concat(chunks);

    // Simple boundary parsing
    const boundary = contentType.split('boundary=')[1];
    if (!boundary) {
      return sendJson(res, 400, { error: 'No boundary found' });
    }

    // Find file data (simplified - in production use proper multipart parser)
    const boundaryBuffer = Buffer.from('--' + boundary);
    const parts = [];
    let start = 0;
    
    while (true) {
      const idx = buffer.indexOf(boundaryBuffer, start);
      if (idx === -1) break;
      const nextIdx = buffer.indexOf(boundaryBuffer, idx + boundaryBuffer.length);
      const part = buffer.slice(idx + boundaryBuffer.length, nextIdx !== -1 ? nextIdx : buffer.length);
      parts.push(part);
      start = idx + boundaryBuffer.length;
      if (nextIdx === -1) break;
    }

    // Process first file part
    let fileData = null;
    let fileName = null;
    
    for (const part of parts) {
      const headerEnd = part.indexOf('\r\n\r\n');
      if (headerEnd === -1) continue;
      
      const headers = part.slice(0, headerEnd).toString();
      const data = part.slice(headerEnd + 4);
      
      if (headers.includes('filename="')) {
        const match = headers.match(/filename="([^"]+)"/);
        if (match) {
          fileName = match[1];
          // Remove trailing \r\n--
          fileData = data.slice(0, -4);
          break;
        }
      }
    }

    if (!fileData || !fileName) {
      return sendJson(res, 400, { error: 'No file found' });
    }

    // Generate unique filename
    const ext = path.extname(fileName) || '.jpg';
    const uniqueName = `blog-${Date.now()}-${Math.random().toString(36).substr(2, 9)}${ext}`;
    const filePath = path.join(blogSystem.BLOGS_UPLOAD_DIR, uniqueName);

    await fs.writeFile(filePath, fileData);

    sendJson(res, 200, {
      success: 1,
      file: {
        url: `/blogs/images/${uniqueName}`,
        name: uniqueName
      }
    });
  } catch (error) {
    log('Failed to upload blog image', { error: String(error) });
    sendJson(res, 500, { error: 'Failed to upload image' });
  }
}

async function handleUserLogout(req, res) {
  const session = getUserSession(req);
  if (session) {
    // Track user logout and session end
    trackUserSession(session.userId, 'logout', { sessionDuration: session.expiresAt - Date.now() });
    userSessions.delete(session.token);
    persistUserSessions().catch(() => {});
  }
  clearUserSession(res);
  sendJson(res, 200, { ok: true });
}

async function handleUserMe(req, res) {
  const session = requireUserAuth(req, res);
  if (!session) return;

  const user = findUserById(session.userId);
  if (!user) {
    return sendJson(res, 404, { error: 'User not found' });
  }

  // Generate CSRF token for state-changing operations
  const csrfToken = generateCsrfToken(user.id);

  sendJson(res, 200, {
    ok: true,
    user: { id: user.id, email: user.email, createdAt: user.createdAt, lastLoginAt: user.lastLoginAt, plan: user.plan },
    expiresAt: session.expiresAt,
    csrfToken
  });
}

// CSRF token refresh endpoint
async function handleCsrfToken(req, res) {
  const session = requireUserAuth(req, res);
  if (!session) return;

  const csrfToken = generateCsrfToken(session.userId);
  sendJson(res, 200, { csrfToken });
}

// Validate CSRF token middleware for state-changing endpoints
function validateCsrfMiddleware(req, res, userId, body = null) {
  const csrfToken = req.headers['x-csrf-token'] || body?.csrfToken;
  if (!csrfToken || !validateCsrfToken(csrfToken, userId)) {
    log('csrf validation failed', { userId, path: req.url });
    return sendJson(res, 403, { error: 'Invalid CSRF token' });
  }
  return null; // Valid
}

async function handleAccountSettingsGet(req, res, url) {
  // Use requireUserId to support both session cookie and X-User-Id header
  // This matches the pattern used by /api/sessions for consistency
  const userId = requireUserId(req, res, url);
  if (!userId) return;

  const user = findUserById(userId);
  if (!user) return sendJson(res, 404, { error: 'User not found' });

  return sendJson(res, 200, { ok: true, account: await serializeAccount(user) });
}

async function handleAccountUsage(req, res) {
  // Prefer authenticated user-session. If absent, fall back to legacy identity (chat_user cookie / X-User-Id)
  // so the builder usage meter still updates for legacy clients.
  const authed = getUserSession(req);
  const resolvedUserId = authed?.userId || resolveUserId(req);
  if (!resolvedUserId) {
    return sendJson(res, 401, { error: 'User identity required' });
  }
  const user = findUserById(resolvedUserId);
  const plan = user?.plan || DEFAULT_PLAN;
  const summary = getTokenUsageSummary(resolvedUserId, plan);
  const externalTesting = getExternalTestUsageSummary(resolvedUserId, plan);
  const payg = PAYG_ENABLED && isPaidPlan(plan) && !user?.unlimitedUsage
    ? computePaygSummary(resolvedUserId, plan)
    : null;
  return sendJson(res, 200, { ok: true, summary: { ...summary, externalTesting }, payg, legacy: !authed });
}

async function handleAccountPlans(_req, res) {
  sendJson(res, 200, { plans: USER_PLANS, defaultPlan: DEFAULT_PLAN });
}

async function handleProviderLimitsGet(_req, res) {
  try {
    sendJson(res, 200, {
      opencodeBackupModel: providerLimits.opencodeBackupModel || '',
      limits: providerLimits.limits || {}
    });
  } catch (error) {
    sendJson(res, 500, { error: error.message || 'Unable to load provider limits' });
  }
}

async function handleAccountSettingsUpdate(req, res) {
  const session = requireUserAuth(req, res);
  if (!session) return;
  
  const user = findUserById(session.userId);
  if (!user) return sendJson(res, 404, { error: 'User not found' });
  const previousPlan = user.plan;

  const body = await parseJsonBody(req).catch(() => ({}));

  // Validate CSRF token
  const csrfError = validateCsrfMiddleware(req, res, session.userId, body);
  if (csrfError) return;

  let updated = false;

  if (!user.referredByAffiliateCode) {
    const cookieCode = sanitizeAffiliateCode(readAffiliateReferralCode(req));
    if (cookieCode && findAffiliateByCode(cookieCode)) {
      user.referredByAffiliateCode = cookieCode;
      user.affiliateAttributionAt = new Date().toISOString();
      updated = true;
    }
  }

  const requestedPlan = normalizePlanSelection(body.plan || body.newPlan);
  const requestedBillingCycle = String(body.billingCycle || '').toLowerCase();
  const requestedCurrency = String(body.currency || '').toLowerCase();

  // Handle plan changes with Dodo subscription management
  if (requestedPlan && requestedPlan !== user.plan) {
    const isPaidToFree = PAID_PLANS.has(user.plan) && requestedPlan === 'hobby';
    const isPaidToPaid = PAID_PLANS.has(user.plan) && PAID_PLANS.has(requestedPlan);
    const isFreeToPaid = user.plan === 'hobby' && PAID_PLANS.has(requestedPlan);

    // Helper function to lookup subscription by customer
    async function lookupSubscriptionByCustomer(user) {
      try {
        log('DEBUG: Starting subscription lookup', {
          userId: user.id,
          email: user.email,
          hasDodoCustomerId: !!user.dodoCustomerId
        });
        
        const customerId = await ensureDodoCustomer(user);
        
        log('DEBUG: After ensureDodoCustomer', {
          userId: user.id,
          customerId: customerId,
          hasCustomerId: !!customerId
        });
        
        if (!customerId) return null;
        
        // First try: Query ALL subscriptions for customer without status filter
        log('DEBUG: Querying ALL subscriptions for customer (no status filter)', {
          userId: user.id,
          customerId: customerId
        });
        
        const allSubscriptions = await dodoRequest('/subscriptions', {
          method: 'GET',
          query: { customer_id: customerId, page_size: 100 }
        });
        
        log('DEBUG: All subscriptions query response', {
          userId: user.id,
          customerId: customerId,
          responseKeys: Object.keys(allSubscriptions || {}),
          hasItems: !!allSubscriptions?.items,
          itemsCount: allSubscriptions?.items?.length || 0,
          itemsDetails: allSubscriptions?.items?.map(s => ({
            id: s.subscription_id || s.id,
            status: s.status,
            productId: s.product_id
          })),
          fullResponse: JSON.stringify(allSubscriptions).substring(0, 1000)
        });
        
        if (allSubscriptions?.items && allSubscriptions.items.length > 0) {
          // Find the most recent active subscription, or any subscription if no active one
          const activeSub = allSubscriptions.items.find(s => s.status === 'active');
          const foundSub = activeSub || allSubscriptions.items[0];
          
          log('Found subscription via customer lookup', {
            userId: user.id,
            email: user.email,
            subscriptionId: foundSub.subscription_id || foundSub.id,
            status: foundSub.status,
            totalSubs: allSubscriptions.items.length,
            customerId
          });
          return foundSub.subscription_id || foundSub.id;
        }
        
        
        // Fallback: Check specific statuses if no results without filter
        const statusesToCheck = ['active', 'pending', 'on_hold', 'cancelled', 'expired'];
        
        for (const status of statusesToCheck) {
          log('DEBUG: Querying subscriptions with status filter', {
            userId: user.id,
            customerId: customerId,
            status: status
          });
          
          const subscriptions = await dodoRequest('/subscriptions', {
            method: 'GET',
            query: { customer_id: customerId, status: status }
          });
          
          log('DEBUG: Subscription query response', {
            userId: user.id,
            status: status,
            itemsCount: subscriptions?.items?.length || 0
          });
          
          if (subscriptions?.items && subscriptions.items.length > 0) {
            const foundSub = subscriptions.items[0];
            log('Found missing subscription ID via Dodo lookup', {
              userId: user.id,
              email: user.email,
              subscriptionId: foundSub.subscription_id || foundSub.id,
              status: status,
              customerId
            });
            return foundSub.subscription_id || foundSub.id;
          }
        }
        
        log('DEBUG: No subscriptions found for customer', {
          userId: user.id,
          customerId: customerId,
          checkedStatuses: statusesToCheck
        });
        
        // Fallback: Search ALL subscriptions without customer filter to find any with matching metadata
        try {
          log('DEBUG: Trying fallback - searching all subscriptions', {
            userId: user.id,
            email: user.email
          });
          
          for (const status of statusesToCheck) {
            const allSubscriptions = await dodoRequest('/subscriptions', {
              method: 'GET',
              query: { status: status, page_size: 100 }
            });
            
            log('DEBUG: All subscriptions query', {
              userId: user.id,
              status: status,
              totalItems: allSubscriptions?.items?.length || 0
            });
            
            if (allSubscriptions?.items && allSubscriptions.items.length > 0) {
              // Look for subscription with matching email in customer data
              const matchingSub = allSubscriptions.items.find(sub => {
                const subEmail = sub.customer?.email || '';
                const subMetadata = sub.metadata || {};
                return subEmail.toLowerCase() === user.email.toLowerCase() || 
                       subMetadata.userId === String(user.id) ||
                       subMetadata.email === user.email;
              });
              
              if (matchingSub) {
                log('Found subscription via fallback search', {
                  userId: user.id,
                  email: user.email,
                  subscriptionId: matchingSub.subscription_id || matchingSub.id,
                  foundCustomerId: matchingSub.customer?.customer_id,
                  status: status
                });
                
                // Update the customer ID if different
                if (matchingSub.customer?.customer_id && matchingSub.customer.customer_id !== customerId) {
                  user.dodoCustomerId = matchingSub.customer.customer_id;
                  await persistUsersDb();
                  log('Updated dodoCustomerId from fallback search', {
                    userId: user.id,
                    oldCustomerId: customerId,
                    newCustomerId: matchingSub.customer.customer_id
                  });
                }
                
                return matchingSub.subscription_id || matchingSub.id;
              }
            }
          }
        } catch (fallbackError) {
          log('DEBUG: Fallback search failed', {
            userId: user.id,
            error: String(fallbackError)
          });
        }
        
        return null;
      } catch (error) {
        log('Failed to lookup subscription by customer', {
          userId: user.id,
          email: user.email,
          error: String(error)
        });
        return null;
      }
    }

    // If paid user is missing dodoSubscriptionId, try to look it up
    if ((isPaidToFree || isPaidToPaid) && !user.dodoSubscriptionId && PAID_PLANS.has(user.plan)) {
      log('DEBUG: About to lookup subscription for paid user', {
        userId: user.id,
        email: user.email,
        currentPlan: user.plan,
        requestedPlan: requestedPlan,
        isPaidToFree: isPaidToFree,
        isPaidToPaid: isPaidToPaid,
        hasDodoCustomerId: !!user.dodoCustomerId,
        dodoCustomerId: user.dodoCustomerId
      });
      
      const foundSubscriptionId = await lookupSubscriptionByCustomer(user);
      if (foundSubscriptionId) {
        user.dodoSubscriptionId = foundSubscriptionId;
        await persistUsersDb();
        log('Recovered missing subscription ID', {
          userId: user.id,
          email: user.email,
          subscriptionId: foundSubscriptionId
        });
      } else {
        // Log the issue for investigation
        log('CRITICAL: Paid user missing dodoSubscriptionId and no active subscription found in Dodo', {
          userId: user.id,
          email: user.email,
          currentPlan: user.plan,
          requestedPlan: requestedPlan,
          billingStatus: user.billingStatus,
          subscriptionRenewsAt: user.subscriptionRenewsAt
        });
      }
    }

    // Schedule cancellation at period end when changing from paid to free hobby plan
    // User keeps premium features until the end of their billing period
    if (isPaidToFree && user.dodoSubscriptionId) {
      const cancelSuccess = await cancelDodoSubscription(user, 'paid_to_free', { 
        clearOnFailure: true, 
        mode: 'at_period_end' 
      });
      
      if (cancelSuccess) {
        // Mark as scheduled for cancellation but keep plan details until period ends
        user.scheduledPlanChange = {
          fromPlan: user.plan,
          toPlan: 'hobby',
          scheduledAt: new Date().toISOString(),
        };
        user.billingStatus = 'cancellation_scheduled';
        // Keep subscriptionRenewsAt and other details until actual cancellation
        updated = true;
        
        log('Scheduled subscription cancellation at period end', { 
          userId: user.id, 
          subscriptionId: user.dodoSubscriptionId,
          renewsAt: user.subscriptionRenewsAt 
        });
        
        // Return success response immediately
        await persistUsersDb();
        return sendJson(res, 200, { ok: true, account: await serializeAccount(user) });
      } else {
        return sendJson(res, 400, { error: 'Failed to schedule subscription cancellation' });
      }
    }
    // For paid-to-paid changes, use Dodo's Change Plan API if subscription exists
    else if (isPaidToPaid && user.dodoSubscriptionId) {
      try {
        // Ensure we have valid billing cycle and currency
        const targetBillingCycle = requestedBillingCycle || user.billingCycle || 'monthly';
        const targetCurrency = requestedCurrency || user.subscriptionCurrency || 'usd';
        
        // Call Dodo's Change Plan API
        await changeDodoSubscriptionPlan(user, requestedPlan, targetBillingCycle, targetCurrency);
        
        // Update user record after successful plan change
        user.plan = requestedPlan;
        user.billingCycle = targetBillingCycle;
        user.subscriptionCurrency = targetCurrency;
        user.billingStatus = DEFAULT_BILLING_STATUS;
        // Keep the same billing cycle end date - Dodo maintains this
        // user.subscriptionRenewsAt stays the same
        updated = true;
        
        // Clear any scheduled plan change if exists
        if (user.scheduledPlanChange) {
          delete user.scheduledPlanChange;
        }
        
        // Return success response immediately after plan change
        await persistUsersDb();
        return sendJson(res, 200, { ok: true, account: await serializeAccount(user) });
      } catch (error) {
        log('Failed to change plan via Dodo API', { userId: user.id, error: String(error) });
        return sendJson(res, 400, { error: error.message || 'Unable to change subscription plan' });
      }
    }
    // Handle paid-to-free or paid-to-paid when subscription ID is still missing after lookup
    else if ((isPaidToFree || isPaidToPaid) && !user.dodoSubscriptionId) {
      log('ERROR: Cannot change plan - paid user missing dodoSubscriptionId after lookup', {
        userId: user.id,
        email: user.email,
        currentPlan: user.plan,
        requestedPlan: requestedPlan,
        changeType: isPaidToFree ? 'paid_to_free' : 'paid_to_paid'
      });
      return sendJson(res, 400, { 
        error: 'Subscription ID not found. Please contact support to resolve this issue.' 
      });
    }
    // For free-to-paid upgrades, create a checkout session and return the URL
    else if (isFreeToPaid || (!user.dodoSubscriptionId && PAID_PLANS.has(requestedPlan))) {
      try {
        const targetBillingCycle = requestedBillingCycle || 'monthly';
        const targetCurrency = requestedCurrency || user.currency || 'usd';
        
        // Validate the subscription selection
        if (!validateSubscriptionSelection(requestedPlan, targetBillingCycle, targetCurrency)) {
          return sendJson(res, 400, { error: 'Invalid plan, billing cycle, or currency combination' });
        }
        
        const product = resolveSubscriptionProduct(requestedPlan, targetBillingCycle, targetCurrency);
        if (!product) {
          return sendJson(res, 400, { error: 'Subscription product not available' });
        }
        
        // Create a unique session ID for this checkout
        const checkoutSessionId = randomUUID();
        
        // Create checkout session with enhanced metadata for session tracking
        const checkoutBody = {
          product_cart: [{
            product_id: product.productId,
            quantity: 1,
          }],
          customer: {
            email: user.billingEmail || user.email,
            name: user.billingEmail || user.email,
          },
          metadata: {
            type: 'subscription',
            orderId: String(checkoutSessionId),
            userId: String(user.id),
            sessionId: String(checkoutSessionId),
            plan: String(requestedPlan),
            billingCycle: String(targetBillingCycle),
            currency: String(targetCurrency),
            amount: String(product.price),
            isUpgrade: 'true',
          },
          settings: {
            allow_payment_methods: ['card'],
            redirect_immediately: true,
          },
        };
        
        const returnUrl = `${resolveBaseUrl(req)}/subscription-success`;
        checkoutBody.return_url = returnUrl;
        
        const checkoutSession = await dodoRequest('/checkouts', {
          method: 'POST',
          body: checkoutBody,
        });
        
        const sessionId = checkoutSession?.session_id || checkoutSession?.id || '';
        if (!sessionId || !checkoutSession?.checkout_url) {
          throw new Error('Dodo checkout session was not created');
        }
        
        // Store pending subscription
        pendingSubscriptions[sessionId] = {
          userId: user.id,
          orderId: checkoutSessionId,
          plan: requestedPlan,
          billingCycle: targetBillingCycle,
          currency: targetCurrency,
          productId: product.productId,
          price: product.price,
          checkoutSessionId,
          createdAt: new Date().toISOString(),
          isUpgrade: true,
        };
        await persistPendingSubscriptions();
        
        log('Created checkout session for free-to-paid upgrade', {
          userId: user.id,
          plan: requestedPlan,
          sessionId,
        });
        
        return sendJson(res, 200, {
          ok: true,
          requiresCheckout: true,
          checkoutUrl: checkoutSession.checkout_url,
          sessionId,
          message: 'Redirecting to checkout to complete your upgrade',
        });
      } catch (error) {
        log('Failed to create checkout for free-to-paid upgrade', { userId: user.id, error: String(error) });
        return sendJson(res, 400, { 
          error: 'Unable to start checkout process. Please try again or contact support.'
        });
      }
    }
    // Simple plan update for free plans or special cases
    else {
      user.plan = requestedPlan;
      user.billingStatus = DEFAULT_BILLING_STATUS;
      
      // Only set renewal date for paid plans
      if (requestedPlan === 'hobby') {
        user.subscriptionRenewsAt = null;
        user.billingCycle = null;
        user.subscriptionCurrency = null;
      } else {
        user.subscriptionRenewsAt = computeRenewalDate(requestedBillingCycle === 'yearly' ? 'yearly' : 'monthly');
      }
      
      updated = true;
    }
  }

  // Handle currency updates
  if (requestedCurrency && SUPPORTED_CURRENCIES.includes(requestedCurrency)) {
    user.subscriptionCurrency = requestedCurrency;
    user.currency = requestedCurrency;
    updated = true;
  }

  // Handle billing cycle updates (separate from plan change)
  if (requestedBillingCycle && BILLING_CYCLES.includes(requestedBillingCycle) && !requestedPlan) {
    user.billingCycle = requestedBillingCycle;
    if (user.plan !== 'hobby') {
      user.subscriptionRenewsAt = computeRenewalDate(requestedBillingCycle);
    }
    updated = true;
  }

  if (typeof body.billingEmail === 'string' && body.billingEmail.trim()) {
    const nextEmail = body.billingEmail.trim().toLowerCase();
    if (!EMAIL_REGEX.test(nextEmail)) {
      return sendJson(res, 400, { error: 'Billing email is invalid' });
    }
    user.billingEmail = nextEmail;
    updated = true;
  }

  if (typeof body.unlimitedUsage === 'boolean') {
    user.unlimitedUsage = body.unlimitedUsage;
    updated = true;
  }

  const action = (body.action || '').toString().toLowerCase();
  if (action === 'cancel') {
    // Cancel Dodo subscription when user explicitly cancels
    if (user.dodoSubscriptionId && PAID_PLANS.has(user.plan)) {
      await cancelDodoSubscription(user, 'manual_cancel', { clearOnFailure: true });
    }
    user.billingStatus = 'canceled';
    user.subscriptionRenewsAt = null;
    updated = true;
  } else if (action === 'resume') {
    user.billingStatus = DEFAULT_BILLING_STATUS;
    if (!user.subscriptionRenewsAt && user.plan !== 'hobby') {
      user.subscriptionRenewsAt = computeRenewalDate(body.billingCycle === 'yearly' ? 'yearly' : 'monthly');
    }
    updated = true;
  }

  if (updated) {
    const normalizedPlan = normalizePlanSelection(user.plan);
    const previousNormalized = normalizePlanSelection(previousPlan);
    if (PAID_PLANS.has(normalizedPlan) && previousNormalized !== normalizedPlan) {
      await trackAffiliateCommission(user, normalizedPlan);
    }
    await persistUsersDb();
  }

  return sendJson(res, 200, { ok: true, account: await serializeAccount(user) });
}

async function handleOnboardingGet(req, res) {
  const session = requireUserAuth(req, res);
  if (!session) return;

  const user = findUserById(session.userId);
  if (!user) return sendJson(res, 404, { error: 'User not found' });

  sendJson(res, 200, { ok: true, completed: !!user.onboardingCompleted });
}

async function handleOnboardingPost(req, res) {
  const session = requireUserAuth(req, res);
  if (!session) return;

  const user = findUserById(session.userId);
  if (!user) return sendJson(res, 404, { error: 'User not found' });

  const body = await parseJsonBody(req).catch(() => ({}));
  const completed = !!body?.completed;

  if (user.onboardingCompleted !== completed) {
    user.onboardingCompleted = completed;
    await persistUsersDb();
  }

  sendJson(res, 200, { ok: true, completed });
}

async function handlePaymentMethodsList(req, res, url) {
  const userId = requireUserId(req, res, url);
  if (!userId) return;

  const user = findUserById(userId);
  if (!user) return sendJson(res, 404, { error: 'User not found' });

  if (!DODO_ENABLED) {
    return sendJson(res, 200, { paymentMethods: [] });
  }

  try {
    let customerId = user.dodoCustomerId;
    if (!customerId) {
      const customers = await dodoRequest('/customers', {
        method: 'POST',
        body: {
          email: user.billingEmail || user.email,
          name: user.email.split('@')[0],
          metadata: { userId: String(user.id) },
        },
      });
      customerId = customers?.customer_id || customers?.id;
      if (customerId) {
        user.dodoCustomerId = customerId;
        await persistUsersDb();
      }
    }

    if (!customerId) {
      return sendJson(res, 200, { paymentMethods: [] });
    }

    const response = await dodoRequest(`/customers/${customerId}/payment-methods`, { method: 'GET' });
    const paymentMethods = Array.isArray(response) ? response : response?.items || [];

    const serializedMethods = paymentMethods.map((pm, index) => {
      const card = pm.card || {};
      return {
        id: pm.payment_method_id,
        brand: card.brand || pm.payment_method || 'Card',
        last4: card.last4 || card.last_digits || '',
        expiresAt: card.expiry || card.expires_at || '',
        isDefault: index === 0,
      };
    });

    return sendJson(res, 200, { paymentMethods: serializedMethods });
  } catch (err) {
    console.error('[PaymentMethods] List error:', err.message);
    return sendJson(res, 200, { paymentMethods: [] });
  }
}

async function handlePaymentMethodCreate(req, res) {
  const userId = requireUserId(req, res, new URL(req.url, `http://${req.headers.host}`));
  if (!userId) return;

  const user = findUserById(userId);
  if (!user) return sendJson(res, 404, { error: 'User not found' });

  if (!DODO_ENABLED) {
    return sendJson(res, 503, { error: 'Dodo Payments is not configured' });
  }

  try {
    let customerId = user.dodoCustomerId;
    if (!customerId) {
      const customers = await dodoRequest('/customers', {
        method: 'POST',
        body: {
          email: user.billingEmail || user.email,
          name: user.email.split('@')[0],
          metadata: { userId: String(user.id) },
        },
      });
      customerId = customers?.customer_id || customers?.id;
      if (customerId) {
        user.dodoCustomerId = customerId;
        await persistUsersDb();
      }
    }

    if (!customerId) {
      return sendJson(res, 500, { error: 'Unable to create customer' });
    }

    const returnUrl = `${resolveBaseUrl(req)}/settings?payment_method_added=1`;

    const portalSession = await dodoRequest(`/customers/${customerId}/customer-portal/session`, {
      method: 'POST',
    });

    const portalUrl = portalSession?.link || portalSession?.url;
    if (!portalUrl) {
      return sendJson(res, 500, { error: 'Unable to create customer portal session' });
    }

    // The frontend expects a checkoutUrl or url field. Return all common keys so
    // clients (existing and future) can open the portal to add/save cards without purchasing.
    return sendJson(res, 200, { portalUrl, checkoutUrl: portalUrl, url: portalUrl });
  } catch (err) {
    console.error('[PaymentMethod] Create error:', err.message);
    return sendJson(res, 500, { error: 'Unable to create customer portal session' });
  }
}

async function handleAccountBalanceAdd(req, res) {
  const session = requireUserAuth(req, res);
  if (!session) return;

  const user = findUserById(session.userId);
  if (!user) return sendJson(res, 404, { error: 'User not found' });

  const body = await parseJsonBody(req);
  const amount = Number(body?.amount);
  const currency = String(body?.currency || user.currency || 'usd').toLowerCase();

  if (!Number.isFinite(amount) || amount < MIN_PAYMENT_AMOUNT) {
    return sendJson(res, 400, { error: `Minimum amount is $${(MIN_PAYMENT_AMOUNT / 100).toFixed(2)}` });
  }

  if (!DODO_ENABLED) {
    return sendJson(res, 503, { error: 'Dodo Payments is not configured' });
  }

  try {
    let customerId = user.dodoCustomerId;
    if (!customerId) {
      const customers = await dodoRequest('/customers', {
        method: 'POST',
        body: {
          email: user.billingEmail || user.email,
          name: user.email.split('@')[0],
          metadata: { userId: String(user.id) },
        },
      });
      customerId = customers?.customer_id || customers?.id;
      if (customerId) {
        user.dodoCustomerId = customerId;
        await persistUsersDb();
      }
    }

    if (!customerId) {
      return sendJson(res, 500, { error: 'Unable to create customer' });
    }

    const returnUrl = `${resolveBaseUrl(req)}/settings?balance_added=1`;

    const sessionId = `balance_${randomUUID()}`;

    pendingSubscriptions[sessionId] = {
      userId: user.id,
      amount,
      currency,
      type: 'balance_add',
      createdAt: Date.now(),
    };

    await persistPendingSubscriptions();

    const productConfigured = Boolean(TOPUP_PRODUCT_IDS[`topup_1_${currency}`]);

    const checkoutSession = await dodoRequest('/checkout/sessions', {
      method: 'POST',
      body: {
        customer_id: customerId,
        success_url: returnUrl,
        cancel_url: returnUrl,
        mode: 'payment',
        payment_method_types: ['card'],
        line_items: [
          {
            amount: amount,
            currency: currency.toUpperCase(),
            quantity: 1,
            name: `Add Funds - ${currency.toUpperCase()}${(amount / 100).toFixed(2)}`,
            description: `Add funds to account balance`,
            metadata: {
              userId: String(user.id),
              sessionId,
              type: 'balance_add',
            },
          },
        ],
      },
    });

    const checkoutUrl = checkoutSession?.checkout_url || checkoutSession?.url;
    if (!checkoutUrl) {
      return sendJson(res, 500, { error: 'Unable to create checkout session' });
    }

    return sendJson(res, 200, { checkoutUrl, sessionId });
  } catch (err) {
    console.error('[Balance] Add error:', err.message);
    return sendJson(res, 500, { error: 'Unable to create checkout' });
  }
}

async function handlePaymentMethodSetDefault(req, res, methodId) {
  const userId = requireUserId(req, res, new URL(req.url, `http://${req.headers.host}`));
  if (!userId) return;

  const user = findUserById(userId);
  if (!user) return sendJson(res, 404, { error: 'User not found' });

  if (!DODO_ENABLED) {
    return sendJson(res, 503, { error: 'Dodo Payments is not configured' });
  }

  try {
    let customerId = user.dodoCustomerId;
    if (!customerId) {
      return sendJson(res, 400, { error: 'Customer not found. Please add a payment method first.' });
    }

    const returnUrl = `${resolveBaseUrl(req)}/settings`;

    const portalSession = await dodoRequest(`/customers/${customerId}/customer-portal/session`, {
      method: 'POST',
    });

    const portalUrl = portalSession?.link || portalSession?.url;
    if (!portalUrl) {
      return sendJson(res, 500, { error: 'Unable to create customer portal session' });
    }

    return sendJson(res, 200, { portalUrl, message: 'Please manage payment methods through the customer portal' });
  } catch (err) {
    console.error('[PaymentMethod] Set default error:', err.message);
    return sendJson(res, 500, { error: 'Unable to update payment method. Please try again.' });
  }
}

async function handlePaymentMethodDelete(req, res, methodId) {
  const userId = requireUserId(req, res, new URL(req.url, `http://${req.headers.host}`));
  if (!userId) return;

  const user = findUserById(userId);
  if (!user) return sendJson(res, 404, { error: 'User not found' });

  if (!DODO_ENABLED) {
    return sendJson(res, 503, { error: 'Dodo Payments is not configured' });
  }

  try {
    let customerId = user.dodoCustomerId;
    if (!customerId) {
      return sendJson(res, 400, { error: 'Customer not found. Please add a payment method first.' });
    }

    const returnUrl = `${resolveBaseUrl(req)}/settings`;

    const portalSession = await dodoRequest(`/customers/${customerId}/customer-portal/session`, {
      method: 'POST',
    });

    const portalUrl = portalSession?.link || portalSession?.url;
    if (!portalUrl) {
      return sendJson(res, 500, { error: 'Unable to create customer portal session' });
    }

    return sendJson(res, 200, { portalUrl, message: 'Please manage payment methods through the customer portal' });
  } catch (err) {
    console.error('[PaymentMethod] Delete error:', err.message);
    return sendJson(res, 500, { error: 'Unable to delete payment method. Please try again.' });
  }
}

async function fetchTopupProduct(tier, currency = 'usd') {
  const pack = resolveTopupPack(tier, currency);
  if (!pack.productId) throw new Error('Top-up product is not configured');
  const product = await getDodoProductById(pack.productId);
  if (!product) throw new Error('Top-up product is unavailable');
  const baseAmount = getTopupPrice(pack.tier, pack.currency);
  if (!Number.isFinite(baseAmount) || baseAmount <= 0) throw new Error('Top-up price amount is invalid');
  return { pack, product, baseAmount, currency: pack.currency };
}

async function handleTopupOptions(req, res, userId) {
  try {
    const user = findUserById(userId);
    if (!user) return sendJson(res, 404, { error: 'User not found' });
    if (!DODO_ENABLED) {
      return sendJson(res, 503, { error: 'Dodo Payments is not configured' });
    }
    const discount = computeTopupDiscount(user.plan);
    const tiers = ['topup_1', 'topup_2', 'topup_3', 'topup_4'];
    const currencies = ['usd', 'gbp', 'eur'];
    const options = [];
    for (const tier of tiers) {
      for (const currency of currencies) {
        try {
          const pack = resolveTopupPack(tier, currency);
          if (!pack.productId || !pack.tokens) continue;
          const baseAmount = getTopupPrice(tier, currency);
          if (!baseAmount || baseAmount <= 0) continue;
          const finalAmount = applyTopupDiscount(baseAmount, discount);
          options.push({
            tier: pack.tier,
            currency: pack.currency,
            tokens: pack.tokens,
            productId: pack.productId,
            baseAmount,
            finalAmount,
            discountRate: discount,
          });
        } catch (packErr) {
          // Log per-pack errors but continue building remaining options
          log('topup options pack error', { tier, currency, error: String(packErr), stack: packErr.stack });
        }
      }
    }
    if (!options.length) return sendJson(res, 503, { error: 'Top-up products are not configured' });
    return sendJson(res, 200, { options, discount, discountRate: discount });
  } catch (error) {
    log('topup options failed', { error: String(error), stack: error.stack });
    return sendJson(res, 500, { error: error.message || 'Unable to fetch top-up options' });
  }
}

async function handleTopupCheckout(req, res) {
  const session = requireUserAuth(req, res);
  if (!session) return;
  const user = findUserById(session.userId);
  if (!user) return sendJson(res, 404, { error: 'User not found' });
  if (!DODO_ENABLED) return sendJson(res, 503, { error: 'Dodo Payments is not configured' });

  try {
    const body = await parseJsonBody(req).catch(() => ({}));
    const tier = body?.tier || 'topup_1';
    const currency = body?.currency || 'usd';
    const isInline = Boolean(body.inline);
    const { pack, baseAmount } = await fetchTopupProduct(tier, currency);
    if (!pack.tokens) throw new Error('Top-up tokens are not configured');
    const discount = computeTopupDiscount(user.plan);
    const unitAmount = applyTopupDiscount(baseAmount, discount);
    const returnUrl = `${resolveBaseUrl(req)}/topup`;
    const orderId = `topup_${randomUUID()}`;
    
    // Ensure we have a Dodo customer ID before creating checkout
    const customerId = await ensureDodoCustomer(user);
    if (!customerId) {
      log('Failed to get or create Dodo customer for topup', { userId: user.id, email: user.email });
      return sendJson(res, 500, { error: 'Unable to initialize customer for checkout' });
    }
    
    const checkoutBody = {
      product_cart: [{
        product_id: pack.productId,
        quantity: 1,
        amount: unitAmount,
      }],
      customer: {
        customer_id: customerId,  // Pass existing customer ID to prevent duplicates
        email: user.billingEmail || user.email,
        name: user.billingEmail || user.email,
      },
      metadata: {
        type: 'topup',
        orderId,
        customerId,
        userId: String(user.id),
        tokens: String(pack.tokens),
        tier: String(pack.tier),
        currency: String(pack.currency),
        amount: String(unitAmount),
        discountRate: String(discount),
        inline: String(isInline),
      },
      settings: {
        redirect_immediately: false,
      },
      return_url: returnUrl,
    };
    const checkoutSession = await dodoRequest('/checkouts', {
      method: 'POST',
      body: checkoutBody,
    });
    const sessionId = checkoutSession?.session_id || checkoutSession?.id || '';
    if (!sessionId || !checkoutSession?.checkout_url) {
      throw new Error('Dodo checkout session was not created');
    }
    pendingTopups[sessionId] = {
      userId: user.id,
      orderId,
      tokens: pack.tokens,
      tier: pack.tier,
      currency: pack.currency,
      amount: unitAmount,
      productId: pack.productId,
      createdAt: new Date().toISOString(),
      inline: isInline,
    };
    await persistPendingTopups();

    const response = {
      sessionId,
      amount: unitAmount,
      currency,
      tokens: pack.tokens,
    };

    if (isInline) {
      response.inlineCheckoutUrl = checkoutSession.checkout_url;
      response.checkoutUrl = checkoutSession.checkout_url;
    } else {
      response.url = checkoutSession.checkout_url;
      response.checkoutUrl = checkoutSession.checkout_url;
    }

    return sendJson(res, 200, response);
  } catch (error) {
    log('top-up checkout failed', { error: String(error), userId: user?.id });
    const status = /configured/i.test(String(error?.message || '')) ? 503 : 400;
    return sendJson(res, status, { error: error.message || 'Unable to start checkout' });
  }
}

function isDodoPaymentComplete(status) {
  const normalized = String(status || '').toLowerCase();
  return ['paid', 'succeeded', 'success', 'completed', 'complete'].includes(normalized);
}

async function handleTopupConfirm(req, res, url) {
  const session = requireUserAuth(req, res);
  if (!session) return;
  const user = findUserById(session.userId);
  if (!user) return sendJson(res, 404, { error: 'User not found' });
  if (!DODO_ENABLED) return sendJson(res, 503, { error: 'Dodo Payments is not configured' });

  const sessionId = (url && url.searchParams && (url.searchParams.get('session_id') || url.searchParams.get('checkout_id') || url.searchParams.get('session'))) || '';
  if (!sessionId) return sendJson(res, 400, { error: 'session_id is required' });

  const existing = processedTopups[sessionId];
  if (existing) {
    if (existing.userId !== user.id) return sendJson(res, 403, { error: 'This top-up belongs to another user' });

    try {
      await createInvoiceIfMissing(user, 'topup', {
        tokens: existing.tokens,
        amount: existing.amount,
        currency: existing.currency,
        tier: existing.tier,
        source: {
          provider: 'dodo',
          checkoutId: sessionId,
          orderId: existing.orderId,
          paymentId: existing.paymentId,
        },
      });
    } catch (invoiceError) {
      log('failed to create invoice', { userId: user.id, error: String(invoiceError) });
    }

    return sendJson(res, 200, { ok: true, alreadyApplied: true, tokensAdded: existing.tokens, summary: getTokenUsageSummary(user.id, user.plan || DEFAULT_PLAN) });
  }

  const pending = pendingTopups[sessionId];
  if (pending && pending.userId !== user.id) {
    return sendJson(res, 403, { error: 'This top-up belongs to another user' });
  }

  try {
    const checkout = await dodoRequest(`/checkouts/${sessionId}`, { method: 'GET' });
    const paid = isDodoPaymentComplete(checkout?.payment_status || checkout?.status);
    if (!paid) {
      return sendJson(res, 200, { ok: false, pending: true, paymentStatus: checkout?.payment_status || checkout?.status || 'unpaid' });
    }
    const tokens = Number(pending?.tokens || checkout?.metadata?.tokens || 0);
    if (!tokens) return sendJson(res, 400, { error: 'Top-up tokens missing' });

    const orderId = pending?.orderId || checkout?.metadata?.orderId || '';
    const tier = pending?.tier || checkout?.metadata?.tier || '';
    const currency = String(pending?.currency || checkout?.metadata?.currency || checkout?.currency || '').toLowerCase() || null;

    const amountCandidate = pending?.amount !== undefined && pending?.amount !== null
      ? Number(pending.amount)
      : Number(checkout?.metadata?.amount || checkout?.amount || checkout?.amount_total || checkout?.total_amount || 0);
    const amount = Number.isFinite(amountCandidate) ? Math.max(0, Math.round(amountCandidate)) : null;

    const paymentId = checkout?.payment_id || checkout?.paymentId || checkout?.charge_id || checkout?.chargeId || '';

    const bucket = ensureTokenUsageBucket(user.id);
    bucket.addOns = Math.max(0, Number(bucket.addOns || 0) + tokens);
    await persistTokenUsage();

    processedTopups[sessionId] = {
      userId: user.id,
      orderId: orderId || null,
      paymentId: paymentId || null,
      tokens,
      tier: tier || null,
      amount,
      currency,
      completedAt: new Date().toISOString(),
    };
    delete pendingTopups[sessionId];
    await Promise.all([persistTopupSessions(), persistPendingTopups()]);

    await sendPaymentConfirmationEmail(user, 'topup', {
      tokens,
      amount,
      currency,
    });

    try {
      await createInvoiceIfMissing(user, 'topup', {
        tokens,
        amount,
        currency,
        tier,
        source: {
          provider: 'dodo',
          checkoutId: sessionId,
          orderId,
          paymentId,
        },
      });
    } catch (invoiceError) {
      log('failed to create invoice', { userId: user.id, error: String(invoiceError) });
    }

    return sendJson(res, 200, { ok: true, tokensAdded: tokens, summary: getTokenUsageSummary(user.id, user.plan || DEFAULT_PLAN) });
  } catch (error) {
    log('top-up confirmation failed', { error: String(error), userId: user?.id, sessionId });
    return sendJson(res, 400, { error: error.message || 'Unable to confirm payment' });
  }
}

// -------------------------
// Admin test endpoints for Dodo top-ups
// -------------------------
async function handleAdminTopupOptions(req, res) {
  try {
    if (!DODO_ENABLED) return sendJson(res, 503, { error: 'Dodo Payments is not configured' });
    const discount = 0; // admin tests run without user plan discount
    const tiers = ['topup_1', 'topup_2', 'topup_3', 'topup_4'];
    const currencies = ['usd', 'gbp', 'eur'];
    const options = [];

    // Quick debug log to surface configuration state when this endpoint is called
    try {
      const productKeys = TOPUP_PRODUCT_IDS && typeof TOPUP_PRODUCT_IDS === 'object' ? Object.keys(TOPUP_PRODUCT_IDS) : [];
      log('admin topup options debug start', {
        DODO_ENABLED,
        productKeysCount: productKeys.length,
        productKeySample: productKeys.slice(0, 6),
        topupTokensKeys: Object.keys(TOPUP_TOKENS || {}).slice(0, 6),
        topupPricesKeys: Object.keys(TOPUP_PRICES || {}).slice(0, 6),
      });
    } catch (dbgErr) {
      log('admin topup options debug logging failed', { dbgErr: String(dbgErr) });
    }

    // Build options similarly to the user-facing flow. Allow missing TOPUP_PRODUCT_IDS but log a warning so
    // admins can still use the test page in environments where product IDs may not be pre-populated.
    if (!TOPUP_PRODUCT_IDS || typeof TOPUP_PRODUCT_IDS !== 'object') {
      log('admin topup options warning', { reason: 'TOPUP_PRODUCT_IDS missing or invalid - attempting to build options from fallback data' });
    }

    for (const tier of tiers) {
      for (const currency of currencies) {
        try {
          const pack = resolveTopupPack(tier, currency);
          if (!pack || !pack.productId || !pack.tokens) {
            log('admin topup options skip pack', { tier, currency, pack });
            continue;
          }

          const baseAmount = getTopupPrice(tier, currency);
          if (!baseAmount || baseAmount <= 0) {
            log('admin topup options skip price', { tier, currency, baseAmount });
            continue;
          }

          const finalAmount = applyTopupDiscount(baseAmount, discount);
          options.push({
            tier: pack.tier,
            currency: pack.currency,
            tokens: pack.tokens,
            productId: pack.productId,
            baseAmount,
            finalAmount,
            discountRate: discount,
          });
        } catch (packErr) {
          // Log per-pack errors but continue building remaining options
          log('admin topup options pack error', { tier, currency, error: String(packErr), stack: packErr.stack });
        }
      }
    }

    if (!options.length) {
      log('admin topup options none found', { TOPUP_PRODUCT_IDS: Object.keys(TOPUP_PRODUCT_IDS || {}).slice(0, 10) });
      return sendJson(res, 503, { error: 'Top-up products are not configured' });
    }

    log('admin topup options success', { optionsCount: options.length });
    return sendJson(res, 200, { options, discount, discountRate: discount });
  } catch (error) {
    log('admin topup options failed', { error: String(error), stack: error.stack });
    return sendJson(res, 500, { error: error.message || 'Unable to fetch admin top-up options' });
  }
}

async function handleAdminTopupCheckout(req, res) {
  const session = requireAdminAuth(req, res);
  if (!session) return;
  if (!DODO_ENABLED) return sendJson(res, 503, { error: 'Dodo Payments is not configured' });

  try {
    const body = await parseJsonBody(req).catch(() => ({}));
    const tier = body?.tier || 'topup_1';
    const currency = (body?.currency || 'usd').toLowerCase();
    const isInline = Boolean(body.inline);
    const customer = body.customer || {};

    const { pack, baseAmount } = await fetchTopupProduct(tier, currency);
    if (!pack.tokens) throw new Error('Top-up tokens are not configured');
    const unitAmount = applyTopupDiscount(baseAmount, 0);
    const returnUrl = `${resolveBaseUrl(req)}/test-checkout`;
    const orderId = `admin_test_topup_${randomUUID()}`;

    // Helper to validate email format
    const isValidEmail = (email) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(email || ''));
    const customerEmail = customer.email || ADMIN_USER || '';
    const validEmail = isValidEmail(customerEmail) ? customerEmail : 'admin@example.com';

    const checkoutBody = {
      product_cart: [{
        product_id: pack.productId,
        quantity: 1,
        amount: unitAmount,
      }],
      customer: {
        email: validEmail,
        name: (customer.name || ADMIN_USER || 'Admin'),
      },
      metadata: {
        type: 'admin_test_topup',
        orderId,
        admin: 'true',
        adminToken: session.token || '',
        tokens: String(pack.tokens),
        tier: String(pack.tier),
        currency: String(pack.currency),
        amount: String(unitAmount),
      },
      settings: {
        redirect_immediately: false,
      },
      return_url: returnUrl,
    };

    const checkoutSession = await dodoRequest('/checkouts', {
      method: 'POST',
      body: checkoutBody,
    });

    const sessionId = checkoutSession?.session_id || checkoutSession?.id || '';
    if (!sessionId || !checkoutSession?.checkout_url) {
      throw new Error('Dodo checkout session was not created');
    }

    pendingTopups[sessionId] = {
      userId: null,
      admin: true,
      adminToken: session.token,
      orderId,
      tokens: pack.tokens,
      tier: pack.tier,
      currency: pack.currency,
      amount: unitAmount,
      productId: pack.productId,
      createdAt: new Date().toISOString(),
      inline: isInline,
    };

    await persistPendingTopups();

    const response = {
      sessionId,
      amount: unitAmount,
      currency,
      tokens: pack.tokens,
    };

    if (isInline) {
      response.inlineCheckoutUrl = checkoutSession.checkout_url;
      response.checkoutUrl = checkoutSession.checkout_url;
    } else {
      response.checkoutUrl = checkoutSession.checkout_url;
      response.url = checkoutSession.checkout_url;
    }

    return sendJson(res, 200, response);
  } catch (error) {
    log('admin top-up checkout failed', { error: String(error) });
    const status = /configured/i.test(String(error?.message || '')) ? 503 : 400;
    return sendJson(res, status, { error: error.message || 'Unable to start admin checkout' });
  }
}

async function handleAdminTopupConfirm(req, res, url) {
  const session = requireAdminAuth(req, res);
  if (!session) return;
  if (!DODO_ENABLED) return sendJson(res, 503, { error: 'Dodo Payments is not configured' });

  const sessionId = (url && url.searchParams && (url.searchParams.get('session_id') || url.searchParams.get('checkout_id') || url.searchParams.get('session'))) || '';
  if (!sessionId) return sendJson(res, 400, { error: 'session_id is required' });

  try {
    const checkout = await dodoRequest(`/checkouts/${sessionId}`, { method: 'GET' });
    const paid = isDodoPaymentComplete(checkout?.payment_status || checkout?.status);
    if (!paid) {
      return sendJson(res, 200, { ok: false, pending: true, paymentStatus: checkout?.payment_status || checkout?.status || 'unpaid' });
    }

    // Admin test confirmation does not apply tokens to any user; return details for inspection
    return sendJson(res, 200, {
      ok: true,
      paid: true,
      sessionId,
      amount: checkout?.amount || checkout?.amount_total || checkout?.total_amount || null,
      currency: checkout?.currency || null,
      metadata: checkout?.metadata || null,
      checkout,
    });
  } catch (error) {
    log('admin top-up confirmation failed', { error: String(error), sessionId });
    return sendJson(res, 400, { error: error.message || 'Unable to confirm admin checkout' });
  }
}

async function handleAdminMe(req, res) {
  const session = requireAdminAuth(req, res);
  if (!session) return;
  return sendJson(res, 200, { ok: true, admin: { username: ADMIN_USER || 'admin' } });
}

function resolvePaygProduct(currency = 'usd') {
  const normalized = String(currency || 'usd').toLowerCase();
  return {
    currency: normalized,
    productId: PAYG_PRODUCT_IDS[normalized] || '',
  };
}

async function handlePaygStatus(req, res) {
  const session = requireUserAuth(req, res);
  if (!session) return;
  const user = findUserById(session.userId);
  if (!user) return sendJson(res, 404, { error: 'User not found' });
  const plan = user.plan || DEFAULT_PLAN;
  if (!isPaidPlan(plan)) return sendJson(res, 200, { ok: true, payg: null, plan });
  const payg = (PAYG_ENABLED && !user?.unlimitedUsage) ? computePaygSummary(user.id, plan) : null;
  const pending = Object.fromEntries(Object.entries(pendingPayg || {}).filter(([, entry]) => entry && entry.userId === user.id));
  return sendJson(res, 200, { ok: true, plan, payg, pending });
}

async function handlePaygCheckout(req, res) {
  const session = requireUserAuth(req, res);
  if (!session) return;
  const user = findUserById(session.userId);
  if (!user) return sendJson(res, 404, { error: 'User not found' });
  const plan = user.plan || DEFAULT_PLAN;
  if (!PAYG_ENABLED) return sendJson(res, 503, { error: 'Pay-as-you-go billing is not enabled' });
  if (!isPaidPlan(plan)) return sendJson(res, 400, { error: 'Pay-as-you-go is only available on paid plans' });
  if (!DODO_ENABLED) return sendJson(res, 503, { error: 'Dodo Payments is not configured' });

  const payg = computePaygSummary(user.id, plan);
  if (payg.billableTokens <= 0 || payg.amount <= 0) {
    return sendJson(res, 400, { error: 'No pay-as-you-go usage to bill', payg });
  }

  if (PAYG_MIN_TOKENS > 0 && payg.billableTokens < PAYG_MIN_TOKENS) {
    return sendJson(res, 400, { error: `Minimum overage to bill is ${PAYG_MIN_TOKENS.toLocaleString()} tokens`, payg });
  }

  const { productId, currency } = resolvePaygProduct(payg.currency);
  if (!productId) {
    return sendJson(res, 503, { error: `Pay-as-you-go product ID for ${currency.toUpperCase()} is not configured` });
  }

  const amount = Math.max(MIN_PAYMENT_AMOUNT, Math.ceil((payg.billableTokens * payg.pricePerUnit) / PAYG_UNIT_TOKENS));
    const returnUrl = `${resolveBaseUrl(req)}/settings`;
    const orderId = `payg_${randomUUID()}`;

    // Ensure we have a Dodo customer ID before creating checkout
    const customerId = await ensureDodoCustomer(user);
    if (!customerId) {
      log('Failed to get or create Dodo customer for PAYG', { userId: user.id, email: user.email });
      return sendJson(res, 500, { error: 'Unable to initialize customer for checkout' });
    }

    try {
    const checkoutSession = await dodoRequest('/checkouts', {
      method: 'POST',
      body: {
        product_cart: [{
          product_id: productId,
          quantity: 1,
          amount,
        }],
        customer: {
          customer_id: customerId,  // Pass existing customer ID to prevent duplicates
          email: user.billingEmail || user.email,
          name: user.billingEmail || user.email,
        },
        return_url: returnUrl,
        metadata: {
          type: 'payg',
          orderId,
          userId: String(user.id),
          payg: 'true',
          tokens: String(payg.billableTokens),
          currency: String(currency),
          amount: String(amount),
          month: String(payg.month),
          customerId,
        },
      },
    });

    const sessionId = checkoutSession?.session_id || checkoutSession?.id || '';
    if (!sessionId || !checkoutSession?.checkout_url) throw new Error('Dodo checkout session was not created');

    pendingPayg[sessionId] = {
      userId: user.id,
      orderId,
      tokens: payg.billableTokens,
      amount,
      currency,
      month: payg.month,
      createdAt: new Date().toISOString(),
    };
    await persistPendingPayg();

    return sendJson(res, 200, {
      url: checkoutSession.checkout_url,
      checkoutUrl: checkoutSession.checkout_url,
      sessionId,
      amount,
      currency,
      tokens: payg.billableTokens,
      payg,
    });
  } catch (error) {
    log('payg checkout failed', { error: String(error), userId: user?.id });
    const status = /configured/i.test(String(error?.message || '')) ? 503 : 400;
    return sendJson(res, status, { error: error.message || 'Unable to start pay-as-you-go checkout' });
  }
}

async function handlePaygConfirm(req, res, url) {
  const session = requireUserAuth(req, res);
  if (!session) return;
  const user = findUserById(session.userId);
  if (!user) return sendJson(res, 404, { error: 'User not found' });
  if (!DODO_ENABLED) return sendJson(res, 503, { error: 'Dodo Payments is not configured' });

  const sessionId = (url && url.searchParams && (url.searchParams.get('session_id') || url.searchParams.get('checkout_id') || url.searchParams.get('session'))) || '';
  if (!sessionId) return sendJson(res, 400, { error: 'session_id is required' });

  const existing = processedPayg[sessionId];
  if (existing) {
    if (existing.userId !== user.id) return sendJson(res, 403, { error: 'This pay-as-you-go charge belongs to another user' });

    try {
      await createInvoiceIfMissing(user, 'payg', {
        tokens: existing.tokens,
        amount: existing.amount,
        currency: existing.currency,
        source: {
          provider: 'dodo',
          checkoutId: sessionId,
          orderId: existing.orderId,
          paymentId: existing.paymentId,
        },
      });
    } catch (invoiceError) {
      log('failed to create invoice', { userId: user.id, error: String(invoiceError) });
    }

    return sendJson(res, 200, { ok: true, alreadyApplied: true, payg: computePaygSummary(user.id, user.plan || DEFAULT_PLAN) });
  }

  const pending = pendingPayg[sessionId];
  if (pending && pending.userId !== user.id) {
    return sendJson(res, 403, { error: 'This pay-as-you-go charge belongs to another user' });
  }

  try {
    const checkout = await dodoRequest(`/checkouts/${sessionId}`, { method: 'GET' });
    const paid = isDodoPaymentComplete(checkout?.payment_status || checkout?.status);
    if (!paid) {
      return sendJson(res, 200, { ok: false, pending: true, paymentStatus: checkout?.payment_status || checkout?.status || 'unpaid' });
    }

    const tokens = Number(pending?.tokens || checkout?.metadata?.tokens || 0);
    if (!tokens) return sendJson(res, 400, { error: 'Pay-as-you-go tokens missing' });

    const bucket = ensureTokenUsageBucket(user.id);
    if (!pending?.month || pending.month === bucket.month) {
      bucket.paygBilled = Math.max(0, Number(bucket.paygBilled || 0) + tokens);
      await persistTokenUsage();
    }

    const orderId = pending?.orderId || checkout?.metadata?.orderId || '';
    const currency = String(pending?.currency || checkout?.metadata?.currency || checkout?.currency || '').toLowerCase() || null;

    const amountCandidate = pending?.amount !== undefined && pending?.amount !== null
      ? Number(pending.amount)
      : Number(checkout?.metadata?.amount || checkout?.amount || checkout?.amount_total || checkout?.total_amount || 0);
    const amount = Number.isFinite(amountCandidate) ? Math.max(0, Math.round(amountCandidate)) : null;

    const paymentId = checkout?.payment_id || checkout?.paymentId || checkout?.charge_id || checkout?.chargeId || '';

    processedPayg[sessionId] = {
      userId: user.id,
      orderId: orderId || null,
      paymentId: paymentId || null,
      tokens,
      amount,
      currency,
      completedAt: new Date().toISOString(),
      month: pending?.month || bucket.month,
    };
    delete pendingPayg[sessionId];
    await Promise.all([persistPaygSessions(), persistPendingPayg()]);

    await sendPaymentConfirmationEmail(user, 'payg', {
      tokens,
      amount,
      currency,
    });

    try {
      await createInvoiceIfMissing(user, 'payg', {
        tokens,
        amount,
        currency,
        source: {
          provider: 'dodo',
          checkoutId: sessionId,
          orderId,
          paymentId,
        },
      });
    } catch (invoiceError) {
      log('failed to create invoice', { userId: user.id, error: String(invoiceError) });
    }

    return sendJson(res, 200, { ok: true, tokensBilled: tokens, payg: computePaygSummary(user.id, user.plan || DEFAULT_PLAN) });
  } catch (error) {
    log('payg confirmation failed', { error: String(error), userId: user?.id, sessionId });
    return sendJson(res, 400, { error: error.message || 'Unable to confirm pay-as-you-go payment' });
  }
}

// Handle subscription checkout creation
async function handleSubscriptionCheckout(req, res) {
  const session = requireUserAuth(req, res);
  if (!session) return;
  const user = findUserById(session.userId);
  if (!user) return sendJson(res, 404, { error: 'User not found' });
  if (!DODO_ENABLED) return sendJson(res, 503, { error: 'Dodo Payments is not configured' });

  try {
    const body = await parseJsonBody(req);
    const plan = normalizePlanSelection(body.plan);
    const billingCycle = String(body.billingCycle || 'monthly').toLowerCase();
    const currency = String(body.currency || 'usd').toLowerCase();
    const isInline = Boolean(body.inline);
    // If the client provided a previousPlan from the UI, prefer that for previous-subscription detection.
    const previousPlan = normalizePlanSelection((body && body.previousPlan) || user.plan);
    const previousSubscriptionId = (
      user.dodoSubscriptionId &&
      PAID_PLANS.has(previousPlan || '') &&
      PAID_PLANS.has(plan) &&
      previousPlan !== plan
    ) ? user.dodoSubscriptionId : '';

    if (previousSubscriptionId) {
      log('subscription checkout: detected previous subscription to cancel after confirm', { userId: user.id, previousPlan, previousSubscriptionId, requestedPlan: plan });
    }

    if (!plan) {
      return sendJson(res, 400, { error: 'Plan is required' });
    }

    // Hobby plan is free, no checkout needed
    if (plan === 'hobby') {
      user.plan = plan;
      user.billingStatus = DEFAULT_BILLING_STATUS;
      user.subscriptionRenewsAt = null;
      user.billingCycle = null;
      user.subscriptionCurrency = null;
      await persistUsersDb();
      
      log('hobby plan selected (free)', { userId: user.id, email: user.email, plan });
      return sendJson(res, 200, { ok: true, plan: 'hobby', message: 'Free hobby plan activated' });
    }

    // Validate subscription selection
    if (!validateSubscriptionSelection(plan, billingCycle, currency)) {
      return sendJson(res, 400, { error: 'Invalid plan, billing cycle, or currency combination' });
    }

    const product = resolveSubscriptionProduct(plan, billingCycle, currency);
    if (!product) {
      return sendJson(res, 400, { error: 'Subscription product not available' });
    }

    // Create a unique session ID for this checkout
    const checkoutSessionId = randomUUID();
    
    // Ensure we have a Dodo customer ID before creating checkout to prevent duplicate customers
    const customerId = await ensureDodoCustomer(user);
    if (!customerId) {
      log('Failed to get or create Dodo customer for checkout', { userId: user.id, email: user.email });
      return sendJson(res, 500, { error: 'Unable to initialize customer for checkout' });
    }
    
    log('Using existing Dodo customer for checkout', { userId: user.id, customerId, email: user.email });
 
    // Create checkout session with enhanced metadata for session tracking
    const returnUrl = `${resolveBaseUrl(req)}/apps`;
    const checkoutBody = {
      product_cart: [{
        product_id: product.productId,
        quantity: 1,
      }],
      customer: {
        customer_id: customerId,  // Pass existing customer ID to prevent duplicates
        email: user.billingEmail || user.email,
        name: user.billingEmail || user.email,
      },
      metadata: {
        type: 'subscription',
        orderId: String(checkoutSessionId),
        userId: String(user.id),
        sessionId: String(checkoutSessionId),
        plan: String(plan),
        billingCycle: String(billingCycle),
        currency: String(currency),
        amount: String(product.price),
        inline: String(isInline),
        customerId: customerId,  // Track which customer ID was used
      },
      settings: {
        allow_payment_methods: ['card'],
        redirect_immediately: !isInline,
      },
    };
    checkoutBody.return_url = returnUrl;
    const checkoutSession = await dodoRequest('/checkouts', {
      method: 'POST',
      body: checkoutBody,
    });

    const sessionId = checkoutSession?.session_id || checkoutSession?.id || '';
    if (!sessionId || !checkoutSession?.checkout_url) {
      throw new Error('Dodo checkout session was not created');
    }

    // Store pending subscription with enhanced tracking
    pendingSubscriptions[sessionId] = {
      userId: user.id,
      orderId: checkoutSessionId,
      plan: plan,
      billingCycle: billingCycle,
      currency: currency,
      productId: product.productId,
      price: product.price,
      checkoutSessionId,
      previousSubscriptionId,
      createdAt: new Date().toISOString(),
      inline: isInline,
    };
    await persistPendingSubscriptions();

    // Return appropriate checkout URL based on request type
    const response = {
      sessionId,
      plan,
      billingCycle,
      currency,
      price: product.price,
    };

    if (isInline) {
      // For inline checkout, return the same URL but mark it as inline
      response.inlineCheckoutUrl = checkoutSession.checkout_url;
      response.checkoutUrl = checkoutSession.checkout_url; // Keep for backward compatibility
    } else {
      response.checkoutUrl = checkoutSession.checkout_url;
      response.url = checkoutSession.checkout_url;
    }

    return sendJson(res, 200, response);
  } catch (error) {
    log('subscription checkout failed', { error: String(error), userId: user?.id });
    const status = /configured/i.test(String(error?.message || '')) ? 503 : 400;
    return sendJson(res, status, { error: error.message || 'Unable to start subscription checkout' });
  }
}

// Handle subscription confirmation
async function handleSubscriptionConfirm(req, res, url) {
  const session = requireUserAuth(req, res);
  if (!session) return;
  const user = findUserById(session.userId);
  if (!user) return sendJson(res, 404, { error: 'User not found' });
  if (!DODO_ENABLED) return sendJson(res, 503, { error: 'Dodo Payments is not configured' });

  const sessionId = (url && url.searchParams && (url.searchParams.get('session_id') || url.searchParams.get('checkout_id') || url.searchParams.get('session'))) || '';
  if (!sessionId) return sendJson(res, 400, { error: 'session_id is required' });

  let processed = processedSubscriptions[sessionId];
  let processedKey = sessionId;

  if (!processed) {
    for (const [key, value] of Object.entries(processedSubscriptions)) {
      if (value?.checkoutSessionId === sessionId || value?.orderId === sessionId) {
        processed = value;
        processedKey = key;
        break;
      }
    }
  }

  if (processed) {
    if (processed.userId !== user.id) return sendJson(res, 403, { error: 'This subscription belongs to another user' });

    try {
      await createInvoiceIfMissing(user, 'subscription', {
        plan: processed.plan,
        billingCycle: processed.billingCycle,
        currency: processed.currency,
        amount: processed.amount,
        source: {
          provider: 'dodo',
          checkoutId: processedKey,
          orderId: processed.orderId || processed.checkoutSessionId,
          paymentId: processed.paymentId,
          subscriptionId: processed.subscriptionId,
        },
      });
    } catch (invoiceError) {
      log('failed to create invoice', { userId: user.id, error: String(invoiceError) });
    }

    const accountData = await serializeAccount(user);
    return sendJson(res, 200, {
      ok: true,
      alreadyApplied: true,
      plan: processed.plan,
      billingCycle: processed.billingCycle,
      currency: processed.currency,
      account: accountData,
    });
  }

  // Enhanced session tracking - try multiple ways to find the pending subscription
  let pending = pendingSubscriptions[sessionId];
  let pendingKey = sessionId;

  // If direct lookup fails, search by checkoutSessionId (for inline payments)
  if (!pending) {
    for (const [key, value] of Object.entries(pendingSubscriptions)) {
      if (value.checkoutSessionId === sessionId || value.orderId === sessionId) {
        pending = value;
        pendingKey = key;
        break;
      }
    }
  }

  if (!pending) return sendJson(res, 404, { error: 'Subscription not found or already processed' });
  if (pending.userId !== user.id) return sendJson(res, 403, { error: 'This subscription belongs to another user' });

  try {
    // Get checkout details from Dodo
    const checkout = await dodoRequest(`/checkouts/${pendingKey}`, { method: 'GET' });
    const paid = isDodoPaymentComplete(checkout?.payment_status || checkout?.status);
    if (!paid) {
      return sendJson(res, 200, { ok: false, pending: true, paymentStatus: checkout?.payment_status || checkout?.status || 'unpaid' });
    }

    // Activate subscription
    const previousSubscriptionId = pending.previousSubscriptionId;

    user.plan = pending.plan;
    user.billingStatus = DEFAULT_BILLING_STATUS;
    user.billingCycle = pending.billingCycle;
    user.subscriptionCurrency = pending.currency;
    user.subscriptionRenewsAt = computeRenewalDate(pending.billingCycle);
    
    // Validate customer ID consistency to prevent duplicate customer issues
    const checkoutCustomerId = checkout?.customer_id;
    const storedCustomerId = user.dodoCustomerId;
    
    if (checkoutCustomerId && storedCustomerId && checkoutCustomerId !== storedCustomerId) {
      log('WARNING: Checkout returned different customer_id than stored', {
        userId: user.id,
        email: user.email,
        storedCustomerId: storedCustomerId,
        checkoutCustomerId: checkoutCustomerId,
        subscriptionId: checkout?.subscription_id,
        pendingCustomerId: pending.metadata?.customerId
      });
      
      // Check if we should use the new customer ID (if it has subscriptions)
      user.dodoCustomerId = checkoutCustomerId;
    } else if (checkoutCustomerId) {
      user.dodoCustomerId = checkoutCustomerId;
    }

    if (checkout?.subscription_id) {
      user.dodoSubscriptionId = checkout.subscription_id;
    }

    if (previousSubscriptionId && previousSubscriptionId !== user.dodoSubscriptionId) {
      const cancelTarget = {
        ...user,
        dodoSubscriptionId: previousSubscriptionId,
      };
      await cancelDodoSubscription(cancelTarget, 'paid_plan_switch', { clearOnFailure: true });
    }

    await persistUsersDb();

    const paymentId = checkout?.payment_id || checkout?.paymentId || checkout?.charge_id || checkout?.chargeId || '';
    const subscriptionId = checkout?.subscription_id || user.dodoSubscriptionId || '';

    // Mark subscription as processed
    processedSubscriptions[pendingKey] = {
      userId: user.id,
      orderId: pending.orderId || pending.checkoutSessionId || null,
      checkoutSessionId: pending.checkoutSessionId || null,
      subscriptionId: subscriptionId || null,
      paymentId: paymentId || null,
      plan: pending.plan,
      billingCycle: pending.billingCycle,
      currency: pending.currency,
      amount: pending.price,
      completedAt: new Date().toISOString(),
    };
    
    // Clean up pending subscription
    delete pendingSubscriptions[pendingKey];
    await Promise.all([persistPendingSubscriptions(), persistProcessedSubscriptions()]);

    // Track conversion and financial
    trackConversion('paid', req);
    trackFinancial(pending.price / 100, pending.plan); // Convert from cents to dollars

    await sendPaymentConfirmationEmail(user, 'subscription', {
      plan: pending.plan,
      billingCycle: pending.billingCycle,
      currency: pending.currency,
      amount: pending.price,
    });

    try {
      await createInvoiceIfMissing(user, 'subscription', {
        plan: pending.plan,
        billingCycle: pending.billingCycle,
        currency: pending.currency,
        amount: pending.price,
        source: {
          provider: 'dodo',
          checkoutId: pendingKey,
          orderId: pending.orderId || pending.checkoutSessionId,
          paymentId,
          subscriptionId,
        },
      });
    } catch (invoiceError) {
      log('failed to create invoice', { userId: user.id, error: String(invoiceError) });
    }

    log('subscription activated', { 
      userId: user.id, 
      email: user.email, 
      plan: pending.plan, 
      billingCycle: pending.billingCycle, 
      currency: pending.currency,
      sessionId: pendingKey,
      checkoutSessionId: sessionId 
    });

    const accountData = await serializeAccount(user);
    return sendJson(res, 200, {
      ok: true,
      plan: pending.plan,
      billingCycle: pending.billingCycle,
      currency: pending.currency,
      account: accountData,
      sessionId: pendingKey,
      checkoutSessionId: sessionId 
    });
  } catch (error) {
    log('subscription confirmation failed', { 
      error: String(error), 
      userId: user?.id, 
      sessionId,
      pendingKey 
    });
    return sendJson(res, 400, { error: error.message || 'Unable to confirm subscription' });
  }
}

// Get subscription status
async function handleSubscriptionStatus(req, res) {
  const session = requireUserAuth(req, res);
  if (!session) return;
  const user = findUserById(session.userId);
  if (!user) return sendJson(res, 404, { error: 'User not found' });

  const isPaidPlan = PAID_PLANS.has(user.plan || '');
  const hasActiveSubscription = isPaidPlan && user.billingStatus === DEFAULT_BILLING_STATUS;

  let paymentMethod = null;
  
  if (user.dodoSubscriptionId) {
    try {
      const subscription = await dodoRequest(`/subscriptions/${user.dodoSubscriptionId}`, {
        method: 'GET',
      });
      
      if (subscription && subscription.payment_method) {
        const pm = subscription.payment_method;
        paymentMethod = {
          brand: pm.card?.brand || pm.payment_method || 'Card',
          last4: pm.card?.last4 || pm.card?.last_digits || '',
          expiresAt: pm.card?.expiry || pm.card?.expires_at || '',
        };
      }
    } catch (error) {
      console.error('[SubscriptionStatus] Failed to fetch subscription details:', error.message);
    }
  }

  return sendJson(res, 200, {
    plan: user.plan || DEFAULT_PLAN,
    hasActiveSubscription,
    billingStatus: user.billingStatus,
    billingCycle: user.billingCycle,
    currency: user.subscriptionCurrency,
    renewsAt: user.subscriptionRenewsAt,
    paymentMethod,
  });
}

// Cancel subscription
async function handleSubscriptionCancel(req, res) {
  const session = requireUserAuth(req, res);
  if (!session) return;
  const user = findUserById(session.userId);
  if (!user) return sendJson(res, 404, { error: 'User not found' });

  if (!PAID_PLANS.has(user.plan || '')) {
    return sendJson(res, 400, { error: 'No active subscription to cancel' });
  }

  // Schedule cancellation at period end
  if (user.dodoSubscriptionId) {
    const cancelSuccess = await cancelDodoSubscription(user, 'manual_cancel', { 
      mode: 'at_period_end' 
    });
    
    if (cancelSuccess) {
      // Mark as scheduled for cancellation
      user.scheduledPlanChange = {
        fromPlan: user.plan,
        toPlan: 'hobby',
        scheduledAt: new Date().toISOString(),
      };
      user.billingStatus = 'cancellation_scheduled';
      // Keep subscriptionRenewsAt until actual cancellation
      log('Scheduled subscription cancellation at period end (manual)', { 
        userId: user.id, 
        subscriptionId: user.dodoSubscriptionId,
        renewsAt: user.subscriptionRenewsAt 
      });
    } else {
      return sendJson(res, 400, { error: 'Failed to schedule subscription cancellation' });
    }
  }

  await persistUsersDb();

  const accountData = await serializeAccount(user);
  return sendJson(res, 200, {
    ok: true,
    message: 'Subscription will be cancelled at the end of your billing period. You will continue to have access until then.',
    account: accountData,
  });
}

async function handleDodoWebhook(req, res) {
  try {
    const DODO_WEBHOOK_KEY = process.env.DODO_PAYMENTS_WEBHOOK_KEY || '';
    const rawBody = await new Promise((resolve, reject) => {
      let body = '';
      req.on('data', (chunk) => { body += chunk; });
      req.on('end', () => resolve(body));
      req.on('error', reject);
    });

    const signature = req.headers['dodo-signature'] || '';
    
    if (DODO_WEBHOOK_KEY && signature) {
      const expectedSignature = `sha256=${require('crypto').createHmac('sha256', DODO_WEBHOOK_KEY).update(rawBody).digest('hex')}`;
      if (!require('crypto').timingSafeEqual(Buffer.from(signature), Buffer.from(expectedSignature))) {
        log('Dodo webhook signature verification failed', { signature });
        return sendJson(res, 401, { error: 'Invalid signature' });
      }
    } else if (DODO_WEBHOOK_KEY) {
      log('Dodo webhook missing signature', { hasKey: !!DODO_WEBHOOK_KEY });
      return sendJson(res, 401, { error: 'Missing signature' });
    }

    const event = JSON.parse(rawBody);
    log('Dodo webhook received', { type: event.type, id: event.id });

    // Idempotency check - prevent duplicate processing
    const eventId = event.id || event.data?.id;
    if (eventId && isWebhookProcessed(eventId)) {
      log('Dodo webhook already processed, skipping', { eventId, type: event.type });
      return sendJson(res, 200, { received: true, duplicate: true });
    }

    switch (event.type) {
      case 'payment_succeeded':
      case 'subscription_payment_succeeded':
        await handlePaymentSucceeded(event);
        break;
      case 'payment_failed':
        await handlePaymentFailed(event);
        break;
      case 'payment_cancelled':
        await handlePaymentCancelled(event);
        break;
      case 'payment_processing':
        await handlePaymentProcessing(event);
        break;
      case 'payment_dispute_created':
        await handlePaymentDisputeCreated(event);
        break;
      case 'dispute_accepted':
        await handleDisputeAccepted(event);
        break;
      case 'dispute_cancelled':
        await handleDisputeCancelled(event);
        break;
      case 'dispute_challenged':
        await handleDisputeChallenged(event);
        break;
      case 'dispute_expired':
        await handleDisputeExpired(event);
        break;
      case 'dispute_lost':
        await handleDisputeLost(event);
        break;
      case 'dispute_won':
        await handleDisputeWon(event);
        break;
      case 'charge_refunded':
        await handleChargeRefunded(event);
        break;
      case 'refund_failed':
        await handleRefundFailed(event);
        break;
      case 'subscription_canceled':
        await handleSubscriptionCanceled(event);
        break;
      case 'subscription_payment_failed':
        await handleSubscriptionPaymentFailed(event);
        break;
      case 'subscription_active':
        await handleSubscriptionActive(event);
        break;
      case 'subscription_expired':
        await handleSubscriptionExpired(event);
        break;
      case 'subscription_on_hold':
        await handleSubscriptionOnHold(event);
        break;
      case 'subscription_plan_changed':
        await handleSubscriptionPlanChanged(event);
        break;
      case 'subscription_renewed':
        await handleSubscriptionRenewed(event);
        break;
      case 'subscription_updated':
        await handleSubscriptionUpdated(event);
        break;
      default:
        log('Unhandled Dodo webhook event', { type: event.type });
    }

    // Mark as processed for idempotency
    const eventId = event.id || event.data?.id;
    if (eventId) {
      await markWebhookProcessed(eventId, event.type);
    }

    sendJson(res, 200, { received: true });
  } catch (error) {
    log('Dodo webhook error', { error: String(error), stack: error.stack });
    // Return 500 to trigger webhook retry from payment provider
    // This ensures failed payments aren't silently lost
    sendJson(res, 500, { error: 'Webhook processing failed', received: false });
  }
}

async function handlePaymentSucceeded(event) {
  const data = event?.data?.object || event?.data || {};
  const metadata = data?.metadata || {};

  const paymentId = data.payment_id || data.paymentId || data.charge_id || data.chargeId || data.id || event.id || '';
  const checkoutId = data.checkout_id || data.checkoutId || data.checkout_session_id || data.checkoutSessionId || '';
  const subscriptionId = data.subscription_id || data.subscriptionId || metadata.subscriptionId || '';
  const orderId = metadata.orderId || metadata.sessionId || metadata.checkoutSessionId || '';

  const inferredType = String(metadata.type || '').toLowerCase()
    || (metadata.payg === 'true' ? 'payg' : '')
    || (String(event?.type || '').toLowerCase().includes('subscription') ? 'subscription' : '');

  let userId = metadata.userId || '';
  let user = userId ? findUserById(userId) : null;

  if (!user && subscriptionId) {
    user = usersDb.find(u => u.dodoSubscriptionId === subscriptionId);
    userId = user?.id || '';
  }

  if (!userId || !user) {
    log('payment_succeeded: user not found', { userId, subscriptionId, eventId: event.id });
    return;
  }

  const findKeyByOrderId = (store, wanted) => {
    if (!wanted) return '';
    for (const [key, value] of Object.entries(store || {})) {
      if (value?.orderId && value.orderId === wanted) return key;
      if (value?.checkoutSessionId && value.checkoutSessionId === wanted) return key;
    }
    return '';
  };

  const parseAmount = (value) => {
    const num = Number(value);
    return Number.isFinite(num) ? Math.max(0, Math.round(num)) : null;
  };

  const ensureInvoice = async (type, details) => {
    try {
      await createInvoiceIfMissing(user, type, details);
    } catch (invoiceError) {
      log('failed to create invoice', { userId: user.id, type, error: String(invoiceError) });
    }
  };

  if (inferredType === 'topup') {
    const pendingKey = (checkoutId && pendingTopups?.[checkoutId] ? checkoutId : '') || findKeyByOrderId(pendingTopups, orderId);
    const pending = pendingKey ? pendingTopups[pendingKey] : null;

    const tokens = Number(metadata.tokens || pending?.tokens || 0);
    if (!tokens) {
      log('payment_succeeded: top-up missing tokens', { userId, eventId: event.id, checkoutId, orderId });
      return;
    }

    const amount = parseAmount(metadata.amount || pending?.amount || data.amount || data.amount_total || data.total_amount);
    const currency = String(metadata.currency || pending?.currency || data.currency || '').toLowerCase() || null;
    const tier = metadata.tier || pending?.tier || null;

    if (pendingKey && processedTopups[pendingKey]) {
      await ensureInvoice('topup', {
        tokens,
        amount: processedTopups[pendingKey].amount ?? amount,
        currency: processedTopups[pendingKey].currency ?? currency,
        tier: processedTopups[pendingKey].tier ?? tier,
        source: {
          provider: 'dodo',
          checkoutId: pendingKey,
          orderId: processedTopups[pendingKey].orderId || orderId,
          paymentId: processedTopups[pendingKey].paymentId || paymentId,
          eventId: event.id,
        },
      });
      return;
    }

    const bucket = ensureTokenUsageBucket(userId);
    bucket.addOns = Math.max(0, Number(bucket.addOns || 0) + tokens);
    await persistTokenUsage();

    if (pendingKey) {
      processedTopups[pendingKey] = {
        userId: user.id,
        orderId: (pending?.orderId || orderId) || null,
        paymentId: paymentId || null,
        tokens,
        tier: tier || null,
        amount,
        currency,
        completedAt: new Date().toISOString(),
      };
      delete pendingTopups[pendingKey];
      await Promise.all([persistTopupSessions(), persistPendingTopups()]);
    }

    await ensureInvoice('topup', {
      tokens,
      amount,
      currency,
      tier,
      source: {
        provider: 'dodo',
        checkoutId: pendingKey || checkoutId,
        orderId,
        paymentId,
        eventId: event.id,
      },
    });

    log('payment_succeeded: top-up processed via webhook', { userId, tokens, eventId: event.id, checkoutId: pendingKey || checkoutId });
    return;
  }

  if (inferredType === 'payment_method_save') {
    log('payment_method_save: payment method added via webhook', { userId, eventId: event.id, checkoutId });
    return;
  }

  if (inferredType === 'subscription') {
    const pendingKey = (checkoutId && pendingSubscriptions?.[checkoutId] ? checkoutId : '') || findKeyByOrderId(pendingSubscriptions, orderId);
    const pending = pendingKey ? pendingSubscriptions[pendingKey] : null;

    if (pendingKey && processedSubscriptions[pendingKey]) {
      await ensureInvoice('subscription', {
        plan: processedSubscriptions[pendingKey].plan,
        billingCycle: processedSubscriptions[pendingKey].billingCycle,
        currency: processedSubscriptions[pendingKey].currency,
        amount: processedSubscriptions[pendingKey].amount,
        source: {
          provider: 'dodo',
          checkoutId: pendingKey,
          orderId: processedSubscriptions[pendingKey].orderId || orderId,
          paymentId: processedSubscriptions[pendingKey].paymentId || paymentId,
          subscriptionId: processedSubscriptions[pendingKey].subscriptionId || subscriptionId,
          eventId: event.id,
        },
      });
      return;
    }

    if (pending) {
      user.plan = pending.plan || metadata.plan || user.plan;
      user.billingStatus = DEFAULT_BILLING_STATUS;
      user.billingCycle = pending.billingCycle || metadata.billingCycle || user.billingCycle;
      user.subscriptionCurrency = pending.currency || metadata.currency || user.subscriptionCurrency;
      if (user.billingCycle) {
        user.subscriptionRenewsAt = computeRenewalDate(user.billingCycle);
      }

      if (subscriptionId) {
        user.dodoSubscriptionId = subscriptionId;
      }

      await persistUsersDb();

      processedSubscriptions[pendingKey] = {
        userId: user.id,
        orderId: pending.orderId || pending.checkoutSessionId || orderId || null,
        checkoutSessionId: pending.checkoutSessionId || null,
        subscriptionId: subscriptionId || null,
        paymentId: paymentId || null,
        plan: user.plan,
        billingCycle: user.billingCycle,
        currency: user.subscriptionCurrency,
        amount: pending.price || parseAmount(metadata.amount || data.amount || data.amount_total || data.total_amount) || null,
        completedAt: new Date().toISOString(),
      };

      delete pendingSubscriptions[pendingKey];
      await Promise.all([persistPendingSubscriptions(), persistProcessedSubscriptions()]);

      await ensureInvoice('subscription', {
        plan: user.plan,
        billingCycle: user.billingCycle,
        currency: user.subscriptionCurrency,
        amount: processedSubscriptions[pendingKey].amount,
        source: {
          provider: 'dodo',
          checkoutId: pendingKey,
          orderId: processedSubscriptions[pendingKey].orderId || orderId,
          paymentId,
          subscriptionId,
          eventId: event.id,
        },
      });

      log('payment_succeeded: subscription activated via webhook', { userId: user.id, eventId: event.id, subscriptionId });
      return;
    }

    // Subscription renewal (or webhook delivered after pending record expired)
    const amount = parseAmount(metadata.amount || data.amount || data.amount_total || data.total_amount);
    const currency = String(metadata.currency || data.currency || user.subscriptionCurrency || '').toLowerCase() || null;

    await ensureInvoice('subscription', {
      plan: metadata.plan || user.plan,
      billingCycle: metadata.billingCycle || user.billingCycle,
      currency,
      amount,
      source: {
        provider: 'dodo',
        paymentId,
        eventId: event.id,
        subscriptionId: subscriptionId || user.dodoSubscriptionId,
      },
    });

    log('payment_succeeded: subscription payment recorded via webhook', { userId: user.id, eventId: event.id, subscriptionId: subscriptionId || user.dodoSubscriptionId });
    return;
  }

  if (inferredType === 'payg') {
    const pendingKey = (checkoutId && pendingPayg?.[checkoutId] ? checkoutId : '') || findKeyByOrderId(pendingPayg, orderId);
    const pending = pendingKey ? pendingPayg[pendingKey] : null;

    const tokens = Number(metadata.tokens || pending?.tokens || 0);
    const amount = parseAmount(metadata.amount || pending?.amount || data.amount || data.amount_total || data.total_amount);
    const currency = String(metadata.currency || pending?.currency || data.currency || '').toLowerCase() || null;

    await ensureInvoice('payg', {
      tokens,
      amount,
      currency,
      source: {
        provider: 'dodo',
        checkoutId: pendingKey || checkoutId,
        orderId,
        paymentId,
        eventId: event.id,
      },
    });

    log('payment_succeeded: payg invoice recorded via webhook', { userId: user.id, eventId: event.id, checkoutId: pendingKey || checkoutId });
    return;
  }

  log('payment_succeeded: unsupported payment type', { userId, type: metadata.type, eventId: event.id });
}

async function handlePaymentFailed(event) {
  const data = event?.data?.object || event?.data || {};
  const metadata = data?.metadata || {};
  const userId = metadata.userId;
  
  if (!userId) {
    log('payment_failed: no userId in metadata', { eventId: event.id });
    return;
  }

  const user = findUserById(userId);
  if (!user) {
    log('payment_failed: user not found', { userId, eventId: event.id });
    return;
  }

  const paymentType = metadata.type;

  if (paymentType === 'subscription' || user.dodoSubscriptionId) {
    await cancelSubscriptionForUser(user, 'payment_failed');
    log('payment_failed: subscription cancelled', { userId, email: user.email, eventId: event.id });

    if (user.email) {
      await sendPaymentFailedEmail(user, data);
    }
  }
}

async function handlePaymentDisputeCreated(event) {
  const data = event?.data?.object || event?.data || {};
  const metadata = data?.metadata || {};
  const userId = metadata.userId;
  
  if (!userId) {
    log('payment_dispute_created: no userId in metadata', { eventId: event.id });
    return;
  }

  const user = findUserById(userId);
  if (!user) {
    log('payment_dispute_created: user not found', { userId, eventId: event.id });
    return;
  }

  await cancelSubscriptionForUser(user, 'dispute_created');
  log('payment_dispute_created: subscription cancelled due to dispute', { userId, email: user.email, eventId: event.id, disputeId: data.dispute_id });

  if (user.email) {
    await sendPaymentDisputeCreatedEmail(user, data);
  }
}

async function handleChargeRefunded(event) {
  const data = event?.data?.object || event?.data || {};
  const metadata = data?.metadata || {};
  const userId = metadata.userId;
  
  if (!userId) {
    log('charge_refunded: no userId in metadata', { eventId: event.id, amount: data.amount });
    return;
  }

  const user = findUserById(userId);
  if (!user) {
    log('charge_refunded: user not found', { userId, eventId: event.id });
    return;
  }

  const paymentType = metadata.type;
  log('charge_refunded: refund processed', { userId, paymentType, amount: data.amount, eventId: event.id });

  if (user.email) {
    await sendChargeRefundedEmail(user, data);
  }
}

async function handleSubscriptionCanceled(event) {
  const { data } = event;
  const subscriptionId = data.id || data.subscription_id;
  
  if (!subscriptionId) {
    log('subscription_canceled: no subscription ID', { eventId: event.id });
    return;
  }

  const user = usersDb.find(u => u.dodoSubscriptionId === subscriptionId);
  if (!user) {
    log('subscription_canceled: user not found for subscription', { subscriptionId, eventId: event.id });
    return;
  }

  // Check if this was a planned downgrade
  const wasScheduledDowngrade = user.scheduledPlanChange && user.scheduledPlanChange.toPlan === 'hobby';

  // Downgrade user to hobby plan
  user.plan = 'hobby';
  user.billingStatus = 'cancelled';
  user.dodoSubscriptionId = '';
  user.subscriptionRenewsAt = null;
  user.billingCycle = null;
  user.subscriptionCurrency = null;
  
  // Clear scheduled plan change if it exists
  if (user.scheduledPlanChange) {
    delete user.scheduledPlanChange;
  }
  
  await persistUsersDb();

  log('subscription_canceled: user downgraded to hobby', { 
    userId: user.id, 
    email: user.email, 
    subscriptionId, 
    eventId: event.id,
    wasScheduledDowngrade,
  });

  if (user.email) {
    await sendSubscriptionCancelledEmail(user, data);
  }
}

async function handleSubscriptionPaymentFailed(event) {
  const { data } = event;
  const subscriptionId = data.id || data.subscription_id;
  
  if (!subscriptionId) {
    log('subscription_payment_failed: no subscription ID', { eventId: event.id });
    return;
  }

  const user = usersDb.find(u => u.dodoSubscriptionId === subscriptionId);
  if (!user) {
    log('subscription_payment_failed: user not found for subscription', { subscriptionId, eventId: event.id });
    return;
  }

  await cancelSubscriptionForUser(user, 'subscription_payment_failed');
  log('subscription_payment_failed: subscription cancelled due to payment failure', { userId: user.id, email: user.email, subscriptionId, eventId: event.id });

  if (user.email) {
    await sendSubscriptionPaymentFailedEmail(user, data);
  }
}

async function cancelSubscriptionForUser(user, reason) {
  if (user.dodoSubscriptionId) {
    await cancelDodoSubscription(user, reason, { clearOnFailure: true });
  }

  user.plan = 'hobby';
  user.billingStatus = 'cancelled';
  user.subscriptionRenewsAt = '';
  await persistUsersDb();
}

async function handlePaymentCancelled(event) {
  const data = event?.data?.object || event?.data || {};
  const metadata = data?.metadata || {};
  const userId = metadata.userId;

  if (!userId) {
    log('payment_cancelled: no userId in metadata', { eventId: event.id });
    return;
  }

  const user = findUserById(userId);
  if (!user) {
    log('payment_cancelled: user not found', { userId, eventId: event.id });
    return;
  }

  const paymentType = metadata.type;
  log('payment_cancelled: payment cancelled', { userId, paymentType, eventId: event.id });

  if (user.email) {
    await sendPaymentCancelledEmail(user, data);
  }
}

async function handlePaymentProcessing(event) {
  const data = event?.data?.object || event?.data || {};
  const metadata = data?.metadata || {};
  const userId = metadata.userId;

  if (!userId) {
    log('payment_processing: no userId in metadata', { eventId: event.id });
    return;
  }

  const user = findUserById(userId);
  if (!user) {
    log('payment_processing: user not found', { userId, eventId: event.id });
    return;
  }

  log('payment_processing: payment processing', { userId, eventId: event.id });
}

async function handleDisputeAccepted(event) {
  const data = event?.data?.object || event?.data || {};
  const metadata = data?.metadata || {};
  const userId = metadata.userId;

  if (!userId) {
    log('dispute_accepted: no userId in metadata', { eventId: event.id });
    return;
  }

  const user = findUserById(userId);
  if (!user) {
    log('dispute_accepted: user not found', { userId, eventId: event.id });
    return;
  }

  log('dispute_accepted: dispute accepted by bank', { userId, disputeId: data.dispute_id, eventId: event.id });

  if (user.email) {
    await sendDisputeAcceptedEmail(user, data);
  }
}

async function handleDisputeCancelled(event) {
  const data = event?.data?.object || event?.data || {};
  const metadata = data?.metadata || {};
  const userId = metadata.userId;

  if (!userId) {
    log('dispute_cancelled: no userId in metadata', { eventId: event.id });
    return;
  }

  const user = findUserById(userId);
  if (!user) {
    log('dispute_cancelled: user not found', { userId, eventId: event.id });
    return;
  }

  log('dispute_cancelled: dispute cancelled', { userId, disputeId: data.dispute_id, eventId: event.id });

  if (user.email) {
    await sendDisputeCancelledEmail(user, data);
  }
}

async function handleDisputeChallenged(event) {
  const data = event?.data?.object || event?.data || {};
  const metadata = data?.metadata || {};
  const userId = metadata.userId;

  if (!userId) {
    log('dispute_challenged: no userId in metadata', { eventId: event.id });
    return;
  }

  const user = findUserById(userId);
  if (!user) {
    log('dispute_challenged: user not found', { userId, eventId: event.id });
    return;
  }

  log('dispute_challenged: dispute challenged', { userId, disputeId: data.dispute_id, eventId: event.id });

  if (user.email) {
    await sendDisputeChallengedEmail(user, data);
  }
}

async function handleDisputeExpired(event) {
  const data = event?.data?.object || event?.data || {};
  const metadata = data?.metadata || {};
  const userId = metadata.userId;

  if (!userId) {
    log('dispute_expired: no userId in metadata', { eventId: event.id });
    return;
  }

  const user = findUserById(userId);
  if (!user) {
    log('dispute_expired: user not found', { userId, eventId: event.id });
    return;
  }

  log('dispute_expired: dispute expired', { userId, disputeId: data.dispute_id, eventId: event.id });

  if (user.email) {
    await sendDisputeExpiredEmail(user, data);
  }
}

async function handleDisputeLost(event) {
  const data = event?.data?.object || event?.data || {};
  const metadata = data?.metadata || {};
  const userId = metadata.userId;

  if (!userId) {
    log('dispute_lost: no userId in metadata', { eventId: event.id });
    return;
  }

  const user = findUserById(userId);
  if (!user) {
    log('dispute_lost: user not found', { userId, eventId: event.id });
    return;
  }

  log('dispute_lost: dispute lost', { userId, disputeId: data.dispute_id, eventId: event.id });

  if (user.email) {
    await sendDisputeLostEmail(user, data);
  }
}

async function handleDisputeWon(event) {
  const data = event?.data?.object || event?.data || {};
  const metadata = data?.metadata || {};
  const userId = metadata.userId;

  if (!userId) {
    log('dispute_won: no userId in metadata', { eventId: event.id });
    return;
  }

  const user = findUserById(userId);
  if (!user) {
    log('dispute_won: user not found', { userId, eventId: event.id });
    return;
  }

  log('dispute_won: dispute won', { userId, disputeId: data.dispute_id, eventId: event.id });

  if (user.email) {
    await sendDisputeWonEmail(user, data);
  }
}

async function handleRefundFailed(event) {
  const data = event?.data?.object || event?.data || {};
  const metadata = data?.metadata || {};
  const userId = metadata.userId;

  if (!userId) {
    log('refund_failed: no userId in metadata', { eventId: event.id });
    return;
  }

  const user = findUserById(userId);
  if (!user) {
    log('refund_failed: user not found', { userId, eventId: event.id });
    return;
  }

  log('refund_failed: refund failed', { userId, amount: data.amount, eventId: event.id });

  if (user.email) {
    await sendRefundFailedEmail(user, data);
  }
}

async function handleSubscriptionActive(event) {
  const { data } = event;
  const subscriptionId = data.id || data.subscription_id;

  if (!subscriptionId) {
    log('subscription_active: no subscription ID', { eventId: event.id });
    return;
  }

  const user = usersDb.find(u => u.dodoSubscriptionId === subscriptionId);
  if (!user) {
    log('subscription_active: user not found for subscription', { subscriptionId, eventId: event.id });
    return;
  }

  user.billingStatus = 'active';
  if (data.renews_at || data.current_period_end) {
    user.subscriptionRenewsAt = data.renews_at || data.current_period_end;
  }
  await persistUsersDb();

  log('subscription_active: subscription activated', { userId: user.id, subscriptionId, eventId: event.id });

  if (user.email) {
    await sendSubscriptionActiveEmail(user, data);
  }
}

async function handleSubscriptionExpired(event) {
  const { data } = event;
  const subscriptionId = data.id || data.subscription_id;

  if (!subscriptionId) {
    log('subscription_expired: no subscription ID', { eventId: event.id });
    return;
  }

  const user = usersDb.find(u => u.dodoSubscriptionId === subscriptionId);
  if (!user) {
    log('subscription_expired: user not found for subscription', { subscriptionId, eventId: event.id });
    return;
  }

  user.plan = 'hobby';
  user.billingStatus = 'expired';
  user.dodoSubscriptionId = '';
  user.subscriptionRenewsAt = '';
  await persistUsersDb();

  log('subscription_expired: subscription expired', { userId: user.id, subscriptionId, eventId: event.id });

  if (user.email) {
    await sendSubscriptionExpiredEmail(user, data);
  }
}

async function handleSubscriptionOnHold(event) {
  const { data } = event;
  const subscriptionId = data.id || data.subscription_id;

  if (!subscriptionId) {
    log('subscription_on_hold: no subscription ID', { eventId: event.id });
    return;
  }

  const user = usersDb.find(u => u.dodoSubscriptionId === subscriptionId);
  if (!user) {
    log('subscription_on_hold: user not found for subscription', { subscriptionId, eventId: event.id });
    return;
  }

  user.billingStatus = 'on_hold';
  await persistUsersDb();

  log('subscription_on_hold: subscription on hold', { userId: user.id, subscriptionId, eventId: event.id });

  if (user.email) {
    await sendSubscriptionOnHoldEmail(user, data);
  }
}

async function handleSubscriptionPlanChanged(event) {
  const { data } = event;
  const subscriptionId = data.id || data.subscription_id;

  if (!subscriptionId) {
    log('subscription_plan_changed: no subscription ID', { eventId: event.id });
    return;
  }

  const user = usersDb.find(u => u.dodoSubscriptionId === subscriptionId);
  if (!user) {
    log('subscription_plan_changed: user not found for subscription', { subscriptionId, eventId: event.id });
    return;
  }

  // Determine the new plan from the subscription data
  const newPlan = data.plan || data.metadata?.plan;
  const oldPlan = user.plan;
  
  if (newPlan && USER_PLANS.includes(newPlan.toLowerCase())) {
    user.plan = newPlan.toLowerCase();
    user.billingStatus = 'active';
    
    // Update billing cycle if available
    if (data.billing_cycle) {
      user.billingCycle = data.billing_cycle;
    }
    
    // Update currency if available
    if (data.currency) {
      user.subscriptionCurrency = data.currency;
    }
    
    // Update renewal date
    if (data.renews_at || data.current_period_end) {
      user.subscriptionRenewsAt = data.renews_at || data.current_period_end;
    }
    
    // Clear any scheduled plan change
    if (user.scheduledPlanChange) {
      delete user.scheduledPlanChange;
    }
  }
  
  await persistUsersDb();

  log('subscription_plan_changed: plan changed', { 
    userId: user.id, 
    subscriptionId, 
    oldPlan,
    newPlan: user.plan, 
    billingStatus: user.billingStatus,
    eventId: event.id 
  });

  if (user.email) {
    await sendSubscriptionPlanChangedEmail(user, data);
  }
}

async function handleSubscriptionRenewed(event) {
  const { data } = event;
  const subscriptionId = data.id || data.subscription_id;

  if (!subscriptionId) {
    log('subscription_renewed: no subscription ID', { eventId: event.id });
    return;
  }

  const user = usersDb.find(u => u.dodoSubscriptionId === subscriptionId);
  if (!user) {
    log('subscription_renewed: user not found for subscription', { subscriptionId, eventId: event.id });
    return;
  }

  user.billingStatus = 'active';
  if (data.renews_at || data.current_period_end) {
    user.subscriptionRenewsAt = data.renews_at || data.current_period_end;
  }
  await persistUsersDb();

  log('subscription_renewed: subscription renewed', { userId: user.id, subscriptionId, eventId: event.id });

  if (user.email) {
    await sendSubscriptionRenewedEmail(user, data);
  }
}

async function handleSubscriptionUpdated(event) {
  const { data } = event;
  const subscriptionId = data.id || data.subscription_id;

  if (!subscriptionId) {
    log('subscription_updated: no subscription ID', { eventId: event.id });
    return;
  }

  const user = usersDb.find(u => u.dodoSubscriptionId === subscriptionId);
  if (!user) {
    log('subscription_updated: user not found for subscription', { subscriptionId, eventId: event.id });
    return;
  }

  if (data.renews_at || data.current_period_end) {
    user.subscriptionRenewsAt = data.renews_at || data.current_period_end;
  }
  await persistUsersDb();

  log('subscription_updated: subscription updated', { userId: user.id, subscriptionId, eventId: event.id });
}

async function handleAccountBoostPurchase(req, res) {
  const session = requireUserAuth(req, res);
  if (!session) return;
  const user = findUserById(session.userId);
  if (!user) return sendJson(res, 404, { error: 'User not found' });

  const packSize = BOOST_PACK_SIZE;
  const discount = user.plan === 'enterprise' ? 0.25 : user.plan === 'professional' ? 0.1 : user.plan === 'starter' ? 0.05 : 0;
  const bucket = ensureTokenUsageBucket(user.id);
  bucket.addOns = Math.max(0, Number(bucket.addOns || 0) + packSize);
  await persistTokenUsage();
  const price = BOOST_BASE_PRICE * (1 - discount);

  // Track financial
  trackFinancial(price, user.plan || DEFAULT_PLAN);

  // Create invoice for boost purchase
  try {
    await createInvoiceIfMissing(user, 'boost', {
      tokens: packSize,
      amount: Math.round(price * 100),
      currency: 'usd',
      source: {
        provider: 'internal',
        type: 'boost_purchase',
      },
    });
  } catch (invoiceError) {
    log('failed to create boost invoice', { userId: user.id, error: String(invoiceError) });
  }

  return sendJson(res, 200, {
    ok: true,
    message: `Added a power boost worth ${packSize.toLocaleString()} tokens`,
    priceCharged: `$${price.toFixed(2)} (simulated)`,
    summary: getTokenUsageSummary(user.id, user.plan || DEFAULT_PLAN),
  });
}

// Test endpoint to simulate token usage for testing the usage bar
async function handleSimulateTokenUsage(req, res) {
  // Prefer authenticated user-session, fall back to legacy identity
  const authed = getUserSession(req);
  const resolvedUserId = authed?.userId || resolveUserId(req);
  if (!resolvedUserId) {
    return sendJson(res, 401, { error: 'User identity required' });
  }
  
  const body = await parseJsonBody(req).catch(() => ({}));
  const tokensToAdd = Math.max(0, Math.ceil(Number(body.tokens || 1000)));
  
  console.log(`[TEST] Simulating ${tokensToAdd} tokens for user ${resolvedUserId}`);
  
  // Record the tokens
  await recordUserTokens(resolvedUserId, tokensToAdd);
  
  // Get updated summary
  const user = findUserById(resolvedUserId);
  const plan = user?.plan || DEFAULT_PLAN;
  const summary = getTokenUsageSummary(resolvedUserId, plan);
  
  console.log(`[TEST] Updated usage summary:`, summary);
  
  return sendJson(res, 200, {
    ok: true,
    message: `Simulated ${tokensToAdd} tokens`,
    tokensAdded: tokensToAdd,
    summary,
  });
}

async function handleVerifyEmailApi(req, res, url) {
  try {
    const tokenFromQuery = (url && url.searchParams && url.searchParams.get('token')) || '';
    const body = req.method === 'POST' ? await parseJsonBody(req).catch(() => ({})) : {};
    const token = (body.token || tokenFromQuery || '').trim();
    if (!token) return sendJson(res, 400, { error: 'Verification token is required' });

    const user = usersDb.find((u) => u.verificationToken === token);
    if (!user) return sendJson(res, 400, { error: 'Verification link is invalid' });

    if (user.verificationExpiresAt) {
      const expires = new Date(user.verificationExpiresAt).getTime();
      if (Number.isFinite(expires) && expires < Date.now()) {
        return sendJson(res, 400, { error: 'Verification link has expired. Please request a new one.' });
      }
    }

    user.emailVerified = true;
    user.verificationToken = '';
    user.verificationExpiresAt = null;
    await persistUsersDb();
    const tokenValue = startUserSession(res, user.id, true);
    log('user email verified', { userId: user.id, email: user.email });

    // Check if user has selected a plan
    const hasPlan = normalizePlanSelection(user?.plan);

    sendJson(res, 200, {
      ok: true,
      user: { id: user.id, email: user.email, emailVerified: true, hasPlan },
      token: tokenValue,
      expiresAt: Date.now() + USER_SESSION_TTL_MS,
      message: 'Email verified successfully.',
      redirect: hasPlan ? '/apps' : '/select-plan',
    });
  } catch (error) {
    sendJson(res, 400, { error: error.message || 'Unable to verify email' });
  }
}

async function handlePasswordResetRequest(req, res) {
  try {
    const body = await parseJsonBody(req);
    const email = (body.email || '').trim().toLowerCase();
    if (!email) return sendJson(res, 400, { error: 'Email is required' });

    const user = findUserByEmail(email);
    if (!user) {
      await new Promise((resolve) => setTimeout(resolve, 250));
      return sendJson(res, 200, { ok: true, message: 'If an account exists, a reset link has been sent.' });
    }

    assignPasswordResetToken(user);
    await persistUsersDb();

    // Send reset email in the background
    sendPasswordResetEmail(user, resolveBaseUrl(req)).catch(err => {
      log('background password reset email failed', { error: String(err), email: user.email });
    });

    log('password reset email queued', { userId: user.id, email: user.email });
    sendJson(res, 200, { ok: true, message: 'If an account exists, a reset link has been sent.' });
  } catch (error) {
    sendJson(res, 400, { error: error.message || 'Unable to start password reset' });
  }
}

async function handlePasswordReset(req, res) {
  try {
    const body = await parseJsonBody(req);
    const token = (body.token || '').trim();
    const newPassword = (body.password || '').trim();
    if (!token || !newPassword) return sendJson(res, 400, { error: 'Token and new password are required' });
    if (newPassword.length < 6) return sendJson(res, 400, { error: 'Password must be at least 6 characters long' });

    const user = usersDb.find((u) => u.resetToken === token);
    if (!user) return sendJson(res, 400, { error: 'Reset link is invalid' });

    if (user.resetExpiresAt) {
      const expires = new Date(user.resetExpiresAt).getTime();
      if (Number.isFinite(expires) && expires < Date.now()) {
        return sendJson(res, 400, { error: 'Reset link has expired' });
      }
    }

    user.password = await bcrypt.hash(newPassword, PASSWORD_SALT_ROUNDS);
    user.resetToken = '';
    user.resetExpiresAt = null;
    if (!user.emailVerified) user.emailVerified = true;
    await persistUsersDb();

    const tokenValue = startUserSession(res, user.id, true);
    log('password reset successful', { userId: user.id, email: user.email });
    sendJson(res, 200, {
      ok: true,
      user: { id: user.id, email: user.email, emailVerified: !!user.emailVerified },
      token: tokenValue,
      expiresAt: Date.now() + USER_SESSION_TTL_MS,
    });
  } catch (error) {
    sendJson(res, 400, { error: error.message || 'Unable to reset password' });
  }
}

async function handleSelectPlan(req, res) {
  const session = requireUserAuth(req, res);
  if (!session) return;

  const user = findUserById(session.userId);
  if (!user) {
    return sendJson(res, 404, { error: 'User not found' });
  }

  try {
    const body = await parseJsonBody(req);
    const plan = normalizePlanSelection(body.plan || body.newPlan);

    if (!plan) {
      return sendJson(res, 400, { error: 'Invalid plan selection' });
    }

    // Hobby plan is free - activate immediately
    if (plan === 'hobby') {
      const previousPlan = user.plan || 'none';

      // Cancel any existing Dodo subscription
      if (user.dodoSubscriptionId) {
        await cancelDodoSubscription(user, 'hobby_plan', { clearOnFailure: true });
      }

      user.plan = plan;
      user.billingStatus = DEFAULT_BILLING_STATUS;
      user.subscriptionRenewsAt = null;
      user.billingCycle = null;
      user.subscriptionCurrency = null;
      await persistUsersDb();

      // Track plan selection and conversion
      trackConversionFunnel('plan_selection', 'hobby_selected', user.id, {
        previousPlan: previousPlan,
        newPlan: plan
      });
      trackPlanUpgrade(previousPlan, plan, user.id);
      trackFeatureUsage('plan_selection', user.id, plan);

      log('hobby plan selected (free)', { userId: user.id, email: user.email, plan });
      return sendJson(res, 200, { ok: true, account: await serializeAccount(user) });
    }

    // For paid plans, redirect to subscription checkout
    // Return a special response that frontend can handle to redirect to subscription flow
    const previousPlan = user.plan || 'none';
    trackConversionFunnel('plan_selection', 'paid_plan_selected', user.id, {
      previousPlan: previousPlan,
      newPlan: plan
    });
    trackPlanUpgrade(previousPlan, plan, user.id);
    trackFeatureUsage('plan_selection', user.id, plan);
    
    return sendJson(res, 200, { 
      requiresPayment: true, 
      plan: plan,
      message: 'Paid plan selected - redirecting to payment' 
    });
    
  } catch (error) {
    sendJson(res, 400, { error: error.message || 'Unable to select plan' });
  }
}

function renderOAuthResultPage(res, { success, message, next, user, token, provider }) {
  const safeNext = sanitizeRedirectPath(next, '/apps');
  const safeMessage = escapeHtml(message || (success ? 'Signing you in...' : 'Authentication failed'));
  let script = `window.location.href = '${safeNext}';`;
  if (success && user) {
    const payload = {
      email: user.email,
      accountId: user.id,
      sessionToken: token || '',
      provider: provider || 'oauth'
    };
    const payloadJson = JSON.stringify(payload).replace(/</g, '\\u003c');
    script = `
      (() => {
        try {
          const data = ${payloadJson};
          const json = JSON.stringify(data);
          localStorage.setItem('wordpress_plugin_ai_user', json);
          localStorage.setItem('shopify_ai_user', json);
          document.cookie = 'chat_user=' + encodeURIComponent(data.accountId) + '; path=/; SameSite=Lax';
        } catch (_) { /* ignore */ }
        window.location.href = '${safeNext}';
      })();
    `;
  }
  const statusCode = success ? 200 : 400;
  res.writeHead(statusCode, { 'Content-Type': 'text/html' });
  res.end(`<!doctype html><html><body><p>${safeMessage}</p><script>${script}</script></body></html>`);
}

function renderOAuthError(res, message, next) {
  return renderOAuthResultPage(res, { success: false, message: message || 'Unable to sign you in', next: next || '/login' });
}

function renderOAuthSuccess(res, user, token, next, provider) {
  // Check if user has selected a plan
  const hasPlan = normalizePlanSelection(user?.plan);
  const redirectNext = hasPlan ? next : '/select-plan';
  return renderOAuthResultPage(res, { success: true, message: 'Signing you in...', next: redirectNext, user, token, provider });
}

async function handleGoogleAuthStart(req, res, url) {
  if (!GOOGLE_CLIENT_ID || !GOOGLE_CLIENT_SECRET) {
    return renderOAuthError(res, 'Google sign-in is not configured', url.searchParams.get('next') || '/login');
  }
  const next = url.searchParams.get('next') || '/apps';
  const remember = url.searchParams.get('remember') === 'true' || url.searchParams.get('remember') === '1';
  const state = createOAuthState(next, 'google', remember);
  const redirectUri = buildRedirectUri(req, 'google');
  const params = new URLSearchParams({
    client_id: GOOGLE_CLIENT_ID,
    redirect_uri: redirectUri,
    response_type: 'code',
    scope: 'openid email profile',
    state,
    access_type: 'offline',
    include_granted_scopes: 'true',
    prompt: 'consent',
  });
  res.writeHead(302, { Location: `https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}` });
  res.end();
}

async function handleGoogleAuthCallback(req, res, url) {
  const stateParam = url.searchParams.get('state') || '';
  const stateEntry = consumeOAuthState(stateParam, 'google');
  const hintNext = sanitizeRedirectPath(url.searchParams.get('next') || '/apps');
  const next = stateEntry?.next || hintNext;

  if (!GOOGLE_CLIENT_ID || !GOOGLE_CLIENT_SECRET) {
    return renderOAuthError(res, 'Google sign-in is not configured', next);
  }
  if (!stateEntry) {
    return renderOAuthError(res, 'This Google login request has expired. Please try again.', next);
  }
  const code = url.searchParams.get('code');
  const error = url.searchParams.get('error');
  if (error) {
    return renderOAuthError(res, `Google authentication was cancelled: ${escapeHtml(error)}`, next);
  }
  if (!code) {
    return renderOAuthError(res, 'Missing Google authorization code', next);
  }

  try {
    const redirectUri = buildRedirectUri(req, 'google');
    const tokenRes = await fetch('https://oauth2.googleapis.com/token', {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body: new URLSearchParams({
        code,
        client_id: GOOGLE_CLIENT_ID,
        client_secret: GOOGLE_CLIENT_SECRET,
        redirect_uri: redirectUri,
        grant_type: 'authorization_code'
      })
    });
    const tokenJson = await tokenRes.json().catch(() => ({}));
    const accessToken = tokenJson.access_token;
    const idToken = tokenJson.id_token;
    if (!tokenRes.ok || (!accessToken && !idToken)) {
      log('google oauth token exchange failed', { status: tokenRes.status, body: tokenJson });
      return renderOAuthError(res, 'Google authentication failed during token exchange', next);
    }

    let profile = {};
    if (idToken) {
      profile = decodeJwtPayload(idToken);
    }
    if (accessToken) {
      try {
        const userInfoRes = await fetch('https://www.googleapis.com/oauth2/v3/userinfo', {
          headers: { Authorization: `Bearer ${accessToken}` }
        });
        if (userInfoRes.ok) {
          profile = await userInfoRes.json();
        }
      } catch (err) {
        log('google userinfo fetch failed', { err: String(err) });
      }
    }

    const email = (profile?.email || '').toLowerCase();
    const providerId = profile?.sub || profile?.id || null;
    if (!providerId) {
      return renderOAuthError(res, 'Google account did not include an id', next);
    }
    if (!email) {
      return renderOAuthError(res, 'Google account did not return an email address', next);
    }

    const user = await upsertOAuthUser('google', providerId, email, profile || {});
    const token = startUserSession(res, user.id, stateEntry.remember || false);
    log('google oauth login success', { userId: user.id, email: user.email, remember: stateEntry.remember });
    return renderOAuthSuccess(res, user, token, next, 'google');
  } catch (err) {
    log('google oauth callback error', { error: String(err) });
    return renderOAuthError(res, 'Unexpected error during Google sign-in', next);
  }
}

async function handleGithubAuthStart(req, res, url) {
  if (!GITHUB_CLIENT_ID || !GITHUB_CLIENT_SECRET) {
    return renderOAuthError(res, 'GitHub sign-in is not configured', url.searchParams.get('next') || '/login');
  }
  const next = url.searchParams.get('next') || '/apps';
  const remember = url.searchParams.get('remember') === 'true' || url.searchParams.get('remember') === '1';
  const state = createOAuthState(next, 'github', remember);
  const redirectUri = buildRedirectUri(req, 'github');
  const params = new URLSearchParams({
    client_id: GITHUB_CLIENT_ID,
    redirect_uri: redirectUri,
    scope: 'read:user user:email',
    state
  });
  res.writeHead(302, { Location: `https://github.com/login/oauth/authorize?${params.toString()}` });
  res.end();
}

async function handleGithubAuthCallback(req, res, url) {
  const stateParam = url.searchParams.get('state') || '';
  const stateEntry = consumeOAuthState(stateParam, 'github');
  const hintNext = sanitizeRedirectPath(url.searchParams.get('next') || '/apps');
  const next = stateEntry?.next || hintNext;

  if (!GITHUB_CLIENT_ID || !GITHUB_CLIENT_SECRET) {
    return renderOAuthError(res, 'GitHub sign-in is not configured', next);
  }
  if (!stateEntry) {
    return renderOAuthError(res, 'This GitHub login request has expired. Please try again.', next);
  }

  const code = url.searchParams.get('code');
  const error = url.searchParams.get('error');
  if (error) {
    return renderOAuthError(res, `GitHub authentication was cancelled: ${escapeHtml(error)}`, next);
  }
  if (!code) {
    return renderOAuthError(res, 'Missing GitHub authorization code', next);
  }

  try {
    const redirectUri = buildRedirectUri(req, 'github');
    const tokenRes = await fetch('https://github.com/login/oauth/access_token', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Accept': 'application/json'
      },
      body: JSON.stringify({
        client_id: GITHUB_CLIENT_ID,
        client_secret: GITHUB_CLIENT_SECRET,
        code,
        redirect_uri: redirectUri,
      })
    });
    const tokenJson = await tokenRes.json().catch(() => ({}));
    const accessToken = tokenJson.access_token;
    if (!tokenRes.ok || !accessToken) {
      log('github oauth token exchange failed', { status: tokenRes.status, body: tokenJson });
      return renderOAuthError(res, 'GitHub authentication failed during token exchange', next);
    }

    const userRes = await fetch('https://api.github.com/user', {
      headers: {
        Authorization: `Bearer ${accessToken}`,
        'User-Agent': OAUTH_USER_AGENT
      }
    });
    const userProfile = await userRes.json().catch(() => ({}));
    if (!userRes.ok) {
      log('github user profile fetch failed', { status: userRes.status, body: userProfile });
      return renderOAuthError(res, 'Could not read your GitHub profile', next);
    }

    let email = (userProfile.email || '').toLowerCase();
    if (!email) {
      try {
        const emailRes = await fetch('https://api.github.com/user/emails', {
          headers: {
            Authorization: `Bearer ${accessToken}`,
            'User-Agent': OAUTH_USER_AGENT
          }
        });
        const emails = await emailRes.json().catch(() => []);
        if (Array.isArray(emails)) {
          const verifiedEmails = emails.filter((e) => e && e.verified);
          const primary = verifiedEmails.find((e) => e.primary) || verifiedEmails[0];
          if (primary?.email) {
            email = primary.email.toLowerCase();
          } else {
            return renderOAuthError(res, 'GitHub account must have a verified email address', next);
          }
        }
      } catch (err) {
        log('github email fetch failed', { err: String(err) });
      }
    }

    const providerId = userProfile?.id ? String(userProfile.id) : null;
    if (!providerId) {
      return renderOAuthError(res, 'GitHub account did not include an id', next);
    }
    if (!email) {
      return renderOAuthError(res, 'GitHub account did not return a verified email address', next);
    }

    const user = await upsertOAuthUser('github', providerId, email, userProfile || {});
    const token = startUserSession(res, user.id, stateEntry.remember || false);
    log('github oauth login success', { userId: user.id, email: user.email, remember: stateEntry.remember });
    return renderOAuthSuccess(res, user, token, next, 'github');
  } catch (err) {
    log('github oauth callback error', { error: String(err) });
    return renderOAuthError(res, 'Unexpected error during GitHub sign-in', next);
  }
}

async function handleAdminLogin(req, res) {
  if (!ADMIN_USER || !ADMIN_PASSWORD) return sendJson(res, 500, { error: 'Admin credentials not configured' });
  try {
    const body = await parseJsonBody(req);
    const user = (body.username || body.user || '').trim();
    const pass = (body.password || body.pass || '').trim();

    // Normalize username comparison to be case-insensitive and trim whitespace
    const adminUserNormalized = (ADMIN_USER || '').trim().toLowerCase();
    const userNormalized = (user || '').trim().toLowerCase();
    const clientIp = req.socket?.remoteAddress || 'unknown';

    // Check honeypot
    if (checkHoneypot(body)) {
      log('admin login honeypot triggered', { ip: clientIp });
      return sendJson(res, 400, { error: 'Invalid request' });
    }

    // Check rate limit
    const rateLimit = checkLoginRateLimit(clientIp, ADMIN_LOGIN_RATE_LIMIT, adminLoginAttempts);
    if (rateLimit.blocked) {
      log('admin login rate limited', { ip: clientIp, retryAfter: rateLimit.retryAfter });
      return sendJson(res, 429, {
        error: 'Too many login attempts. Please try again later.',
        retryAfter: rateLimit.retryAfter || 60
      });
    }

    if (!adminUserNormalized || !ADMIN_PASSWORD) {
      log('admin credentials not configured (runtime)');
      return sendJson(res, 500, { error: 'Admin credentials not configured' });
    }

    // Validate username
    if (userNormalized !== adminUserNormalized) {
      log('failed admin login', { user: userNormalized, ip: clientIp, reason: 'invalid_username' });
      return sendJson(res, 401, { error: 'Incorrect credentials' });
    }

    // Validate password using bcrypt - no plaintext fallback for security
    if (!adminPasswordHash) {
      log('admin login failed: bcrypt hash not initialized', { ip: clientIp });
      return sendJson(res, 500, { error: 'Authentication system not ready. Please try again.' });
    }
    
    const passwordValid = await bcrypt.compare(pass, adminPasswordHash);
    if (!passwordValid) {
      log('failed admin login', { user: userNormalized, ip: clientIp, reason: 'invalid_password' });
      return sendJson(res, 401, { error: 'Incorrect credentials' });
    }

    // Clear failed attempts on success
    adminLoginAttempts.delete(clientIp);

    const token = startAdminSession(res);
    sendJson(res, 200, { ok: true, token, user: ADMIN_USER, expiresAt: Date.now() + ADMIN_SESSION_TTL_MS });
  } catch (error) {
    sendJson(res, 400, { error: error.message || 'Unable to login' });
  }
}

async function handleAdminLogout(req, res) {
  const session = getAdminSession(req);
  if (session) adminSessions.delete(session.token);
  clearAdminSession(res);
  sendJson(res, 200, { ok: true });
}

async function handleAdminMe(req, res) {
  const session = requireAdminAuth(req, res);
  if (!session) return;
  sendJson(res, 200, { ok: true, user: ADMIN_USER, expiresAt: session.expiresAt });
}

async function handleAdminAvailableModels(req, res, cliParam = 'opencode') {
  const session = requireAdminAuth(req, res);
  if (!session) return;
  try {
    const cli = normalizeCli(cliParam);
    const models = await listModels(cli);
    sendJson(res, 200, { models });
  } catch (error) {
    sendJson(res, 500, { error: error.message || 'Failed to load available models' });
  }
}

async function handleAdminListIcons(req, res) {
  const session = requireAdminAuth(req, res);
  if (!session) return;
  const icons = await listAdminIcons();
  sendJson(res, 200, { icons });
}

async function handleAdminModelsList(req, res) {
  const session = requireAdminAuth(req, res);
  if (!session) return;
  // Return clean structure
  sendJson(res, 200, { 
    opencodeModels,
    publicModels,
    // Legacy support
    models: getConfiguredModels('opencode'),
  });
}

async function handleAdminModelUpsert(req, res) {
  const session = requireAdminAuth(req, res);
  if (!session) return;
  try {
    const body = await parseJsonBody(req);
    const modelName = (body.name || body.model || '').trim();
    const label = (body.label || body.displayName || modelName).trim();
    const tier = normalizeTier(body.tier);
    
    if (!modelName) return sendJson(res, 400, { error: 'Model name is required' });
    
    let icon = '';
    if (typeof body.icon === 'string' && body.icon.trim()) {
      icon = await normalizeIconPath(body.icon) || '';
    }
    const supportsMedia = typeof body.supportsMedia === 'boolean' ? body.supportsMedia : false;
    
    const payload = { 
      id: body.id || randomUUID(), 
      name: modelName, 
      label: label || modelName, 
      icon, 
      tier, 
      supportsMedia,
      multiplier: getTierMultiplier(tier),
    };
    
    if (body.type === 'opencode') {
      // Add to opencode models
      const existingIndex = body.id ? opencodeModels.findIndex((m) => m.id === body.id) : -1;
      if (existingIndex >= 0) opencodeModels[existingIndex] = { ...opencodeModels[existingIndex], ...payload };
      else opencodeModels.push(payload);
      
      await persistAdminModels();
      sendJson(res, 200, { 
        opencodeModels,
        publicModels,
      });
    } else if (body.type === 'public') {
      // Add to public models
      const existingIndex = body.id ? publicModels.findIndex((m) => m.id === body.id) : -1;
      if (existingIndex >= 0) publicModels[existingIndex] = { ...publicModels[existingIndex], ...payload };
      else publicModels.push(payload);
      
      await persistAdminModels();
      sendJson(res, 200, { 
        opencodeModels,
        publicModels,
      });
    } else {
      return sendJson(res, 400, { error: 'Invalid type. Use "opencode" or "public"' });
    }
  } catch (error) {
    sendJson(res, 400, { error: error.message || 'Unable to save model' });
  }
}

async function handleAdminModelsReorder(req, res) {
  const session = requireAdminAuth(req, res);
  if (!session) return;
  try {
    const body = await parseJsonBody(req);
    if (!Array.isArray(body.models)) {
      return sendJson(res, 400, { error: 'models array is required' });
    }
    
    const reorderType = body.type || 'public';
    
    if (reorderType === 'opencode') {
      // Validate that all provided IDs exist in opencodeModels
      const currentIds = new Set(opencodeModels.map(m => m.id));
      const newIds = body.models.map(m => m.id);
      const allExist = newIds.every(id => currentIds.has(id));
      
      if (!allExist) {
        return sendJson(res, 400, { error: 'Invalid model IDs in reorder request' });
      }
      
      // Reorder opencodeModels based on the provided order
      const reordered = [];
      body.models.forEach(m => {
        const model = opencodeModels.find(pm => pm.id === m.id);
        if (model) reordered.push(model);
      });
      
      opencodeModels = reordered;
    } else {
      // Validate that all provided IDs exist in publicModels
      const currentIds = new Set(publicModels.map(m => m.id));
      const newIds = body.models.map(m => m.id);
      const allExist = newIds.every(id => currentIds.has(id));
      
      if (!allExist) {
        return sendJson(res, 400, { error: 'Invalid model IDs in reorder request' });
      }
      
      // Reorder publicModels based on the provided order
      const reordered = [];
      body.models.forEach(m => {
        const model = publicModels.find(pm => pm.id === m.id);
        if (model) reordered.push(model);
      });
      
      publicModels = reordered;
    }
    
    await persistAdminModels();
    
    sendJson(res, 200, {
      ok: true,
      opencodeModels,
      publicModels,
    });
  } catch (error) {
    sendJson(res, 400, { error: error.message || 'Unable to reorder models' });
  }
}

async function handleAdminModelDelete(req, res, id) {
  const session = requireAdminAuth(req, res);
  if (!session) return;
  
  const url = new URL(req.url, `http://${req.headers.host}`);
  const deleteType = url.searchParams.get('type') || 'public';
  
  if (deleteType === 'opencode') {
    const before = opencodeModels.length;
    opencodeModels = opencodeModels.filter((m) => m.id !== id);
    if (opencodeModels.length === before) {
      return sendJson(res, 404, { error: 'Model not found' });
    }
  } else {
    const before = publicModels.length;
    publicModels = publicModels.filter((m) => m.id !== id);
    if (publicModels.length === before) {
      return sendJson(res, 404, { error: 'Model not found' });
    }
  }
  
  await persistAdminModels();
  sendJson(res, 200, { 
    ok: true,
    opencodeModels,
    publicModels,
  });
}

async function handleAdminOpenRouterSettingsGet(req, res) {
  const session = requireAdminAuth(req, res);
  if (!session) return;
  sendJson(res, 200, openrouterSettings);
}

async function handleAdminMistralSettingsGet(req, res) {
  const session = requireAdminAuth(req, res);
  if (!session) return;
  sendJson(res, 200, mistralSettings);
}

async function handleAdminMistralSettingsPost(req, res) {
  const session = requireAdminAuth(req, res);
  if (!session) return;
  try {
    const body = await parseJsonBody(req);
    mistralSettings = {
      primaryModel: (body.primaryModel || '').trim(),
      backupModel1: (body.backupModel1 || '').trim(),
      backupModel2: (body.backupModel2 || '').trim(),
      backupModel3: (body.backupModel3 || '').trim(),
    };
    await persistMistralSettings();
    sendJson(res, 200, { ok: true, settings: mistralSettings });
  } catch (error) {
    sendJson(res, 400, { error: error.message || 'Unable to save settings' });
  }
}

async function handleAdminPlanSettingsGet(req, res) {
  const session = requireAdminAuth(req, res);
  if (!session) return;
  sendJson(res, 200, planSettings);
}

async function handleAdminPlanSettingsPost(req, res) {
  const session = requireAdminAuth(req, res);
  if (!session) return;
  try {
    const body = await parseJsonBody(req);
    if (body.provider && PLANNING_PROVIDERS.includes(normalizeProviderName(body.provider))) {
      planSettings.provider = normalizeProviderName(body.provider);
    }
    if (Array.isArray(body.planningChain)) {
      planSettings.planningChain = normalizePlanningChain(body.planningChain);
    }
    if (typeof body.freePlanModel === 'string') {
      planSettings.freePlanModel = body.freePlanModel.trim();
    }
    if (!planSettings.planningChain.length) {
      planSettings.planningChain = defaultPlanningChainFromSettings(planSettings.provider);
    }
    await persistPlanSettings();
    sendJson(res, 200, { ok: true, settings: planSettings });
  } catch (error) {
    sendJson(res, 400, { error: error.message || 'Unable to save settings' });
  }
}

// Get current plan token allocations
async function handleAdminPlanTokensGet(req, res) {
  const session = requireAdminAuth(req, res);
  if (!session) return;
  try {
    sendJson(res, 200, { limits: planTokenLimits });
  } catch (error) {
    sendJson(res, 500, { error: error.message || 'Unable to fetch plan tokens' });
  }
}

// Update plan token allocations
async function handleAdminPlanTokensPost(req, res) {
  const session = requireAdminAuth(req, res);
  if (!session) return;
  try {
    const body = await parseJsonBody(req);
    if (!body || typeof body !== 'object' || typeof body.limits !== 'object') return sendJson(res, 400, { error: 'Invalid payload' });
    const incoming = body.limits || {};
    const clean = JSON.parse(JSON.stringify(planTokenLimits || {}));
    for (const plan of Object.keys(clean)) {
      if (incoming[plan] && typeof incoming[plan] === 'number') {
        clean[plan] = Math.max(0, Number(incoming[plan] || 0));
      }
    }
    planTokenLimits = clean;
    await persistPlanTokenLimits();
    sendJson(res, 200, { ok: true, limits: planTokenLimits });
  } catch (error) {
    sendJson(res, 400, { error: error.message || 'Unable to save plan tokens' });
  }
}

// Get token rates
async function handleAdminTokenRatesGet(req, res) {
  const session = requireAdminAuth(req, res);
  if (!session) return;
  try {
    sendJson(res, 200, { rates: tokenRates });
  } catch (error) {
    sendJson(res, 500, { error: error.message || 'Unable to fetch token rates' });
  }
}

// Update token rates
async function handleAdminTokenRatesPost(req, res) {
  const session = requireAdminAuth(req, res);
  if (!session) return;
  try {
    const body = await parseJsonBody(req);
    if (!body || typeof body !== 'object' || typeof body.rates !== 'object') return sendJson(res, 400, { error: 'Invalid payload' });
    const incoming = body.rates || {};
    const clean = JSON.parse(JSON.stringify(tokenRates || {}));
    for (const currency of Object.keys(clean)) {
      if (incoming[currency] && typeof incoming[currency] === 'number') {
        clean[currency] = Math.max(0, Number(incoming[currency] || 0));
      }
    }
    tokenRates = clean;
    await persistTokenRates();
    sendJson(res, 200, { ok: true, rates: tokenRates });
  } catch (error) {
    sendJson(res, 400, { error: error.message || 'Unable to save token rates' });
  }
}

async function handleAdminProviderLimitsGet(req, res) {
  const session = requireAdminAuth(req, res);
  if (!session) return;
  try {
    const discovery = await discoverProviderModels();
    const snapshot = getProviderUsageSnapshot(discovery.providers);
    sendJson(res, 200, {
      limits: providerLimits.limits,
      usage: snapshot,
      opencodeBackupModel: providerLimits.opencodeBackupModel || '',
      providers: discovery.providers,
      providerModels: discovery.providerModels,
    });
  } catch (error) {
    sendJson(res, 500, { error: error.message || 'Unable to load provider limits' });
  }
}

async function handleAdminProviderLimitsPost(req, res) {
  const session = requireAdminAuth(req, res);
  if (!session) return;
  try {
    const body = await parseJsonBody(req);
    const provider = normalizeProviderName(body.provider);
    if (!provider) return sendJson(res, 400, { error: 'Provider is required' });
    const cfg = ensureProviderLimitDefaults(provider);
    if (body.scope === 'model') cfg.scope = 'model';
    else if (body.scope === 'provider') cfg.scope = 'provider';

    const targetModel = (body.model || '').trim();
    const target = cfg.scope === 'model' && targetModel ? (cfg.perModel[targetModel] = cfg.perModel[targetModel] || {}) : cfg;
    ['tokensPerMinute', 'tokensPerHour', 'tokensPerDay', 'requestsPerMinute', 'requestsPerHour', 'requestsPerDay'].forEach((field) => {
      if (body[field] !== undefined) target[field] = sanitizeLimitNumber(body[field]);
    });

    if (typeof body.opencodeBackupModel === 'string') {
      providerLimits.opencodeBackupModel = body.opencodeBackupModel.trim();
    }

    await persistProviderLimits();
    const discovery = await discoverProviderModels();
    sendJson(res, 200, {
      ok: true,
      limits: providerLimits.limits,
      usage: getProviderUsageSnapshot(discovery.providers),
      opencodeBackupModel: providerLimits.opencodeBackupModel || '',
      providers: discovery.providers,
      providerModels: discovery.providerModels,
    });
  } catch (error) {
    sendJson(res, 400, { error: error.message || 'Unable to save provider limits' });
  }
}

// Admin-only environment diagnostics for debugging missing provider API keys
async function handleAdminEnvConfig(req, res) {
  const session = requireAdminAuth(req, res);
  if (!session) return;
  try {
    const groqKey = process.env.GROQ_API_KEY || process.env.GROQ_API_TOKEN || '';
    const mistralKey = process.env.MISTRAL_API_KEY || process.env.MISTRAL_API_TOKEN || '';
    const openrouterKey = process.env.OPENROUTER_API_KEY || process.env.OPENROUTER_API_TOKEN || '';
    const chutesKey = process.env.PLUGIN_COMPASS_CHUTES_API_KEY || process.env.CHUTES_API_KEY || process.env.CHUTES_API_TOKEN || '';
    const kiloKey = process.env.KILO_API_KEY || '';

    const payload = {
      GROQ: {
        configured: !!groqKey,
        prefix: groqKey ? `${groqKey.substring(0, 8)}...` : null,
        source: groqKey ? (process.env.GROQ_API_KEY ? 'GROQ_API_KEY' : 'GROQ_API_TOKEN') : null,
      },
      MISTRAL: {
        configured: !!mistralKey,
        prefix: mistralKey ? `${mistralKey.substring(0, 8)}...` : null,
        source: mistralKey ? (process.env.MISTRAL_API_KEY ? 'MISTRAL_API_KEY' : 'MISTRAL_API_TOKEN') : null,
      },
      OPENROUTER: {
        configured: !!openrouterKey,
        prefix: openrouterKey ? `${openrouterKey.substring(0, 8)}...` : null,
        source: openrouterKey ? (process.env.OPENROUTER_API_KEY ? 'OPENROUTER_API_KEY' : 'OPENROUTER_API_TOKEN') : null,
      },
      CHUTES: {
        configured: !!chutesKey,
        prefix: chutesKey ? `${chutesKey.substring(0, 8)}...` : null,
        source: chutesKey ? (process.env.PLUGIN_COMPASS_CHUTES_API_KEY ? 'PLUGIN_COMPASS_CHUTES_API_KEY' : process.env.CHUTES_API_KEY ? 'CHUTES_API_KEY' : 'CHUTES_API_TOKEN') : null,
      },
      KILO: {
        configured: !!kiloKey,
        prefix: kiloKey ? `${kiloKey.substring(0, 8)}...` : null,
        source: kiloKey ? 'KILO_API_KEY' : null,
      },
    };

    sendJson(res, 200, { ok: true, env: payload });
  } catch (err) {
    sendJson(res, 500, { error: err.message || String(err) });
  }
}

async function handleAdminAccountsList(req, res) {
  const session = requireAdminAuth(req, res);
  if (!session) return;
  const accountPromises = usersDb.map((u) => serializeAccount(u));
  const accounts = (await Promise.all(accountPromises)).filter(Boolean);
  sendJson(res, 200, { accounts });
}

async function handleAdminAccountPlanUpdate(req, res) {
  const session = requireAdminAuth(req, res);
  if (!session) return;
  try {
    const body = await parseJsonBody(req);
    const userId = body.userId;
    const plan = normalizePlanSelection(body.plan);

    if (!userId) return sendJson(res, 400, { error: 'User ID is required' });
    if (!plan) return sendJson(res, 400, { error: 'Invalid plan selected' });

    const user = findUserById(userId);
    if (!user) return sendJson(res, 404, { error: 'User not found' });

    user.plan = plan;
    // Also update billing status and renewal date if it's a paid plan to make it look "active"
    if (isPaidPlan(plan)) {
      user.billingStatus = 'active';
      user.subscriptionRenewsAt = computeRenewalDate('monthly');
    } else {
      user.billingStatus = 'active';
      user.subscriptionRenewsAt = null;
    }

    await persistUsersDb();
    const accountData = await serializeAccount(user);
    log('Admin updated user plan', { userId, plan, admin: ADMIN_USER });
    sendJson(res, 200, { ok: true, account: accountData });
  } catch (error) {
    sendJson(res, 400, { error: error.message || 'Unable to update plan' });
  }
}

// Admin account delete endpoint
async function handleAdminAccountDelete(req, res) {
  const session = requireAdminAuth(req, res);
  if (!session) return;
  try {
    const body = await parseJsonBody(req);
    const userId = body.userId;

    if (!userId) return sendJson(res, 400, { error: 'User ID is required' });

    const userIndex = usersDb.findIndex(u => u.id === userId);
    if (userIndex === -1) return sendJson(res, 404, { error: 'User not found' });

    const user = usersDb[userIndex];
    
    // Permanently delete user data
    usersDb.splice(userIndex, 1);
    
    for (const [token, session] of userSessions.entries()) {
      if (session.userId === userId) {
        userSessions.delete(token);
      }
    }
    
    // Remove user's sessions from the main state
    state.sessions = state.sessions.filter(s => s.userId !== userId);
    
    // Delete user's workspaces/sessions from file system
    const userWorkspaceDir = path.join(WORKSPACES_ROOT, userId);
    if (fsSync.existsSync(userWorkspaceDir)) {
      try {
        fsSync.rmSync(userWorkspaceDir, { recursive: true, force: true });
      } catch (err) {
        console.error('Failed to delete user workspace:', err);
        // Continue with deletion even if workspace cleanup fails
      }
    }
    
    // Delete user's session state
    const userStateFile = path.join(STATE_DIR, `session-${userId}.json`);
    if (fsSync.existsSync(userStateFile)) {
      try {
        fsSync.unlinkSync(userStateFile);
      } catch (err) {
        console.error('Failed to delete user state file:', err);
        // Continue with deletion even if state cleanup fails
      }
    }
    
    await persistUsersDb();
    await persistState();
    
    log('Admin permanently deleted user', { 
      userId, 
      email: user.email, 
      admin: ADMIN_USER 
    });
    
    sendJson(res, 200, { ok: true, message: 'User permanently deleted' });
  } catch (error) {
    console.error('Error deleting user:', error);
    sendJson(res, 500, { error: error.message || 'Unable to delete user' });
  }
}

// Admin account token override endpoint
async function handleAdminAccountTokensPost(req, res) {
  const session = requireAdminAuth(req, res);
  if (!session) return;
  try {
    const body = await parseJsonBody(req);
    const userId = body.userId;
    const tokenAmount = body.tokens;
    const mode = body.mode || 'limit'; // 'limit', 'usage', or 'remaining'

    if (!userId) return sendJson(res, 400, { error: 'User ID is required' });
    if (typeof tokenAmount !== 'number' || tokenAmount < 0 || !Number.isFinite(tokenAmount)) {
      return sendJson(res, 400, { error: 'Invalid token amount. Must be a non-negative number.' });
    }
    if (!['limit', 'usage', 'remaining'].includes(mode)) {
      return sendJson(res, 400, { error: 'Invalid mode. Must be "limit", "usage", or "remaining".' });
    }

    const user = findUserById(userId);
    if (!user) return sendJson(res, 404, { error: 'User not found' });

    const bucket = ensureTokenUsageBucket(userId);
    if (!bucket) return sendJson(res, 500, { error: 'Failed to access token usage data' });

    let result = {};

    if (mode === 'limit') {
      // Set the token override (original behavior)
      bucket.tokenOverride = tokenAmount === 0 ? null : Math.floor(tokenAmount);
      result = { mode: 'limit', tokenOverride: bucket.tokenOverride };
      log('Admin set user token limit override', { userId, tokens: bucket.tokenOverride, admin: ADMIN_USER });
    } else if (mode === 'usage') {
      // Set the usage directly (Option B)
      bucket.usage = Math.floor(tokenAmount);
      result = { mode: 'usage', usage: bucket.usage };
      log('Admin set user token usage', { userId, usage: bucket.usage, admin: ADMIN_USER });
    } else if (mode === 'remaining') {
      // Set remaining tokens (Option A) - calculate usage from limit
      const limit = getPlanTokenLimits(user.plan, userId);
      const newUsage = Math.max(0, limit - Math.floor(tokenAmount));
      bucket.usage = newUsage;
      result = { mode: 'remaining', remaining: Math.floor(tokenAmount), usage: newUsage, limit };
      log('Admin set user token remaining', { userId, remaining: tokenAmount, usage: newUsage, limit, admin: ADMIN_USER });
    }

    await persistTokenUsage();

    const accountData = await serializeAccount(user);
    sendJson(res, 200, { ok: true, account: accountData, ...result });
  } catch (error) {
    sendJson(res, 400, { error: error.message || 'Unable to update token settings' });
  }
}

// Affiliate management API endpoints
async function handleAdminAffiliatesList(req, res) {
  const session = requireAdminAuth(req, res);
  if (!session) return;
  const affiliates = affiliatesDb.map((a) => summarizeAffiliate(a)).filter(Boolean);
  sendJson(res, 200, { affiliates });
}

async function handleAdminAffiliateDelete(req, res) {
  const session = requireAdminAuth(req, res);
  if (!session) return;
  try {
    const body = await parseJsonBody(req);
    const affiliateId = body.affiliateId;

    if (!affiliateId) return sendJson(res, 400, { error: 'Affiliate ID is required' });

    const affiliateIndex = affiliatesDb.findIndex(a => a.id === affiliateId);
    if (affiliateIndex === -1) return sendJson(res, 404, { error: 'Affiliate not found' });

    const affiliate = affiliatesDb[affiliateIndex];

    // Remove from database
    affiliatesDb.splice(affiliateIndex, 1);

    // Clear any active sessions for this affiliate
    for (const [token, sess] of affiliateSessions.entries()) {
      if (sess.affiliateId === affiliateId) {
        affiliateSessions.delete(token);
      }
    }

    await persistAffiliatesDb();

    log('Admin permanently deleted affiliate', {
      affiliateId,
      email: affiliate.email,
      admin: ADMIN_USER
    });

    sendJson(res, 200, { ok: true, message: 'Affiliate permanently deleted' });
  } catch (error) {
    console.error('Error deleting affiliate:', error);
    sendJson(res, 500, { error: error.message || 'Unable to delete affiliate' });
  }
}

// Tracking API endpoints
async function handleAdminTrackingStats(req, res) {
  const session = requireAdminAuth(req, res);
  if (!session) return;
  
  try {
    // Convert dailyVisits for serialization
    const dailyVisits = {};
    for (const [date, data] of Object.entries(trackingData.summary.dailyVisits)) {
      dailyVisits[date] = {
        count: data.count,
        uniqueVisitors: data.uniqueVisitors instanceof Set 
          ? data.uniqueVisitors.size 
          : (Array.isArray(data.uniqueVisitors) ? data.uniqueVisitors.length : 0)
      };
    }

    // Get top referrers
    const referrersList = Object.entries(trackingData.summary.referrers)
      .map(([domain, count]) => ({ domain, count }))
      .sort((a, b) => b.count - a.count);

    // Get top pages
    const pagesList = Object.entries(trackingData.summary.pages)
      .map(([path, count]) => ({ path, count }))
      .sort((a, b) => b.count - a.count);

    // Get recent visits (last 100)
    const recentVisits = trackingData.visits.slice(-100).reverse();

    // Get comprehensive analytics summary
    const analytics = getAnalyticsSummary();

    const stats = {
      // Legacy tracking data
      totalVisits: trackingData.summary.totalVisits,
      uniqueVisitors: trackingData.summary.uniqueVisitors.size,
      topReferrers: referrersList.slice(0, 20),
      topPages: pagesList.slice(0, 20),
      dailyVisits: dailyVisits,
      recentVisits: recentVisits,
      referrersList: referrersList,
      pagesList: pagesList,
      conversions: trackingData.summary.conversions,
      conversionSources: trackingData.summary.conversionSources,
      financials: trackingData.summary.financials,
      referrersToUpgrade: Object.entries(trackingData.summary.referrersToUpgrade || {})
        .map(([domain, count]) => ({ domain, count }))
        .sort((a, b) => b.count - a.count)
        .slice(0, 10),
      upgradeSources: trackingData.summary.upgradeSources || {},
      retention: calculateRetention(),

      // Enhanced Analytics
      userEngagement: analytics.userEngagement,
      featureUsage: analytics.featureUsage,
      modelUsage: analytics.modelUsage,
      exportUsage: analytics.exportUsage,
      errorRates: analytics.errorRates,
      retentionCohorts: analytics.retentionCohorts,
      businessMetrics: analytics.businessMetrics,
      technicalMetrics: analytics.technicalMetrics,
      planUpgradePatterns: analytics.planUpgradePatterns,
      conversionFunnels: analytics.conversionFunnels,
      featureAdoptionByPlan: analytics.featureAdoptionByPlan,

      // Additional Insights
      sessionInsights: {
        totalSessions: Object.keys(trackingData.userAnalytics.userSessions).length,
        averageSessionDuration: analytics.userEngagement.averageSessionDuration,
        totalProjectsCreated: Object.keys(trackingData.userAnalytics.projectData).length,
        totalExports: Object.values(trackingData.userAnalytics.exportUsage).reduce((a, b) => a + b, 0),
        totalErrors: Object.values(trackingData.userAnalytics.errorRates).reduce((a, b) => a + b, 0)
      }
    };

    sendJson(res, 200, { ok: true, stats });
  } catch (error) {
    log('Failed to fetch tracking stats', { error: String(error) });
    sendJson(res, 500, { error: error.message || 'Unable to fetch tracking stats' });
  }
}

async function handleAdminCancelMessages(req, res) {
  const adminSession = requireAdminAuth(req, res);
  if (!adminSession) return;

  try {
    let totalCancelled = 0;
    let sessionsAffected = 0;
    let runningCancelled = 0;
    let queuedCancelled = 0;

    for (const session of state.sessions) {
      let sessionCancelled = 0;
      for (const message of (session.messages || [])) {
        if (message.status === 'running' || message.status === 'queued') {
          message.status = 'cancelled';
          message.finishedAt = new Date().toISOString();
          message.cancelledAt = new Date().toISOString();
          message.cancelledBy = 'admin';
          if (!message.reply) message.reply = '';
          if (!message.opencodeSummary) message.opencodeSummary = 'Cancelled by admin';

          if (message.status === 'running') {
            runningCancelled++;
            runningProcesses.delete(message.id);
            if (activeStreams.has(message.id)) {
              const streams = activeStreams.get(message.id);
              streams.forEach(streamRes => {
                try {
                  streamRes.write(`data: ${JSON.stringify({ type: 'cancelled', reason: 'admin_cancelled', timestamp: new Date().toISOString() })}\n\n`);
                  streamRes.end();
                } catch (e) { }
              });
              activeStreams.delete(message.id);
            }
          } else {
            queuedCancelled++;
          }

          sessionCancelled++;
          totalCancelled++;
        }
      }

      if (sessionCancelled > 0) {
        sessionsAffected++;
        sessionQueues.delete(session.id);
        session.updatedAt = new Date().toISOString();
      }
    }

    if (totalCancelled > 0) {
      await persistState();
    }

    log('Admin cancelled all messages', { totalCancelled, sessionsAffected, runningCancelled, queuedCancelled, adminId: adminSession.userId });

    sendJson(res, 200, {
      ok: true,
      totalCancelled,
      sessionsAffected,
      runningCancelled,
      queuedCancelled
    });
  } catch (error) {
    log('Failed to cancel messages', { error: String(error) });
    sendJson(res, 500, { error: error.message || 'Unable to cancel messages' });
  }
}

// Test Ollama connection from admin panel
async function handleAdminOllamaTest(req, res) {
  const adminSession = requireAdminAuth(req, res);
  if (!adminSession) return;

  try {
    const startTime = Date.now();
    
    // Log the configuration (without exposing the full API key)
    const apiKeyConfigured = !!OLLAMA_API_KEY;
    const apiKeyPreview = OLLAMA_API_KEY 
      ? `${OLLAMA_API_KEY.substring(0, 4)}...${OLLAMA_API_KEY.substring(OLLAMA_API_KEY.length - 4)}`
      : 'Not configured';
    
    console.log('[ADMIN] Testing Ollama connection', {
      url: OLLAMA_API_URL,
      model: OLLAMA_DEFAULT_MODEL,
      apiKeyConfigured,
      apiKeyPreview
    });

    // Make a test request to Ollama
    const testMessages = [
      { role: 'user', content: 'Say "Ollama test successful" and nothing else.' }
    ];

    let result;
    let error = null;
    
    try {
      result = await sendOllamaChat({ messages: testMessages, model: OLLAMA_DEFAULT_MODEL });
    } catch (err) {
      error = err;
      console.log('[ADMIN] Ollama test failed', { 
        error: err.message, 
        status: err.status,
        detail: err.detail 
      });
    }

    const duration = Date.now() - startTime;

    // Prepare the response
    const response = {
      ok: !error,
      duration,
      config: {
        url: OLLAMA_API_URL,
        model: OLLAMA_DEFAULT_MODEL,
        apiKeyConfigured,
        apiKeyPreview
      },
      result: error ? null : {
        reply: result.reply,
        model: result.model
      },
      error: error ? {
        message: error.message,
        status: error.status,
        detail: error.detail,
        isAuthError: error.isAuthError,
        isModelMissing: error.isModelMissing,
        code: error.code
      } : null
    };

    console.log('[ADMIN] Ollama test completed', { ok: response.ok, duration });
    sendJson(res, 200, response);
  } catch (error) {
    console.error('[ADMIN] Ollama test handler error', error);
    sendJson(res, 500, { 
      ok: false, 
      error: error.message || 'Internal server error during Ollama test' 
    });
  }
}

// Get detailed resource usage breakdown by session for admin panel
async function handleAdminResources(req, res) {
  const adminSession = requireAdminAuth(req, res);
  if (!adminSession) return;

  try {
    const mem = process.memoryUsage();
    const cpuUsage = process.cpuUsage();
    const now = Date.now();

    // Calculate per-session memory estimates and collect detailed info
    const sessionsData = state.sessions.map((session) => {
      const sessionAge = now - new Date(session.createdAt).getTime();
      const messages = (session.messages || []).map((msg) => {
        // Estimate message memory footprint
        let messageSize = 0;
        messageSize += (msg.content || '').length * 2; // UTF-16 characters
        messageSize += (msg.reply || '').length * 2;
        messageSize += (msg.partialOutput || '').length * 2;
        messageSize += (msg.opencodeSummary || '').length * 2;
        if (msg.attachments) {
          messageSize += JSON.stringify(msg.attachments).length * 2;
        }
        return {
          id: msg.id,
          role: msg.role,
          status: msg.status,
          model: msg.model,
          createdAt: msg.createdAt,
          finishedAt: msg.finishedAt,
          contentLength: (msg.content || '').length,
          replyLength: (msg.reply || '').length,
          estimatedMemoryBytes: messageSize,
          estimatedMemoryKb: (messageSize / 1024).toFixed(2) + ' KB',
          isRunning: msg.status === 'running',
          isQueued: msg.status === 'queued',
          isDone: msg.status === 'done',
          isError: msg.status === 'error'
        };
      });

      // Calculate session total
      const totalMessageMemory = messages.reduce((sum, m) => sum + m.estimatedMemoryBytes, 0);
      const runningMessages = messages.filter(m => m.isRunning);
      const queuedMessages = messages.filter(m => m.isQueued);
      const errorMessages = messages.filter(m => m.isError);

      return {
        id: session.id,
        userId: session.userId,
        title: session.title || 'Untitled',
        appId: session.appId || null,
        appName: session.appName || null,
        createdAt: session.createdAt,
        updatedAt: session.updatedAt,
        ageMs: sessionAge,
        age: formatDuration(sessionAge),
        messageCount: messages.length,
        messages: messages,
        runningMessages: runningMessages.length,
        queuedMessages: queuedMessages.length,
        errorMessages: errorMessages.length,
        totalMessageMemoryBytes: totalMessageMemory,
        totalMessageMemoryKb: (totalMessageMemory / 1024).toFixed(2) + ' KB',
        estimatedSessionMemoryKb: ((totalMessageMemory + 10240) / 1024).toFixed(2) + ' KB', // Base overhead
        pending: session.pending || 0,
        workspaceDir: session.workspaceDir ? session.workspaceDir.replace(WORKSPACES_ROOT, '...') : null,
        model: session.model || null,
        cli: session.cli || 'opencode',
        opencodeSessionId: session.opencodeSessionId || null,
        hasOpencodeSession: !!session.opencodeSessionId
      };
    });

    // Sort sessions by estimated memory usage (descending)
    const sortedSessions = sessionsData.sort((a, b) => b.totalMessageMemoryBytes - a.totalMessageMemoryBytes);

    // Get running processes info
    const runningProcessInfo = [];
    for (const [messageId, procInfo] of runningProcesses.entries()) {
      // Find the session and message for this process
      let sessionId = null;
      let messageContent = null;
      for (const session of state.sessions) {
        const msg = session.messages?.find(m => m.id === messageId);
        if (msg) {
          sessionId = session.id;
          messageContent = (msg.content || '').slice(0, 100);
          break;
        }
      }
      runningProcessInfo.push({
        messageId,
        sessionId,
        startTime: procInfo.startTime,
        age: formatDuration(now - procInfo.startTime),
        messagePreview: messageContent
      });
    }

    // Get child processes info
    const childProcessInfo = [];
    for (const [processId, info] of childProcesses.entries()) {
      const age = now - info.startTime;
      childProcessInfo.push({
        processId,
        pid: info.pid,
        sessionId: info.sessionId,
        messageId: info.messageId,
        startTime: info.startTime,
        age: formatDuration(age),
        ageMs: age
      });
    }

    // Get active streams info
    const streamInfo = [];
    for (const [messageId, streams] of activeStreams.entries()) {
      // Find the session for this message
      let sessionId = null;
      for (const session of state.sessions) {
        const msg = session.messages?.find(m => m.id === messageId);
        if (msg) {
          sessionId = session.id;
          break;
        }
      }
      streamInfo.push({
        messageId,
        sessionId,
        streamCount: streams.size,
        statuses: Array.from(streams).map(s => s.statusCode || 200)
      });
    }

    // OpenCode process manager stats
    const managerStats = opencodeManager.getStats ? opencodeManager.getStats() : { isRunning: false };

    // Calculate totals
    const totals = {
      sessions: sessionsData.length,
      totalMessages: sessionsData.reduce((sum, s) => sum + s.messageCount, 0),
      runningMessages: sessionsData.reduce((sum, s) => sum + s.runningMessages, 0),
      queuedMessages: sessionsData.reduce((sum, s) => sum + s.queuedMessages, 0),
      errorMessages: sessionsData.reduce((sum, s) => sum + s.errorMessages, 0),
      totalEstimatedMemoryMb: (sessionsData.reduce((sum, s) => sum + s.totalMessageMemoryBytes, 0) / (1024 * 1024)).toFixed(2) + ' MB'
    };

    // System load
    const loadAvg = os.loadavg();

    sendJson(res, 200, {
      system: {
        memory: {
          rss: `${(mem.rss / 1024 / 1024).toFixed(2)} MB`,
          heapTotal: `${(mem.heapTotal / 1024 / 1024).toFixed(2)} MB`,
          heapUsed: `${(mem.heapUsed / 1024 / 1024).toFixed(2)} MB`,
          external: `${(mem.external / 1024 / 1024).toFixed(2)} MB`,
          arrayBuffers: `${((mem.arrayBuffers || 0) / 1024 / 1024).toFixed(2)} MB`,
          raw: {
            rss: mem.rss,
            heapTotal: mem.heapTotal,
            heapUsed: mem.heapUsed,
            external: mem.external,
            arrayBuffers: mem.arrayBuffers || 0
          }
        },
        cpu: {
          user: cpuUsage.user,
          system: cpuUsage.system,
          userPercent: ((cpuUsage.user / 1000000 / process.uptime()) * 100).toFixed(1) + '%',
          systemPercent: ((cpuUsage.system / 1000000 / process.uptime()) * 100).toFixed(1) + '%',
          loadAvg1m: loadAvg[0].toFixed(2),
          loadAvg5m: loadAvg[1].toFixed(2),
          loadAvg15m: loadAvg[2].toFixed(2)
        },
        limits: {
          memoryBytes: RESOURCE_LIMITS.memoryBytes,
          memoryMb: `${(RESOURCE_LIMITS.memoryBytes / 1024 / 1024).toFixed(2)} MB`,
          cpuCores: RESOURCE_LIMITS.cpuCores,
          memoryPercentUsed: ((mem.rss / RESOURCE_LIMITS.memoryBytes) * 100).toFixed(1) + '%'
        },
        process: {
          uptime: process.uptime(),
          uptimeFormatted: formatDuration(process.uptime() * 1000),
          pid: process.pid,
          nodeVersion: process.version,
          platform: process.platform,
          arch: process.arch
        }
      },
      totals,
      sessions: sortedSessions,
      runningProcesses: runningProcessInfo,
      childProcesses: childProcessInfo,
      activeStreams: streamInfo,
      opencode: {
        mode: managerStats.isRunning ? 'singleton' : 'per-session',
        isReady: managerStats.isReady || false,
        pendingRequests: managerStats.pendingRequests || 0,
        sessionWorkspaces: managerStats.activeSessions || 0,
        runningInstances: managerStats.activeSessions || 0
      },
      maps: {
        sessionQueues: sessionQueues.size,
        activeStreams: activeStreams.size,
        runningProcesses: runningProcesses.size,
        childProcesses: childProcesses.size,
        oauthStates: typeof oauthStateStore !== 'undefined' ? oauthStateStore.size : 0,
        loginAttempts: typeof loginAttempts !== 'undefined' ? loginAttempts.size : 0,
        adminLoginAttempts: typeof adminLoginAttempts !== 'undefined' ? adminLoginAttempts.size : 0,
        apiRateLimit: typeof apiRateLimit !== 'undefined' ? apiRateLimit.size : 0
      },
      timestamp: new Date().toISOString()
    });
  } catch (error) {
    log('Failed to get resource usage', { error: String(error) });
    sendJson(res, 500, { error: error.message || 'Unable to fetch resource usage' });
  }
}

async function handleAdminExternalTestingStatus(req, res) {
  const adminSession = requireAdminAuth(req, res);
  if (!adminSession) return;
  const config = getExternalTestingConfig();
  sendJson(res, 200, {
    ok: true,
    config: {
      wpHost: config.wpHost || '',
      wpPath: config.wpPath || '',
      wpBaseUrl: config.wpBaseUrl || '',
      enableMultisite: !!config.enableMultisite,
      subsiteMode: config.subsiteMode,
      subsiteDomain: config.subsiteDomain || '',
      maxConcurrentTests: config.maxConcurrentTests,
      autoCleanup: !!config.autoCleanup,
      cleanupDelayMs: config.cleanupDelayMs,
      sshKeyConfigured: Boolean(config.wpSshKey),
    }
  });
}

async function handleAdminExternalTestingSelfTest(req, res) {
  const adminSession = requireAdminAuth(req, res);
  if (!adminSession) return;

  let tempDir;
  try {
    tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'pc-external-test-'));
    const pluginSlug = 'pc-external-test';
    const pluginDir = path.join(tempDir, pluginSlug);
    await fs.mkdir(pluginDir, { recursive: true });
    const pluginFile = path.join(pluginDir, `${pluginSlug}.php`);
    const pluginContents = `<?php\n/**\n * Plugin Name: External Testing Self Check\n * Plugin URI: https://plugincompass.com\n * Author: Plugin Compass\n * Version: 0.1.0\n */\n\nfunction pc_external_test_activate() {\n  update_option('pc_external_test_flag', 'ok');\n}\nregister_activation_hook(__FILE__, 'pc_external_test_activate');\n`;
    await fs.writeFile(pluginFile, pluginContents, 'utf8');

    const testInput = {
      plugin_path: pluginDir,
      plugin_slug: pluginSlug,
      test_mode: 'cli',
      test_scenarios: [
        {
          name: 'Activate self-test plugin',
          type: 'custom',
          wp_cli_command: `plugin activate ${pluginSlug}`,
          assertions: { wp_cli_success: true, not_contains: ['Fatal error', 'Parse error'] }
        },
        {
          name: 'Activation hook wrote option',
          type: 'custom',
          wp_cli_command: 'eval "echo get_option(\'pc_external_test_flag\') ? get_option(\'pc_external_test_flag\') : \"missing\";"',
          assertions: { contains: ['ok'] }
        }
      ]
    };

    const result = await externalWpTester.runTest(testInput, { configOverrides: {} });
    sendJson(res, 200, { ok: true, result });
  } catch (error) {
    sendJson(res, 500, { ok: false, error: error.message || 'Self-test failed' });
  } finally {
    if (tempDir) {
      fs.rm(tempDir, { recursive: true, force: true }).catch(() => {});
    }
  }
}

async function handleAdminOpenRouterSettingsPost(req, res) {
  const session = requireAdminAuth(req, res);
  if (!session) return;
  try {
    const body = await parseJsonBody(req);
    openrouterSettings = {
      primaryModel: (body.primaryModel || '').trim(),
      backupModel1: (body.backupModel1 || '').trim(),
      backupModel2: (body.backupModel2 || '').trim(),
      backupModel3: (body.backupModel3 || '').trim(),
    };
    await persistOpenRouterSettings();
    sendJson(res, 200, { ok: true, settings: openrouterSettings });
  } catch (error) {
    sendJson(res, 400, { error: error.message || 'Unable to save settings' });
  }
}

async function handleNewSession(req, res, userId) {
  try {
    const body = await parseJsonBody(req);
    const session = await createSession(
      {
        title: body.title,
        model: body.model,
        cli: body.cli,
        appId: body.appId || body.app || body.appSlug,
        reuseAppId: body.reuseAppId === true
      },
      userId
    );

    // Handle template loading
    if (body.templateId) {
      try {
        const templatePath = path.join(__dirname, 'templates', sanitizeSegment(body.templateId));
        // recursive copy function - explicitly defined here to avoid reference issues
        const copyDirRecursive = async (src, dest) => {
          await fs.mkdir(dest, { recursive: true });
          const entries = await fs.readdir(src, { withFileTypes: true });
          for (let entry of entries) {
            const srcPath = path.join(src, entry.name);
            const destPath = path.join(dest, entry.name);
            if (entry.isDirectory()) {
              await copyDirRecursive(srcPath, destPath);
            } else {
              await fs.copyFile(srcPath, destPath);
            }
          }
        };

        if (session.workspaceDir) {
          await copyDirRecursive(templatePath, session.workspaceDir);
          session.planSummary = `Started from template: ${body.templateId}`;
          session.planApproved = true; // Skip plan phase
          session.entryMode = 'opencode'; // Force builder to build mode
          log('Template loaded into session', { sessionId: session.id, templateId: body.templateId });
        }
      } catch (err) {
        log('Failed to copy template', { error: String(err) });
      }
    }

    await persistState();
    sendJson(res, 201, { session: serializeSession(session) });
  } catch (error) {
    const status = error.statusCode && Number.isInteger(error.statusCode) ? error.statusCode : 400;
    sendJson(res, status, { error: error.message || 'Unable to create session' });
  }
}

async function handleListSessions(req, res, userId) {
  // Allow optional filtering by appId query param to support client-side lookups
  try {
    const url = new URL(req.url || '', 'http://localhost');
    const rawAppId = url.searchParams.get('appId') || '';
    const appId = sanitizeSegment(rawAppId, '');
    const sessions = state.sessions.filter((s) => s.userId === userId && (!appId || s.appId === appId));
    sendJson(res, 200, { sessions: sessions.map(serializeSession) });
  } catch (err) {
    // Fallback: return all sessions if parsing fails
    const sessions = state.sessions.filter((s) => s.userId === userId);
    sendJson(res, 200, { sessions: sessions.map(serializeSession) });
  }
}

async function handleGetSession(_req, res, sessionId, userId) { const session = getSession(sessionId, userId); if (!session) return sendJson(res, 404, { error: 'Session not found' }); sendJson(res, 200, { session: serializeSession(session) }); }

async function handleGetSessionTodos(_req, res, sessionId, userId) {
  const session = getSession(sessionId, userId);
  if (!session) return sendJson(res, 404, { error: 'Session not found' });

  // Get all todos from messages in the session
  const allTodos = [];
  if (session.messages) {
    for (const msg of session.messages) {
      if (msg.todos && Array.isArray(msg.todos)) {
        // Add message ID to each todo for reference
        const todosWithMeta = msg.todos.map(todo => ({
          ...todo,
          messageId: msg.id,
          messageStatus: msg.status
        }));
        allTodos.push(...todosWithMeta);
      }
    }
  }

  sendJson(res, 200, { todos: allTodos });
}

async function handleNewMessage(req, res, sessionId, userId) {
  const session = getSession(sessionId, userId);
  if (!session) return sendJson(res, 404, { error: 'Session not found' });
  try {
    await ensureSessionPaths(session);
    const body = await parseJsonBody(req);
    
    // Security: Check for prompt injection attacks
    const rawContent = body.content || '';
    const securityCheck = shouldBlockUserInput(rawContent);
    if (securityCheck.blocked) {
      return sendJson(res, 400, { 
        error: 'Message blocked',
        blocked: true,
        reason: securityCheck.reason,
        message: 'This message was blocked due to potential security concerns. If you believe this is an error, please contact support.'
      });
    }
    
    const content = sanitizeMessage(rawContent);
    const displayContent = typeof body.displayContent === 'string' && body.displayContent.trim()
      ? body.displayContent.trim()
      : content;
    // Allow empty content if there are attachments
    const hasAttachments = Array.isArray(body.attachments) && body.attachments.length;
    if (!content && !hasAttachments) return sendJson(res, 400, { error: 'Message is required' });
    const userPlan = resolveUserPlan(session.userId);

    // Paid plans only: image attachments.
    if (hasAttachments) {
      const hasImage = body.attachments.some((a) => a && isImageMime(a.type));
      if (hasImage && !isPaidPlan(userPlan)) {
        return sendJson(res, 402, { error: 'Image uploads are available on Business and Enterprise plans only. Please upgrade to attach images.' });
      }
    }
    const model = resolvePlanModel(userPlan, body.model || session.model);
    const estimatedTokens = estimateTokensFromText(content) + TOKEN_ESTIMATION_BUFFER; // include headroom for reply
    const allowance = canConsumeTokens(session.userId, userPlan, estimatedTokens);
    if (!allowance.allowed) {
      const friendlyRemaining = allowance.remaining > 0 ? `${allowance.remaining.toLocaleString()} remaining` : 'no remaining balance';
      return sendJson(res, 402, { error: `You have reached your token allowance (${friendlyRemaining}). Upgrade or add a boost.`, allowance });
    }

    // If this request will trigger an external WP test, enforce plan limits immediately and return a friendly upgrade prompt
    if (body.isProceedWithBuild && body.externalTestingEnabled === true) {
      const user = findUserById(session.userId);
      const limitCheck = canUseExternalTesting(session.userId, userPlan, user?.unlimitedUsage === true);
      if (!limitCheck.allowed) {
        return sendJson(res, 402, { error: 'External WP CLI testing limit reached for your plan. Upgrade to run more tests.', externalTesting: limitCheck.summary, upgradeUrl: '/topup' });
      }
    }
    const cli = normalizeCli(body.cli || session.cli);
    const now = new Date().toISOString();
    const message = { id: randomUUID(), role: 'user', content, originalContent: content, displayContent, model, cli, status: 'queued', createdAt: now, updatedAt: now, opencodeTokensUsed: null };
    if (body.isProceedWithBuild) message.isProceedWithBuild = true;
    if (body.externalTestingEnabled !== undefined) {
      message.externalTestingEnabled = body.externalTestingEnabled === true;
    }
    // Copy continuation-related fields for background continuations
    if (body.isContinuation) message.isContinuation = true;
    if (body.isBackgroundContinuation) message.isBackgroundContinuation = true;
    if (body.originalMessageId) message.originalMessageId = body.originalMessageId;
    // Preserve opencodeSessionId for session continuity in retries/continuations
    // Also prioritize body.opencodeSessionId over session.opencodeSessionId for explicit continuation
    // CRITICAL: If session.opencodeSessionId is null (session was reset), ignore body.opencodeSessionId
    // to prevent reusing a corrupted session that was previously reset
    if (session.opencodeSessionId === null && body.opencodeSessionId) {
      log('Ignoring body opencodeSessionId because session was reset', { 
        bodySessionId: body.opencodeSessionId,
        reason: 'Session was reset due to corruption/undo failure - forcing new session creation'
      });
      // Don't set message.opencodeSessionId - let ensureOpencodeSession create a new one
    } else if (body.opencodeSessionId) {
      message.opencodeSessionId = body.opencodeSessionId;
      log('Using explicit opencodeSessionId from request body', { opencodeSessionId: body.opencodeSessionId });
    } else if (session.opencodeSessionId) {
      message.opencodeSessionId = session.opencodeSessionId;
      log('Inheriting opencodeSessionId from session', { opencodeSessionId: session.opencodeSessionId });
    }
    // Process attachments if provided as base64 JSON entries
    if (hasAttachments) {
      message.attachments = [];
      for (const att of body.attachments) {
        try {
          if (!att.data || !att.type) continue;

          // Validate file size
          const buffer = Buffer.from(att.data, 'base64');
          if (buffer.length > MAX_ATTACHMENT_SIZE) { log('attachment too large, skipping', { size: buffer.length, max: MAX_ATTACHMENT_SIZE, name: att.name }); continue; }

          // Validate MIME type
          const clientMimeType = att.type.toLowerCase();
          const allowedMimeTypes = [
            'image/png', 'image/jpeg', 'image/gif', 'image/svg+xml', 'image/webp',
            'application/pdf', 'text/plain', 'text/css', 'application/javascript',
            'text/html', 'text/markdown', 'text/csv', 'application/json',
            'application/xml', 'text/xml'
          ];

          if (!allowedMimeTypes.includes(clientMimeType)) {
            log('attachment rejected - invalid mime type', { mimeType: clientMimeType, name: att.name });
            continue;
          }

          // Magic byte verification for images
          if (clientMimeType.startsWith('image/')) {
            if (!validateImageSignature(clientMimeType, buffer)) {
              log('attachment rejected - invalid image signature', { mimeType: clientMimeType, name: att.name });
              continue;
            }
          }

          // Compress images before storing.
          let storedBuffer = buffer;
          let storedMimeType = clientMimeType;
          let forcedExt = null;
          if (clientMimeType.startsWith('image/')) {
            const compressed = await compressImageBuffer(buffer, clientMimeType);
            storedBuffer = compressed.buffer;
            storedMimeType = compressed.mimeType;
            forcedExt = compressed.ext;
          }

          const id = `${randomUUID()}-${safeFileNamePart(att.name)}`;
          const ext = forcedExt || extensionForMime(storedMimeType);
          const safeExt = String(ext || 'bin').replace(/[^a-zA-Z0-9]/g, '').slice(0, 10) || 'bin';
          const filename = `${id}.${safeExt}`;
          const outPath = path.join(session.uploadsDir, filename);
          await fs.writeFile(outPath, storedBuffer);
          const attachmentUrl = `/uploads/${session.id}/${session.attachmentKey}/${filename}`;
          message.attachments.push({ name: att.name || filename, type: storedMimeType, url: attachmentUrl, size: storedBuffer.length, originalType: clientMimeType, originalSize: buffer.length });
        } catch (err) {
          log('attachment save failed', { err: String(err), attName: att.name });
        }
      }
    }
    session.messages.push(message);
    session.model = model;
    session.cli = cli;
    session.updatedAt = now;
    updatePending(sessionId, 1, session.userId);
    await persistState();
    await queueMessage(sessionId, message);
    sendJson(res, 202, { message });
  } catch (error) {
    sendJson(res, 400, { error: error.message || 'Unable to queue message' });
  }
}

// SSE endpoint for real-time streaming
async function handleMessageStream(req, res, sessionId, messageId, userId) {
  const session = getSession(sessionId, userId);
  if (!session) return sendJson(res, 404, { error: 'Session not found' });

  const message = session.messages.find(m => m.id === messageId);
  if (!message) return sendJson(res, 404, { error: 'Message not found' });

  // Set up SSE headers
  res.writeHead(200, {
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache',
    'Connection': 'keep-alive',
    'X-Accel-Buffering': 'no', // Disable nginx buffering
  });

  try { req.socket.setTimeout(0); } catch (_) { }
  try { req.socket.setKeepAlive(true); } catch (_) { }
  try { res.flushHeaders(); } catch (_) { }

  // Register this stream
  if (!activeStreams.has(messageId)) {
    activeStreams.set(messageId, new Set());
  }
  activeStreams.get(messageId).add(res);

  log('SSE stream opened', { sessionId, messageId, activeStreams: activeStreams.size });

  // Declare timers before cleanupStream to avoid hoisting issues
  let heartbeat = null;
  let streamTimeout = null;

  // Helper to cleanup this specific stream
  const cleanupStream = () => {
    clearInterval(heartbeat);
    clearTimeout(streamTimeout);
    if (activeStreams.has(messageId)) {
      activeStreams.get(messageId).delete(res);
      if (activeStreams.get(messageId).size === 0) {
        activeStreams.delete(messageId);
      }
    }
  };

  // Send initial status
  const initialData = JSON.stringify({
    type: 'start',
    messageId,
    status: message.status,
    timestamp: new Date().toISOString()
  });
  try {
    res.write(`data: ${initialData}\n\n`);
  } catch (err) {
    cleanupStream();
    return;
  }

  // If message already has output, send it
  if (message.partialOutput) {
    const catchupData = JSON.stringify({
      type: 'chunk',
      content: message.partialOutput,
      filtered: message.partialOutput,
      outputType: message.outputType,
      partialOutput: message.partialOutput,
      timestamp: message.partialUpdatedAt || new Date().toISOString()
    });
    try {
      res.write(`data: ${catchupData}\n\n`);
    } catch (_) {}
  }

  // If message is already done, send completion
  if (message.status === 'done' || message.status === 'error' || message.status === 'cancelled' || message.status === 'skipped') {
    const completeData = JSON.stringify({
      type: message.status === 'error' ? 'error' : 'complete',
      content: message.reply || message.partialOutput,
      error: message.error,
      outputType: message.outputType,
      exitCode: message.opencodeExitCode,
      timestamp: message.finishedAt || new Date().toISOString()
    });
    try {
      res.write(`data: ${completeData}\n\n`);
      res.end();
    } catch (_) {}
    cleanupStream();
    return;
  }

  // Stream timeout - close streams that have been open too long (30 minutes max)
  const STREAM_MAX_DURATION_MS = 30 * 60 * 1000;
  streamTimeout = setTimeout(() => {
    try {
      const timeoutData = JSON.stringify({
        type: 'timeout',
        message: 'Stream timeout - please refresh to reconnect',
        timestamp: new Date().toISOString()
      });
      res.write(`data: ${timeoutData}\n\n`);
      res.end();
    } catch (_) {}
    cleanupStream();
    log('SSE stream timeout', { sessionId, messageId });
  }, STREAM_MAX_DURATION_MS);

  // Keep connection alive with heartbeat/pings.
  // Send a small data event periodically so proxies/load balancers don't treat the stream as idle.
  let heartbeatCount = 0;
  heartbeat = setInterval(() => {
    try {
      heartbeatCount++;
      
      // Check if message has completed while stream was open
      const currentMsg = session.messages.find(m => m.id === messageId);
      if (currentMsg && (currentMsg.status === 'done' || currentMsg.status === 'error' || currentMsg.status === 'cancelled')) {
        // Message completed - send final status and close
        const completeData = JSON.stringify({
          type: currentMsg.status === 'error' ? 'error' : 'complete',
          content: currentMsg.reply || currentMsg.partialOutput,
          error: currentMsg.error,
          outputType: currentMsg.outputType,
          exitCode: currentMsg.opencodeExitCode,
          timestamp: currentMsg.finishedAt || new Date().toISOString()
        });
        res.write(`data: ${completeData}\n\n`);
        res.end();
        cleanupStream();
        return;
      }
      
      res.write(`: heartbeat ${heartbeatCount} ${Date.now()}\n\n`);
      const healthData = JSON.stringify({
        type: 'health',
        timestamp: new Date().toISOString(),
        heartbeatCount,
        status: currentMsg?.status || message.status
      });
      res.write(`data: ${healthData}\n\n`);
    } catch (err) {
      cleanupStream();
    }
  }, 15000);

  // Clean up on client disconnect
  req.on('close', () => {
    cleanupStream();
    log('SSE stream closed by client', { sessionId, messageId });
  });
  
  // Also handle errors
  req.on('error', (err) => {
    cleanupStream();
    log('SSE stream error', { sessionId, messageId, error: String(err) });
  });
  
  res.on('error', (err) => {
    cleanupStream();
    log('SSE response error', { sessionId, messageId, error: String(err) });
  });
}

// Check if opencode is currently running for a message
async function handleRunningStatus(req, res, sessionId, messageId, userId) {
  const session = getSession(sessionId, userId);
  if (!session) return sendJson(res, 404, { error: 'Session not found' });

  const message = session.messages.find(m => m.id === messageId);
  if (!message) return sendJson(res, 404, { error: 'Message not found' });

  const isRunning = runningProcesses.has(messageId);
  const processInfo = runningProcesses.get(messageId);

  sendJson(res, 200, {
    running: isRunning,
    status: message.status,
    process: processInfo ? {
      started: processInfo.started,
      duration: Date.now() - processInfo.started,
      cli: processInfo.cli,
      model: processInfo.model
    } : null,
    hasOutput: !!(message.partialOutput || message.reply),
    outputType: message.outputType
  });
}

// Get current status of opencode CLI
async function handleOpencodeStatus(req, res) {
  const cliCommand = resolveCliCommand('opencode');
  
  // Get OpenCode process manager stats
  const managerStats = opencodeManager.getStats();
  
  try {
    const { stdout } = await runCommand(cliCommand, ['--version'], { timeout: 3000 });
    sendJson(res, 200, {
      available: true,
      version: stdout.trim(),
      command: cliCommand,
      runningProcesses: runningProcesses.size,
      activeStreams: activeStreams.size,
      processManager: {
        ...managerStats,
        mode: managerStats.isRunning ? 'singleton' : 'per-session',
        description: managerStats.isRunning 
          ? 'All sessions sharing single OpenCode instance' 
          : 'Each message spawns separate OpenCode process'
      }
    });
  } catch (error) {
    sendJson(res, 200, {
      available: false,
      error: error.message,
      command: cliCommand,
      runningProcesses: runningProcesses.size,
      activeStreams: activeStreams.size,
      processManager: {
        ...managerStats,
        mode: managerStats.isRunning ? 'singleton' : 'per-session'
      }
    });
  }
}

// Get memory and resource statistics for monitoring
async function handleMemoryStats(req, res) {
  const mem = process.memoryUsage();
  const now = Date.now();
  
  // Count sessions and messages
  let totalMessages = 0;
  let runningMessages = 0;
  let queuedMessages = 0;
  let oldSessions = 0;
  
  for (const session of state.sessions) {
    if (session.messages) {
      totalMessages += session.messages.length;
      for (const msg of session.messages) {
        if (msg.status === 'running') runningMessages++;
        if (msg.status === 'queued') queuedMessages++;
      }
    }
    // Check for old sessions
    const sessionAge = now - new Date(session.createdAt).getTime();
    if (sessionAge > SESSION_MAX_AGE_MS) oldSessions++;
  }
  
  sendJson(res, 200, {
    memory: {
      rss: `${(mem.rss / 1024 / 1024).toFixed(2)} MB`,
      heapTotal: `${(mem.heapTotal / 1024 / 1024).toFixed(2)} MB`,
      heapUsed: `${(mem.heapUsed / 1024 / 1024).toFixed(2)} MB`,
      external: `${(mem.external / 1024 / 1024).toFixed(2)} MB`,
      arrayBuffers: `${((mem.arrayBuffers || 0) / 1024 / 1024).toFixed(2)} MB`,
      raw: {
        rss: mem.rss,
        heapTotal: mem.heapTotal,
        heapUsed: mem.heapUsed,
        external: mem.external,
        arrayBuffers: mem.arrayBuffers || 0
      }
    },
    limits: {
      memoryBytes: RESOURCE_LIMITS.memoryBytes,
      memoryMb: `${(RESOURCE_LIMITS.memoryBytes / 1024 / 1024).toFixed(2)} MB`,
      cpuCores: RESOURCE_LIMITS.cpuCores,
      softMemoryRatio: RESOURCE_MEMORY_SOFT_RATIO,
      softMemoryBytes: RESOURCE_LIMITS.memoryBytes * RESOURCE_MEMORY_SOFT_RATIO
    },
    processes: {
      uptime: process.uptime(),
      pid: process.pid,
      cpuUsage: process.cpuUsage(),
      resourceReservations: resourceReservations
    },
    maps: {
      sessions: state.sessions.length,
      sessionQueues: sessionQueues.size,
      activeStreams: activeStreams.size,
      runningProcesses: runningProcesses.size,
      childProcesses: childProcesses.size,
      oauthStates: typeof oauthStateStore !== 'undefined' ? oauthStateStore.size : 0,
      loginAttempts: typeof loginAttempts !== 'undefined' ? loginAttempts.size : 0,
      adminLoginAttempts: typeof adminLoginAttempts !== 'undefined' ? adminLoginAttempts.size : 0,
      apiRateLimit: typeof apiRateLimit !== 'undefined' ? apiRateLimit.size : 0
    },
    messages: {
      total: totalMessages,
      running: runningMessages,
      queued: queuedMessages
    },
    sessions: {
      total: state.sessions.length,
      old: oldSessions
    },
    cleanup: {
      lastCleanup: lastMemoryCleanup,
      lastCleanupAgo: `${Math.round((now - lastMemoryCleanup) / 1000)}s ago`,
      cleanupIntervalMs: MEMORY_CLEANUP_INTERVAL_MS
    },
    load: {
      loadAvg: os.loadavg(),
      freeMem: `${(os.freemem() / 1024 / 1024).toFixed(2)} MB`,
      totalMem: `${(os.totalmem() / 1024 / 1024).toFixed(2)} MB`
    }
  });
}

// Trigger manual memory cleanup (admin only)
async function handleForceMemoryCleanup(req, res) {
  const session = requireAdminAuth(req, res);
  if (!session) return;
  
  const beforeMem = process.memoryUsage();
  triggerMemoryCleanup('admin_manual');
  const afterMem = process.memoryUsage();
  
  sendJson(res, 200, {
    ok: true,
    before: {
      rss: `${(beforeMem.rss / 1024 / 1024).toFixed(2)} MB`,
      heapUsed: `${(beforeMem.heapUsed / 1024 / 1024).toFixed(2)} MB`
    },
    after: {
      rss: `${(afterMem.rss / 1024 / 1024).toFixed(2)} MB`,
      heapUsed: `${(afterMem.heapUsed / 1024 / 1024).toFixed(2)} MB`
    },
    freed: {
      rss: `${((beforeMem.rss - afterMem.rss) / 1024 / 1024).toFixed(2)} MB`,
      heapUsed: `${((beforeMem.heapUsed - afterMem.heapUsed) / 1024 / 1024).toFixed(2)} MB`
    }
  });
}

// Handle undo request - sends /undo command to opencode to revert file changes
async function handleUndoMessage(req, res, sessionId, messageId, userId) {
  const session = getSession(sessionId, userId);
  if (!session) return sendJson(res, 404, { error: 'Session not found' });

  const message = session.messages.find(m => m.id === messageId);
  if (!message) return sendJson(res, 404, { error: 'Message not found' });

  // Only allow undo for opencode messages
  if (message.cli !== 'opencode') {
    return sendJson(res, 400, { error: 'Undo only available for opencode messages' });
  }

  try {
    // Check if message had any output - if not, skip the opencode undo
    const hasOutput = !!(message.reply || message.partialOutput) || message.status === 'done';

    if (hasOutput) {
      log('Sending undo command to opencode', { sessionId, messageId, opencodeSessionId: session.opencodeSessionId });

      const cliCommand = resolveCliCommand('opencode');
      const args = ['--message', '/undo'];

      // Add session if we have one
      if (session.opencodeSessionId) {
        args.push('--session', session.opencodeSessionId);
      }

      await runCommand(cliCommand, args, {
        cwd: session.workspaceDir || REPO_ROOT,
        timeout: 30000
      });
    } else {
      log('Skipping undo command - message had zero output', { sessionId, messageId });
      
      // If the message failed/had error, reset the opencode session to ensure fresh start
      if (message.error || message.status === 'error') {
        log('Resetting opencode session due to failed message', { 
          sessionId, 
          messageId, 
          previousSessionId: session.opencodeSessionId,
          messageStatus: message.status,
          hasError: !!message.error
        });
        session.opencodeSessionId = null;
        session.initialOpencodeSessionId = null;
      }
    }

    // Clear todos from the message when undone
    if (message.todos) {
      const todoCount = message.todos.length;
      message.todos = [];
      log('Cleared todos from undone message', { sessionId, messageId, clearedCount: todoCount });
    }

    // Remove the message from session history
    const messageIndex = session.messages.findIndex(m => m.id === messageId);
    if (messageIndex !== -1) {
      session.messages.splice(messageIndex, 1);
      log('Removed message from history', { sessionId, messageId, remainingMessages: session.messages.length });
    }

    // Persist the updated state
    await persistState();

    log('Undo command completed', { sessionId, messageId });
    sendJson(res, 200, { ok: true, message: 'Undo command sent successfully' });
  } catch (error) {
    log('Undo command failed', { sessionId, messageId, error: String(error) });
    
    // Reset opencode session since it's likely corrupted
    log('Resetting opencode session due to undo failure', { 
      sessionId, 
      messageId, 
      previousSessionId: session.opencodeSessionId 
    });
    session.opencodeSessionId = null;
    session.initialOpencodeSessionId = null;
    
    // Still try to remove the message and clear todos even if opencode undo failed
    try {
      if (message.todos) {
        message.todos = [];
      }
      const messageIndex = session.messages.findIndex(m => m.id === messageId);
      if (messageIndex !== -1) {
        session.messages.splice(messageIndex, 1);
      }
      await persistState();
      log('Message removed despite undo command failure', { sessionId, messageId });
    } catch (cleanupErr) {
      log('Failed to cleanup message after undo failure', { sessionId, messageId, error: String(cleanupErr) });
    }
    
    // Return success since the message is removed from history
    sendJson(res, 200, { ok: true, message: 'Undo completed (session reset)' });
  }
}

async function handleRedoMessage(req, res, sessionId, messageId, userId) {
  const session = getSession(sessionId, userId);
  if (!session) return sendJson(res, 404, { error: 'Session not found' });

  const message = session.messages.find(m => m.id === messageId);
  if (!message) return sendJson(res, 404, { error: 'Message not found' });

  // Only allow redo for opencode messages
  if (message.cli !== 'opencode') {
    return sendJson(res, 400, { error: 'Redo only available for opencode messages' });
  }

  // Ensure we have an opencode session
  if (!session.opencodeSessionId) {
    return sendJson(res, 400, { error: 'No opencode session found. Cannot redo without an active session.' });
  }

  try {
    log('Redoing message', { sessionId, messageId, opencodeSessionId: session.opencodeSessionId });

    // Clear the message state to trigger reprocessing
    // Clear content/response
    message.content = message.originalContent || message.content;
    message.response = null;
    message.error = null;
    message.status = 'queued';
    message.completedAt = null;
    message.toolCalls = null;
    message.toolResults = null;
    message.streamBuffer = '';
    message.fullResponse = null;
    
    // Clear todos from the message when redone (they will be regenerated during the new execution)
    if (message.todos) {
      const todoCount = message.todos.length;
      message.todos = [];
      log('Cleared todos from redone message', { sessionId, messageId, clearedCount: todoCount });
    }

    // Persist the state change
    await persistState();
    
    // Trigger message processing (non-blocking)
    processMessage(sessionId, message).catch(err => {
      log('Redo message processing failed', { sessionId, messageId, error: String(err) });
    });

    log('Redo initiated successfully', { sessionId, messageId });
    sendJson(res, 200, { ok: true, message: 'Redo initiated successfully' });
  } catch (error) {
    log('Redo failed', { sessionId, messageId, error: String(error) });
    sendJson(res, 500, { error: `Redo failed: ${error.message}` });
  }
}

// Add small debug logging for message lifecycle

function traceMessageLifecycle(stage, sessionId, message) {
  try {
    const snippet = (message && message.content) ? (String(message.content).slice(0, 200)) : '';
    log(`message ${stage}`, { sessionId, messageId: message && message.id, model: message && message.model, snippet });
  } catch (err) {
    log('traceMessageLifecycle error', { err: String(err) });
  }
}

async function handleUpdateSession(req, res, sessionId, userId) {
  const session = getSession(sessionId, userId);
  if (!session) return sendJson(res, 404, { error: 'Session not found' });
  try {
    const body = await parseJsonBody(req);
    let changed = false;
    if (body.model && typeof body.model === 'string' && body.model.trim()) {
      const plan = resolveUserPlan(session.userId);
      session.model = resolvePlanModel(plan, body.model.trim());
      session.updatedAt = new Date().toISOString();
      changed = true;
    }
    if (body.title && typeof body.title === 'string' && body.title.trim()) {
      session.title = body.title.trim();
      session.updatedAt = new Date().toISOString();
      changed = true;
    }
    if (body.cli && typeof body.cli === 'string') {
      session.cli = normalizeCli(body.cli);
      session.updatedAt = new Date().toISOString();
      changed = true;
    }
    if (changed) await persistState();
    sendJson(res, 200, { session: serializeSession(session) });
  } catch (error) {
    sendJson(res, 400, { error: error.message || 'Unable to update session' });
  }
}

async function handleDeleteSession(req, res, sessionId, userId) {
  const idx = state.sessions.findIndex((s) => s.id === sessionId && s.userId === userId);
  if (idx === -1) return sendJson(res, 404, { error: 'Session not found' });
  const removed = state.sessions.splice(idx, 1)[0];
  // Remove any queue for the session
  if (sessionQueues.has(sessionId)) sessionQueues.delete(sessionId);
  // Delete workspace directory from disk
  if (removed.workspaceDir) {
    const workspaceRoot = path.resolve(WORKSPACES_ROOT);
    const workspacePath = path.resolve(removed.workspaceDir);
    if (workspacePath.startsWith(workspaceRoot)) {
      try {
        await fs.rm(workspacePath, { recursive: true, force: true });
        log('workspace directory deleted', { sessionId, workspacePath });
      } catch (err) {
        log('workspace directory cleanup failed', { sessionId, workspacePath, err: String(err) });
      }
    }
  }
  try { await persistState(); } catch (err) { log('failed to persist state after delete', { err: String(err) }); }
  log('session deleted', { id: sessionId });
  sendJson(res, 200, { ok: true, session: serializeSession(removed) });
}

async function handleDiagnostics(_req, res) {
  try {
    const versionRes = await runCommand('opencode', ['--version'], { timeout: 5000 }).catch((e) => ({ stdout: e.stdout, stderr: e.stderr, code: e.code }));
    const modelRes = await runCommand('opencode', ['models'], { timeout: 10000 }).catch((e) => ({ stdout: e.stdout, stderr: e.stderr, code: e.code }));
    sendJson(res, 200, { version: versionRes.stdout || versionRes.stderr, modelsOutput: modelRes.stdout || modelRes.stderr });
  } catch (error) {
    sendJson(res, 500, { error: String(error.message || error) });
  }
}

async function handleAccountClaim(req, res, userId) {
  try {
    // Require proper user authentication for account claiming
    const session = requireUserAuth(req, res);
    if (!session) return;

    const body = await parseJsonBody(req);
    const previousUserId = sanitizeSegment(body.previousUserId || body.deviceUserId || body.fromUserId || '', '');
    const targetUserId = session.userId; // Use the authenticated user ID

    if (!previousUserId || previousUserId === targetUserId) {
      return sendJson(res, 200, { ok: true, moved: 0, skipped: 0 });
    }

    const result = await migrateUserSessions(previousUserId, targetUserId);
    log('account claim completed', { previousUserId, targetUserId, moved: result.moved, skipped: result.skipped });
    return sendJson(res, 200, { ok: true, moved: result.moved, skipped: result.skipped });
  } catch (error) {
    return sendJson(res, 400, { error: error.message || 'Unable to claim account' });
  }
}

async function handleUploadApp(req, res, userId) {
  const plan = resolveUserPlan(userId);
  if (!isPaidPlan(plan)) {
    return sendJson(res, 403, { error: 'Uploading existing apps is available on Business and Enterprise plans.' });
  }

  // Base64 adds ~33% overhead (4/3 ratio); allow a small buffer over that.
  const uploadLimit = Math.max(MAX_JSON_BODY_SIZE, Math.ceil(MAX_UPLOAD_ZIP_SIZE * BASE64_OVERHEAD_MULTIPLIER));
  let body;
  try {
    body = await parseJsonBody(req, uploadLimit);
  } catch (err) {
    return sendJson(res, 400, { error: err.message || 'Invalid upload payload' });
  }

  const rawData = (body.data || body.fileData || '').toString();
  if (!rawData) return sendJson(res, 400, { error: 'ZIP file is required' });

  let zipBuffer;
  try {
    zipBuffer = decodeBase64Payload(rawData);
  } catch (err) {
    return sendJson(res, 400, { error: 'Could not decode uploaded file' });
  }

  if (!zipBuffer || !zipBuffer.length) return sendJson(res, 400, { error: 'ZIP file is empty' });
  if (zipBuffer.length > MAX_UPLOAD_ZIP_SIZE) {
    const mb = Math.ceil(MAX_UPLOAD_ZIP_SIZE / (1024 * 1024));
    return sendJson(res, 413, { error: `ZIP too large. Maximum size is ${mb} MB.` });
  }
  if (!isLikelyZip(zipBuffer)) return sendJson(res, 400, { error: 'Only ZIP archives are supported.' });

  const displayName = (body.title || body.name || body.fileName || 'Uploaded App').toString().trim() || 'Uploaded App';
  const baseName = path.parse(body.fileName || displayName).name || displayName;
  const desiredAppId = sanitizeSegment(body.appId || baseName, '');

  let session;
  try {
    session = await createSession({
      title: displayName,
      appId: desiredAppId,
      cli: 'opencode',
      model: 'default',
      entryMode: 'opencode',
      source: 'upload',
      planApproved: true,
    }, userId, desiredAppId);

    // Ensure workspace exists and is clean
    if (session.workspaceDir) {
      const workspaceRoot = path.resolve(WORKSPACES_ROOT);
      const workspacePath = path.resolve(session.workspaceDir);
      if (!workspacePath.startsWith(workspaceRoot)) throw new Error('Invalid workspace path');
      await fs.rm(workspacePath, { recursive: true, force: true }).catch((err) => {
        log('workspace cleanup failed', { path: workspacePath, err: String(err) });
      });
      await fs.mkdir(workspacePath, { recursive: true });
    }
    if (session.uploadsDir) {
      await fs.mkdir(session.uploadsDir, { recursive: true }).catch(() => { });
    }

    const files = await extractZipToWorkspace(zipBuffer, session.workspaceDir);
    session.planSummary = session.planSummary || 'Imported from ZIP upload';
    session.planUserRequest = session.planUserRequest || displayName;
    
    // Try to detect and preserve version from imported plugin
    try {
      // Collect PHP files to find main plugin file
      const validFiles = [];
      await collectValidFiles(session.workspaceDir, session.workspaceDir, validFiles, [
        'node_modules',
        '.git',
        '.data',
        'uploads',
        '*.log',
        '*.zip'
      ]);
      
      // Find main plugin file and extract version
      for (const fileInfo of validFiles) {
        if (fileInfo.fullPath.endsWith('.php')) {
          try {
            const content = await fs.readFile(fileInfo.fullPath, 'utf-8');
            if (content.includes('Plugin Name:')) {
              const versions = versionManager.extractAllVersions(content);
              if (versions.detectedVersion) {
                session.pluginVersion = versions.detectedVersion;
                session.pluginVersionHistory = [{
                  version: versions.detectedVersion,
                  previousVersion: null,
                  bumpType: 'import',
                  timestamp: new Date().toISOString(),
                  fileUpdated: fileInfo.relativePath,
                  note: 'Imported from uploaded ZIP'
                }];
                log('Detected version from imported plugin', {
                  sessionId: session.id,
                  version: versions.detectedVersion,
                  file: fileInfo.relativePath
                });
                break;
              }
            }
          } catch (err) {
            // Continue to next file
          }
        }
      }
    } catch (versionError) {
      log('Version detection failed during import', { error: String(versionError) });
      // Don't fail the import if version detection fails
    }
    
    await persistState();
    return sendJson(res, 201, { session: serializeSession(session), files });
  } catch (error) {
    // Clean up the failed session if it exists
    if (session) {
      const idx = state.sessions.findIndex((s) => s.id === session.id);
      if (idx !== -1) state.sessions.splice(idx, 1);
      try { await persistState(); } catch (_) { }
      if (session.workspaceDir) {
        try { await fs.rm(session.workspaceDir, { recursive: true, force: true }); } catch (_) { }
      }
    }
    return sendJson(res, 500, { error: `Unable to import ZIP: ${error.message}` });
  }
}

// Export app as ZIP file
async function handleExportZip(_req, res, sessionId, userId) {
  try {
    const session = getSession(sessionId, userId);
    if (!session) return sendJson(res, 404, { error: 'Session not found' });
    await ensureSessionPaths(session);

    // Track export start
    const userPlan = resolveUserPlan(session.userId);
    trackUserSession(session.userId, 'export', { 
      exportType: 'zip',
      sessionId: sessionId,
      appId: session.appId,
      plan: userPlan
    });
    trackFeatureUsage('app_export', session.userId, userPlan);
    trackConversionFunnel('app_export', 'export_started', session.userId, {
      sessionId: sessionId,
      appId: session.appId,
      plan: userPlan
    });

    log('Starting export to ZIP...', { sessionId, userId: session.userId, workspace: session.workspaceDir });

    // Check if workspace directory exists and has files
    let validFiles = [];
    try {
      await fs.access(session.workspaceDir);
      // Collect all valid files with their relative paths
      await collectValidFiles(session.workspaceDir, session.workspaceDir, validFiles, [
        'node_modules',
        '.git',
        '.data',
        'uploads',
        '*.log',
        '*.zip'
      ]);

      if (validFiles.length === 0) {
        log('Workspace is empty', { workspace: session.workspaceDir });
        return sendJson(res, 400, {
          error: 'No app content found to export. Please create or build your app first before exporting.'
        });
      }

      // Check file count limit
      if (validFiles.length > MAX_EXPORT_FILE_COUNT) {
        log('Workspace exceeds file count limit', { workspace: session.workspaceDir, fileCount: validFiles.length, limit: MAX_EXPORT_FILE_COUNT });
        return sendJson(res, 400, {
          error: `Export contains too many files (${validFiles.length}). Maximum allowed is ${MAX_EXPORT_FILE_COUNT} files.`
        });
      }
    } catch (accessError) {
      log('Workspace directory not accessible', { workspace: session.workspaceDir, error: String(accessError) });
      return sendJson(res, 400, {
        error: 'App workspace not found. Please create your app first before exporting.'
      });
    }

    const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
    
    // Use pluginSlug for ZIP filename if available to ensure WordPress recognizes same plugin across versions
    const exportBaseName = session.pluginSlug || sanitizeSegment(session.appId || session.id || 'app-export', 'app-export');
    const zipFilename = `${exportBaseName}-${timestamp}.zip`;
    const zipPath = path.join('/tmp', zipFilename);

    // Create ZIP using archiver
    const output = fsSync.createWriteStream(zipPath);
    const archive = archiver('zip', { zlib: { level: 9 } });

    let fileCount = 0;
    let totalUncompressedSize = 0;

    // Set up size limit enforcement
    archive.on('entry', (data) => {
      fileCount++;
      totalUncompressedSize += data.size;
      if (totalUncompressedSize > MAX_EXPORT_ZIP_SIZE) {
        archive.emit('error', new Error(`Export size exceeds ${Math.ceil(MAX_EXPORT_ZIP_SIZE / (1024 * 1024))}MB limit`));
      }
    });

    await new Promise(async (resolve, reject) => {
      output.on('close', resolve);
      archive.on('error', (err) => {
        // Clean up temp file on error
        fs.unlink(zipPath).catch(() => { });
        reject(err);
      });
      archive.pipe(output);

      // Find the main plugin PHP file by looking for WordPress plugin header
      let mainPluginFile = null;
      let pluginContent = null;
      
      for (const fileInfo of validFiles) {
        if (fileInfo.fullPath.endsWith('.php')) {
          try {
            const content = await fs.readFile(fileInfo.fullPath, 'utf-8');
            // Check for WordPress plugin header
            if (content.includes('Plugin Name:') || content.includes('Plugin URI:')) {
              mainPluginFile = fileInfo;
              pluginContent = content;
              break;
            }
          } catch (err) {
            // Continue if we can't read the file
          }
        }
      }
      
      // Version management for WordPress plugins
      let versionUpdatedContent = null;
      let oldVersion = null;
      let newVersion = null;
      
      if (mainPluginFile && pluginContent) {
        try {
          // Extract current version from plugin file
          const versions = versionManager.extractAllVersions(pluginContent);
          const detectedVersion = versions.detectedVersion;
          
          // Determine which version to use:
          // 1. Session version if available and valid
          // 2. Detected version from file
          // 3. Default to 1.0.0 if nothing found
          let currentVersion = session.pluginVersion || detectedVersion || '1.0.0';
          
          // If we detected a version from file but session doesn't have one, use detected
          if (detectedVersion && !session.pluginVersion) {
            currentVersion = detectedVersion;
            log('Detected version from imported plugin file', { 
              file: mainPluginFile.relativePath, 
              version: detectedVersion 
            });
          }
          
          // Bump the version (default to patch bump)
          // Note: In the future, this could be configurable via query parameter
          newVersion = versionManager.bumpVersion(currentVersion, 'patch');
          
          if (newVersion && newVersion !== currentVersion) {
            // Update the plugin content with new version
            versionUpdatedContent = versionManager.updateVersionInContent(
              pluginContent, 
              currentVersion, 
              newVersion
            );
            
            if (versionUpdatedContent) {
              oldVersion = currentVersion;
              
              // Update session tracking
              session.pluginVersion = newVersion;
              session.lastVersionBumpType = 'patch';
              session.pluginVersionHistory.push({
                version: newVersion,
                previousVersion: currentVersion,
                bumpType: 'patch',
                timestamp: new Date().toISOString(),
                fileUpdated: mainPluginFile.relativePath
              });
              
              log('Bumped plugin version', { 
                from: currentVersion, 
                to: newVersion,
                file: mainPluginFile.relativePath,
                sessionId: session.id 
              });
            }
          } else {
            // Keep current version if bump failed
            session.pluginVersion = currentVersion;
          }
        } catch (versionError) {
          log('Version detection/update failed, continuing with original files', { 
            error: String(versionError),
            file: mainPluginFile?.relativePath 
          });
          // Don't fail the export if version management fails
        }
      }
       
      // Determine the plugin folder name
      // WordPress identifies plugins by folder name + main file name
      let pluginFolderName = session.pluginSlug;
      
      if (mainPluginFile && !pluginFolderName) {
        // If we don't have a pluginSlug, derive it from the main plugin file
        const pathParts = mainPluginFile.fullPath.split(path.sep);
        const mainPluginFolderIndex = pathParts.length - 2;
        if (mainPluginFolderIndex >= 0) {
          const existingFolder = pathParts[mainPluginFolderIndex];
          pluginFolderName = existingFolder;
          log('Using folder from main plugin file', { file: mainPluginFile.relativePath, folder: existingFolder });
        }
      } else if (mainPluginFile && pluginFolderName) {
        const pathParts = mainPluginFile.fullPath.split(path.sep);
        const mainPluginFolderIndex = pathParts.length - 2;
        if (mainPluginFolderIndex >= 0) {
          const existingFolder = pathParts[mainPluginFolderIndex];
          log('Plugin folder will be normalized', { file: mainPluginFile.relativePath, existingFolder, pluginSlug: pluginFolderName });
        }
      }
       
      // Determine the optimal root to avoid nested wrappers
      const optimalRoot = findOptimalExportRoot(validFiles, session.workspaceDir);

      // Add files to archive with corrected paths
      // For WordPress plugins, we MUST ensure the folder name is consistent across exports
      // WordPress identifies plugins by: folder-name/main-file.php
      const wrapInPluginFolder = pluginFolderName ? pluginFolderName : null;
      
      // Check if we should rename the existing folder to match pluginSlug
      // This is critical for WordPress to recognize it as the same plugin across exports
      let renameFirstLevelDir = null;
      
      if (wrapInPluginFolder) {
        // Analyze first-level directory structure (only actual directories, not files)
        const firstLevelDirs = new Set();
        for (const fileInfo of validFiles) {
          const relativePath = path.relative(optimalRoot, fileInfo.fullPath);
          const parts = relativePath.split(path.sep);
          if (parts.length > 1 && parts[0]) {
            firstLevelDirs.add(parts[0]);
          }
        }
        
        // If there's exactly one first-level directory and it doesn't match our target,
        // we'll rename it in the archive
        if (firstLevelDirs.size === 1) {
          const existingDir = Array.from(firstLevelDirs)[0];
          if (existingDir !== wrapInPluginFolder) {
            renameFirstLevelDir = existingDir;
            log('Renaming plugin folder for consistency', { from: existingDir, to: wrapInPluginFolder });
          }
        } else if (firstLevelDirs.size === 0) {
          // No subdirectories, files are at root level of optimalRoot
          // We'll wrap them in the pluginSlug folder
          log('Files at root level, wrapping in plugin folder', { folder: wrapInPluginFolder });
        }
      }
      
      for (const fileInfo of validFiles) {
        const relativePath = path.relative(optimalRoot, fileInfo.fullPath);
        
        // Check if this is the main plugin file with updated content
        const isMainPluginFile = mainPluginFile && fileInfo.fullPath === mainPluginFile.fullPath;
        
        if (wrapInPluginFolder) {
          let archivePath;
          
          if (renameFirstLevelDir) {
            // Rename the first-level directory to match pluginSlug
            const pathParts = relativePath.split(path.sep);
            if (pathParts[0] === renameFirstLevelDir) {
              pathParts[0] = wrapInPluginFolder;
              archivePath = pathParts.join(path.sep);
            } else {
              archivePath = path.join(wrapInPluginFolder, relativePath);
            }
          } else {
            // Wrap in pluginSlug folder
            archivePath = path.join(wrapInPluginFolder, relativePath);
          }
          
          // If this is the main plugin file and we have updated content, use it
          if (isMainPluginFile && versionUpdatedContent) {
            archive.append(Buffer.from(versionUpdatedContent, 'utf-8'), { name: archivePath });
          } else {
            archive.file(fileInfo.fullPath, { name: archivePath });
          }
        } else {
          // If this is the main plugin file and we have updated content, use it
          if (isMainPluginFile && versionUpdatedContent) {
            archive.append(Buffer.from(versionUpdatedContent, 'utf-8'), { name: relativePath });
          } else {
            archive.file(fileInfo.fullPath, { name: relativePath });
          }
        }
      }

      archive.finalize();
    });

    // Check if any files were added to the archive
    if (fileCount === 0) {
      await fs.unlink(zipPath).catch(() => { });
      log('No files added to archive', { workspace: session.workspaceDir });
      return sendJson(res, 400, {
        error: 'No files found to export. Your app workspace appears to be empty.'
      });
    }

    // Read and send the ZIP file
    const zipContent = await fs.readFile(zipPath);

    // Check final size limit
    if (zipContent.length > MAX_EXPORT_ZIP_SIZE) {
      await fs.unlink(zipPath).catch(() => { });
      log('Export exceeds size limit', { size: zipContent.length, limit: MAX_EXPORT_ZIP_SIZE });
      return sendJson(res, 400, {
        error: `Export size (${Math.ceil(zipContent.length / (1024 * 1024))}MB) exceeds the maximum allowed size (${Math.ceil(MAX_EXPORT_ZIP_SIZE / (1024 * 1024))}MB). Try excluding large files like images or binaries.`
      });
    }

    // Clean up temp file
    await fs.unlink(zipPath).catch(() => { });

    res.writeHead(200, {
      'Content-Type': 'application/zip',
      'Content-Disposition': `attachment; filename="${zipFilename}"`,
      'Content-Length': zipContent.length
    });
    res.end(zipContent);
    log('Export completed successfully', { 
      filename: zipFilename, 
      size: zipContent.length, 
      fileCount,
      version: newVersion || session.pluginVersion || 'unknown',
      previousVersion: oldVersion || 'initial'
    });
  } catch (error) {
    if (error.message && error.message.includes('size exceeds')) {
      return sendJson(res, 400, { error: error.message });
    }
    log('Export failed', { error: String(error) });
    sendJson(res, 500, { error: `Export failed: ${error.message}` });
  }
}

// Helper function to collect all valid files recursively
async function collectValidFiles(rootDir, currentDir, validFiles, excludePatterns) {
  let entries;
  try {
    entries = await fs.readdir(currentDir, { withFileTypes: true });
  } catch {
    return;
  }

  for (const entry of entries) {
    const fullPath = path.join(currentDir, entry.name);
    const relativePath = path.relative(rootDir, fullPath);
    const parts = relativePath.split(path.sep);

    // Check if any part matches exclusion patterns
    const isExcluded = parts.some(part => {
      if (part.startsWith('.')) return true;
      if (excludePatterns.includes(part)) return true;
      // Check wildcard patterns like *.log, *.zip
      return excludePatterns.some(pattern => {
        if (pattern.startsWith('*.')) {
          const ext = pattern.slice(1);
          return entry.name.endsWith(ext);
        }
        return false;
      });
    });

    if (isExcluded) continue;

    if (entry.isDirectory()) {
      await collectValidFiles(rootDir, fullPath, validFiles, excludePatterns);
    } else if (entry.isFile()) {
      // Exclude files named opencode.json specifically
      if (entry.name !== 'opencode.json') {
        validFiles.push({ fullPath, relativePath });
      }
    }
  }
}

// Helper function to find the optimal root for export
// Avoids single-level wrapper folders that contain everything
function findOptimalExportRoot(files, workspaceRoot) {
  if (files.length === 0) return workspaceRoot;

  // Get all unique first-level directories
  const firstLevelDirs = new Set();
  const firstLevelFiles = new Set();

  for (const fileInfo of files) {
    const relativePath = fileInfo.relativePath;
    const parts = relativePath.split(path.sep);
    if (parts.length > 1) {
      firstLevelDirs.add(parts[0]);
    } else {
      firstLevelFiles.add(parts[0]);
    }
  }

  // If there are files directly at the root level, that's the optimal root
  if (firstLevelFiles.size > 0) {
    return workspaceRoot;
  }

  // If there's only one first-level directory, check if it's a wrapper
  if (firstLevelDirs.size === 1) {
    const singleDir = Array.from(firstLevelDirs)[0];
    const allFilesInSingleDir = files.every(f => {
      const parts = f.relativePath.split(path.sep);
      return parts[0] === singleDir;
    });

    // Check if this single directory looks like a wrapper (has no direct files, only subdirs)
    const filesInSingleDir = files.filter(f => {
      const parts = f.relativePath.split(path.sep);
      return parts[0] === singleDir;
    });

    const hasDirectFilesInSingleDir = filesInSingleDir.some(f => {
      const parts = f.relativePath.split(path.sep);
      return parts.length === 2; // Only one level deep in the single dir
    });

    // If all files are in a single subdirectory and there are direct files there,
    // we should use that subdirectory as the root
    if (allFilesInSingleDir && hasDirectFilesInSingleDir) {
      return path.join(workspaceRoot, singleDir);
    }
  }

  // Default to workspace root
  return workspaceRoot;
}

// Helper function to get all files recursively (alternative approach)
async function getAllFilesRecursively(dir) {
  const files = [];
  try {
    const entries = await fs.readdir(dir, { withFileTypes: true });
    for (const entry of entries) {
      const fullPath = path.join(dir, entry.name);
      if (entry.isDirectory()) {
        files.push(...await getAllFilesRecursively(fullPath));
      } else if (entry.isFile()) {
        files.push(fullPath);
      }
    }
  } catch {
    // Directory might not exist or be accessible
  }
  return files;
}

async function handleListTemplates(req, res) {
  try {
    const templatesPath = path.join(__dirname, 'templates', 'templates.json');
    const content = await fs.readFile(templatesPath, 'utf-8');
    const templates = JSON.parse(content);
    sendJson(res, 200, { templates });
  } catch (error) {
    if (error.code === 'ENOENT') {
      return sendJson(res, 200, { templates: [] });
    }
    log('Failed to list templates', { error: String(error) });
    sendJson(res, 500, { error: 'Failed to list templates' });
  }
}

async function runGit(commands, opts = {}) {
  // Ensure git operations are serialized to avoid concurrent fetch/pulls producing
  // "cannot lock ref" errors when remote refs change during a simultaneous fetch.
  // We also retry on transient ref lock errors ("cannot lock ref", "unable to update local ref",
  // "index.lock") a few times with backoff to reduce races.
  if (!global.__opencode_git_queue) global.__opencode_git_queue = Promise.resolve();
  const enqueue = (workFn) => {
    const next = global.__opencode_git_queue.then(() => workFn());
    // Prevent the queue from getting rejected - keep it alive
    global.__opencode_git_queue = next.catch(() => { });
    return next;
  };

  const worker = async () => {
    let last = '';
    let combinedStdout = '';
    let combinedStderr = '';
    const credentials = (opts && opts.credentials) ? opts.credentials : null;
    const os = require('os');
    const tmpdir = require('path');
    const fsSync = require('fs');
    const tmp = require('fs/promises');
    let tempHomeDir;
    let envOverride = null;
    if (credentials && credentials.username && credentials.pat) {
      const prefix = 'web-chat-netrc-';
      tempHomeDir = await tmp.mkdtemp(tmpdir.join(os.tmpdir(), prefix));
      const netrcPath = tmpdir.join(tempHomeDir, '.netrc');
      const netrcContent = `machine github.com\nlogin ${credentials.username}\npassword ${credentials.pat}\n`;
      await tmp.writeFile(netrcPath, netrcContent, { mode: 0o600, encoding: 'utf8' });
      envOverride = { ...process.env, HOME: tempHomeDir, GIT_TERMINAL_PROMPT: '0' };
    }
    try {
      for (const step of commands) {
        const shouldRetryOnRefLock = step.cmd === 'git' && Array.isArray(step.args) && (step.args.includes('pull') || step.args.includes('fetch'));
        let attempt = 0;
        while (true) {
          attempt += 1;
          try {
            const spawnOpts = { cwd: REPO_ROOT };
            if (envOverride) spawnOpts.env = envOverride;
            const { stdout, stderr } = await runCommand(step.cmd, step.args, spawnOpts);
            combinedStdout += stdout || '';
            combinedStderr += stderr || '';
            last = stdout || last;
            break; // step succeeded
          } catch (error) {
            // If this looks like a remote ref lock/update race, retry a few times
            const stderrData = String(error.stderr || '').toLowerCase() + ' ' + String(error.stdout || '').toLowerCase();
            if (shouldRetryOnRefLock && attempt < 4 && (stderrData.includes('cannot lock ref') || stderrData.includes('unable to update local ref') || stderrData.includes('index.lock'))) {
              const backoff = Math.min(2000, 100 * Math.pow(2, attempt));
              log('Git operation had a ref-lock problem, retrying', { cmd: step.cmd, args: step.args, attempt, backoff, stderr: error.stderr || error.stdout || '' });
              await new Promise((r) => setTimeout(r, backoff));
              continue; // try again
            }
            const err = new Error(String(error.message || error));
            err.stdout = (combinedStdout || '') + (error.stdout || '');
            err.stderr = (combinedStderr || '') + (error.stderr || '');
            throw err;
          }
        }
      }
    } finally {
      if (tempHomeDir) {
        try { await tmp.rm(tempHomeDir, { recursive: true, force: true }); } catch (ignore) { }
      }
    }
    const result = { stdout: (combinedStdout || '').trim(), stderr: (combinedStderr || '').trim(), last: (last || '').trim() };
    return result;
  }

  // Run a serialized git worker so commands don't run concurrently.
  return enqueue(worker);
}

// Attempt to create an opencode session on the CLI side. Returns the id if created or null.
async function createOpencodeSession(wantedId, model, cwd) {
  const cliCommand = resolveCliCommand('opencode');
  const candidates = [
    ['session', 'create', '--id', wantedId, '--model', model, '--json'],
    ['session', 'create', '--id', wantedId, '--json'],
    ['sessions', 'create', '--id', wantedId, '--model', model, '--json'],
    ['sessions', 'create', '--id', wantedId, '--json'],
    ['session', 'create', '--model', model, '--json'],
    ['sessions', 'create', '--model', model, '--json'],
    ['session', 'create', '--json'],
  ];
  for (const args of candidates) {
    try {
      log('creating opencode session (candidate)', { args });
      const { stdout } = await runCommand(cliCommand, args, { timeout: 15000, cwd: cwd || REPO_ROOT });
      try {
        const parsed = JSON.parse(stdout);
        const id = parsed.id || (parsed.session && parsed.session.id) || parsed.sessionId || parsed.session_id || null;
        if (id) return id;
        if (typeof parsed === 'string' && parsed.startsWith('ses-')) return parsed;
      } catch (_) {
        const lines = stdout.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
        for (const line of lines) {
          const m = line.match(/(ses-[a-f0-9\-]+)/i);
          if (m) return m[1];
        }
      }
    } catch (err) {
      log('session creation candidate failed', { args, stderr: err.stderr || err.stdout || String(err) });
    }
  }
  return null;
}

async function listOpencodeSessions(cwd) {
  const cliCommand = resolveCliCommand('opencode');
  const candidates = [
    ['session', '--list', '--json'],
    ['sessions', '--list', '--json'],
    ['session', 'list', '--json'],
    ['sessions', 'list', '--json'],
  ];

  for (const args of candidates) {
    try {
      const { stdout } = await runCommand(cliCommand, args, { timeout: 7000, cwd });
      if (!stdout || !stdout.trim()) continue;

      try {
        const parsed = JSON.parse(stdout);
        const items = Array.isArray(parsed)
          ? parsed
          : (Array.isArray(parsed.sessions) ? parsed.sessions
            : (Array.isArray(parsed.data) ? parsed.data
              : (Array.isArray(parsed.items) ? parsed.items : [])));

        return items.map((it) => ({
          id: it?.id || it?.sessionId || it?.session_id || null,
          createdAt: it?.createdAt || it?.created_at || it?.startedAt || it?.startTime || null,
          updatedAt: it?.updatedAt || it?.updated_at || it?.lastUpdatedAt || it?.last_updated_at || null,
        })).filter((it) => it.id);
      } catch (_) {
        const matches = stdout.match(/ses[-_][a-zA-Z0-9-]+/g) || [];
        return matches.map((id) => ({ id }));
      }
    } catch (_) {
      // try next candidate
    }
  }

  return [];
}

// Note: Session token usage queries removed - OpenCode CLI doesn't support session info/usage/show commands.
// Token tracking now relies on:
// 1. Streaming capture during CLI execution (primary method)
// 2. Estimation as fallback when streaming fails

async function handleGit(req, res, action) {
  // Validate git action
  if (!validateGitAction(action)) {
    return sendJson(res, 400, { error: 'Invalid git action' });
  }

  try {
    const body = req.method === 'POST' ? await parseJsonBody(req) : {};
    let output = '';
    let _stdout = '';
    let _stderr = '';
    if (action === 'pull') {
      // Use plain git pull (no rebase)
      const result = await runGit([{ cmd: 'git', args: ['pull'] }], { credentials: { username: process.env.GITHUB_USERNAME, pat: process.env.GITHUB_PAT } });
      output = result.stdout || result.last || '';
      _stdout = result.stdout;
      _stderr = result.stderr;
    } else if (action === 'push') {
      const message = sanitizeGitMessage(body.message) || 'Update from chat UI';
      // Add all changes (use `git add .` rather than `-A`)
      await runGit([{ cmd: 'git', args: ['add', '.'] }]);
      // Check if there are staged changes
      const statusResult = await runCommand('git', ['status', '--porcelain'], { cwd: REPO_ROOT, timeout: 60000 }).catch((e) => ({ stdout: e.stdout || '', stderr: e.stderr || '' }));
      if ((statusResult.stdout || '').trim().length === 0) {
        // Nothing to commit; just push
        const pushResult = await runGit([{ cmd: 'git', args: ['push', 'origin', 'main'] }], { credentials: { username: process.env.GITHUB_USERNAME, pat: process.env.GITHUB_PAT } });
        output = pushResult.stdout || pushResult.last || '';
        _stdout = pushResult.stdout;
        _stderr = pushResult.stderr;
      } else {
        const commitResult = await runGit([{ cmd: 'git', args: ['commit', '-m', message] }], { credentials: { username: process.env.GITHUB_USERNAME, pat: process.env.GITHUB_PAT } });
        const pushResult = await runGit([{ cmd: 'git', args: ['push', 'origin', 'main'] }], { credentials: { username: process.env.GITHUB_USERNAME, pat: process.env.GITHUB_PAT } });
        output = `${commitResult.stdout || ''}\n${pushResult.stdout || ''}`.trim();
        _stdout = `${commitResult.stdout || ''}\n${pushResult.stdout || ''}`.trim();
        _stderr = `${commitResult.stderr || ''}\n${pushResult.stderr || ''}`.trim();
      }
    } else if (action === 'sync') {
      const message = sanitizeGitMessage(body.message) || 'Update from chat UI';
      // Sync: pull; add; commit if needed; push
      const resultPull = await runGit([{ cmd: 'git', args: ['pull'] }], { credentials: { username: process.env.GITHUB_USERNAME, pat: process.env.GITHUB_PAT } });
      await runGit([{ cmd: 'git', args: ['add', '.'] }]);
      const statusResult2 = await runCommand('git', ['status', '--porcelain'], { cwd: REPO_ROOT, timeout: 60000 }).catch((e) => ({ stdout: e.stdout || '', stderr: e.stderr || '' }));
      if ((statusResult2.stdout || '').trim().length === 0) {
        const pushResult = await runGit([{ cmd: 'git', args: ['push', 'origin', 'main'] }], { credentials: { username: process.env.GITHUB_USERNAME, pat: process.env.GITHUB_PAT } });
        output = `${resultPull.stdout || ''}\n${pushResult.stdout || ''}`.trim();
        _stdout = `${resultPull.stdout || ''}\n${pushResult.stdout || ''}`.trim();
        _stderr = `${resultPull.stderr || ''}\n${pushResult.stderr || ''}`.trim();
      } else {
        const commitResult = await runGit([{ cmd: 'git', args: ['commit', '-m', message] }], { credentials: { username: process.env.GITHUB_USERNAME, pat: process.env.GITHUB_PAT } });
        const pushResult = await runGit([{ cmd: 'git', args: ['push', 'origin', 'main'] }], { credentials: { username: process.env.GITHUB_USERNAME, pat: process.env.GITHUB_PAT } });
        output = `${resultPull.stdout || ''}\n${commitResult.stdout || ''}\n${pushResult.stdout || ''}`.trim();
        _stdout = `${resultPull.stdout || ''}\n${commitResult.stdout || ''}\n${pushResult.stdout || ''}`.trim();
        _stderr = `${resultPull.stderr || ''}\n${commitResult.stderr || ''}\n${pushResult.stderr || ''}`.trim();
      }
    } else if (action === 'status') {
      const { stdout, stderr } = await runCommand('git', ['status', '--short'], { cwd: REPO_ROOT, timeout: 20000 });
      output = stdout;
      _stdout = stdout;
      _stderr = stderr;
    } else if (action === 'log') {
      const { stdout, stderr } = await runCommand('git', ['log', '--oneline', '-n', '20'], { cwd: REPO_ROOT, timeout: 20000 });
      output = stdout;
      _stdout = stdout;
      _stderr = stderr;
    } else if (action === 'fetch') {
      const result = await runGit([{ cmd: 'git', args: ['fetch', '--all'] }], { credentials: { username: process.env.GITHUB_USERNAME, pat: process.env.GITHUB_PAT } });
      output = result.stdout || result.last || '';
      _stdout = result.stdout;
      _stderr = result.stderr;
    } else {
      return sendJson(res, 400, { error: 'Unknown git action' });
    }
    sendJson(res, 200, { output, stdout: _stdout, stderr: _stderr });
  } catch (error) {
    const payload = { error: error.message || 'Git command failed' };
    if (error.stdout) payload.stdout = error.stdout;
    if (error.stderr) payload.stderr = error.stderr;
    sendJson(res, 500, payload);
  }
}

async function route(req, res) {
  try {
    // Validate and sanitize the URL to handle malformed requests
    const urlString = sanitizeUrl(req.url);
    const url = new URL(urlString, `http://${req.headers.host}`);
    const pathname = url.pathname;

    // Handle CORS preflight requests
    const origin = req.headers.origin;
    const allowedOrigins = [
      'capacitor://localhost',
      'http://localhost',
      'https://localhost',
      'https://plugincompass.com',
      'http://plugincompass.com'
    ];
    
    if (origin && allowedOrigins.some(allowed => origin.includes(allowed) || allowed.includes(origin))) {
      res.setHeader('Access-Control-Allow-Origin', origin);
    } else if (origin) {
      // Allow any origin for mobile apps
      res.setHeader('Access-Control-Allow-Origin', origin);
    }
    
    res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS, PATCH');
    res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-User-Id, X-Request-ID');
    res.setHeader('Access-Control-Allow-Credentials', 'true');
    res.setHeader('Access-Control-Max-Age', '86400');
    
    // Handle OPTIONS preflight
    if (req.method === 'OPTIONS') {
      res.writeHead(204);
      res.end();
      return;
    }

    // Track visitor
    trackVisit(req, res);

    // Add rate limit headers to all responses
    const userId = resolveUserId(req, url);
    if (userId) {
      const rateLimit = checkApiRateLimit(userId);
      res.setHeader('X-RateLimit-Limit', rateLimit.limit);
      res.setHeader('X-RateLimit-Remaining', rateLimit.remaining);
      res.setHeader('X-RateLimit-Reset', rateLimit.resetIn);

      if (rateLimit.limited) {
        return sendRateLimitExceeded(res, rateLimit.resetIn, rateLimit.limit);
      }
    }

    // Add timing header for bot detection
    res.setHeader('X-Request-Time', Date.now());

    const affiliateParam = sanitizeAffiliateCode(url.searchParams.get('aff') || url.searchParams.get('affiliate'));
    if (affiliateParam && findAffiliateByCode(affiliateParam)) {
      setAffiliateReferralCookie(res, affiliateParam);
    }

    await routeInternal(req, res, url, pathname);
  } catch (error) {
    // Log all route errors to ensure they're visible in container logs
    log('Route error', {
      url: req.url,
      method: req.method,
      error: String(error),
      stack: error.stack
    });

    // If response hasn't been sent yet, send a 500 error
    if (!res.headersSent) {
      try {
        sendJson(res, 500, { error: 'Internal server error' });
      } catch (sendError) {
        // If even sending the error response fails, just end the response
        log('Failed to send error response', { sendError: String(sendError) });
        res.end();
      }
    }
  }
}

async function routeInternal(req, res, url, pathname) {
  if (req.method === 'GET' && pathname === '/api/health') return sendJson(res, 200, { ok: true });
  if (req.method === 'GET' && pathname === '/api/opencode/status') return handleOpencodeStatus(req, res);
  if (req.method === 'GET' && pathname === '/api/memory/stats') return handleMemoryStats(req, res);
  if (req.method === 'POST' && pathname === '/api/memory/cleanup') return handleForceMemoryCleanup(req, res);
  if (req.method === 'GET' && pathname === '/auth/google') return handleGoogleAuthStart(req, res, url);
  if (req.method === 'GET' && pathname === '/auth/google/callback') return handleGoogleAuthCallback(req, res, url);
  if (req.method === 'GET' && pathname === '/auth/github') return handleGithubAuthStart(req, res, url);
  if (req.method === 'GET' && pathname === '/auth/github/callback') return handleGithubAuthCallback(req, res, url);
  if (req.method === 'POST' && pathname === '/api/account/claim') {
    const userId = requireUserId(req, res, url);
    if (!userId) return;
    return handleAccountClaim(req, res, userId);
  }
  if (req.method === 'POST' && pathname === '/api/affiliates/signup') return handleAffiliateSignup(req, res);
  if (req.method === 'POST' && pathname === '/api/affiliates/login') return handleAffiliateLogin(req, res);
  if (req.method === 'POST' && pathname === '/api/affiliates/logout') return handleAffiliateLogout(req, res);
  if (req.method === 'GET' && pathname === '/api/affiliates/me') return handleAffiliateMe(req, res, url);
  if (req.method === 'GET' && pathname === '/api/affiliates/verify-email') return handleAffiliateVerifyEmailApi(req, res, url);
  if (req.method === 'POST' && pathname === '/api/affiliates/verify-email') return handleAffiliateVerifyEmailApi(req, res, url);
  if (req.method === 'GET' && pathname === '/api/affiliates/transactions') return handleAffiliateTransactions(req, res);
  if (req.method === 'POST' && pathname === '/api/affiliates/links') return handleAffiliateCreateLink(req, res);
  if (req.method === 'POST' && pathname === '/api/affiliates/withdrawals') return handleAffiliateCreateWithdrawal(req, res);
  if (req.method === 'GET' && pathname === '/api/feature-requests') return handleFeatureRequestsList(req, res);
  if (req.method === 'POST' && pathname === '/api/feature-requests') return handleFeatureRequestCreate(req, res);
  const featureUpvoteMatch = pathname.match(/^\/api\/feature-requests\/([a-f0-9\-]+)\/upvote$/i);
  if (req.method === 'POST' && featureUpvoteMatch) return handleFeatureRequestUpvote(req, res, featureUpvoteMatch[1]);
  // Admin feature request endpoints
  if (req.method === 'GET' && pathname === '/api/admin/feature-requests') return handleAdminFeatureRequestsList(req, res);
  const featureRequestReplyMatch = pathname.match(/^\/api\/admin\/feature-requests\/([a-f0-9\-]+)\/reply$/i);
  if (req.method === 'POST' && featureRequestReplyMatch) return handleFeatureRequestReply(req, res, featureRequestReplyMatch[1]);
  const featureRequestStatusMatch = pathname.match(/^\/api\/admin\/feature-requests\/([a-f0-9\-]+)\/status$/i);
  if (req.method === 'POST' && featureRequestStatusMatch) return handleFeatureRequestUpdateStatus(req, res, featureRequestStatusMatch[1]);
  const featureRequestDeleteMatch = pathname.match(/^\/api\/admin\/feature-requests\/([a-f0-9\-]+)$/i);
  if (req.method === 'DELETE' && featureRequestDeleteMatch) return handleFeatureRequestDelete(req, res, featureRequestDeleteMatch[1]);
  if (req.method === 'POST' && pathname === '/api/contact') return handleContactMessageCreate(req, res);
  const contactMessagesMatch = pathname.match(/^\/api\/contact\/messages$/i);
  if (req.method === 'GET' && contactMessagesMatch) return handleContactMessagesList(req, res);
  const contactMessageReadMatch = pathname.match(/^\/api\/contact\/messages\/([a-f0-9\-]+)\/read$/i);
  if (req.method === 'POST' && contactMessageReadMatch) return handleContactMessageMarkRead(req, res, contactMessageReadMatch[1]);
  const contactMessageDeleteMatch = pathname.match(/^\/api\/contact\/messages\/([a-f0-9\-]+)$/i);
  if (req.method === 'DELETE' && contactMessageDeleteMatch) return handleContactMessageDelete(req, res, contactMessageDeleteMatch[1]);

  // Blog API Routes
  if (req.method === 'GET' && pathname === '/api/blogs') return handleBlogsList(req, res, url);
  if (req.method === 'GET' && pathname === '/api/blogs/categories') return handleBlogCategoriesList(req, res);
  const blogDetailMatch = pathname.match(/^\/api\/blogs\/([^\/]+)$/i);
  if (req.method === 'GET' && blogDetailMatch) return handleBlogGet(req, res, blogDetailMatch[1]);
  if (req.method === 'GET' && pathname === '/api/admin/blogs') {
    const session = requireAdminAuth(req, res);
    if (!session) return;
    return handleAdminBlogsList(req, res, url);
  }
  if (req.method === 'POST' && pathname === '/api/admin/blogs') {
    const session = requireAdminAuth(req, res);
    if (!session) return;
    return handleAdminBlogCreate(req, res);
  }
  const adminBlogUpdateMatch = pathname.match(/^\/api\/admin\/blogs\/([^\/]+)$/i);
  if (req.method === 'PUT' && adminBlogUpdateMatch) {
    const session = requireAdminAuth(req, res);
    if (!session) return;
    return handleAdminBlogUpdate(req, res, adminBlogUpdateMatch[1]);
  }
  const adminBlogDeleteMatch = pathname.match(/^\/api\/admin\/blogs\/([^\/]+)$/i);
  if (req.method === 'DELETE' && adminBlogDeleteMatch) {
    const session = requireAdminAuth(req, res);
    if (!session) return;
    return handleAdminBlogDelete(req, res, adminBlogDeleteMatch[1]);
  }
  if (req.method === 'POST' && pathname === '/api/admin/blogs/categories') {
    const session = requireAdminAuth(req, res);
    if (!session) return;
    return handleAdminBlogCategoryCreate(req, res);
  }
  const adminBlogCategoryUpdateMatch = pathname.match(/^\/api\/admin\/blogs\/categories\/([^\/]+)$/i);
  if (req.method === 'PUT' && adminBlogCategoryUpdateMatch) {
    const session = requireAdminAuth(req, res);
    if (!session) return;
    return handleAdminBlogCategoryUpdate(req, res, adminBlogCategoryUpdateMatch[1]);
  }
  const adminBlogCategoryDeleteMatch = pathname.match(/^\/api\/admin\/blogs\/categories\/([^\/]+)$/i);
  if (req.method === 'DELETE' && adminBlogCategoryDeleteMatch) {
    const session = requireAdminAuth(req, res);
    if (!session) return;
    return handleAdminBlogCategoryDelete(req, res, adminBlogCategoryDeleteMatch[1]);
  }
  if (req.method === 'POST' && pathname === '/api/admin/blogs/upload-image') {
    const session = requireAdminAuth(req, res);
    if (!session) return;
    return handleAdminBlogImageUpload(req, res);
  }

  if (req.method === 'POST' && pathname === '/api/login') return handleUserLogin(req, res);
  if (req.method === 'POST' && pathname === '/api/register') return handleUserRegister(req, res);
  if (req.method === 'POST' && pathname === '/api/logout') return handleUserLogout(req, res);
  if (req.method === 'GET' && pathname === '/api/me') return handleUserMe(req, res);
  if (req.method === 'GET' && pathname === '/api/admin/me') {
    const session = requireAdminAuth(req, res);
    if (!session) return;
    return handleAdminMe(req, res);
  }
  if (req.method === 'GET' && pathname === '/api/csrf') return handleCsrfToken(req, res);
  if (req.method === 'GET' && pathname === '/api/account') return handleAccountSettingsGet(req, res, url);
  if (req.method === 'GET' && pathname === '/api/account/usage') return handleAccountUsage(req, res);
  if (req.method === 'GET' && pathname === '/api/provider-limits') return handleProviderLimitsGet(req, res);
  if (req.method === 'GET' && pathname === '/api/account/plans') return handleAccountPlans(req, res);
  if (req.method === 'GET' && pathname === '/api/invoices') return handleInvoicesList(req, res);
  const invoiceDownloadMatch = pathname.match(/^\/api\/invoices\/([a-f0-9\-]+)\/download$/i);
  if (req.method === 'GET' && invoiceDownloadMatch) return handleInvoiceDownload(req, res, url, invoiceDownloadMatch[1]);
  if (req.method === 'GET' && pathname === '/api/topups/options') {
    const session = requireUserAuth(req, res);
    if (!session) return;
    return await handleTopupOptions(req, res, session.userId);
  }
  // Admin-only test endpoints for Dodo top-ups
  if (req.method === 'GET' && pathname === '/api/admin/topups/options') {
    const session = requireAdminAuth(req, res);
    if (!session) return;
    return handleAdminTopupOptions(req, res);
  }
  if (req.method === 'GET' && pathname === '/api/admin/topups/confirm') {
    const session = requireAdminAuth(req, res);
    if (!session) return;
    return handleAdminTopupConfirm(req, res, url);
  }
  if (req.method === 'POST' && pathname === '/api/admin/topups/checkout') {
    const session = requireAdminAuth(req, res);
    if (!session) return;
    return handleAdminTopupCheckout(req, res);
  }

  if (req.method === 'GET' && pathname === '/api/topups/confirm') return await handleTopupConfirm(req, res, url);
  if (req.method === 'POST' && pathname === '/api/topups/checkout') return await handleTopupCheckout(req, res);
  if (req.method === 'GET' && pathname === '/api/payg/status') return handlePaygStatus(req, res);
  if (req.method === 'POST' && pathname === '/api/payg/checkout') return handlePaygCheckout(req, res);
  if (req.method === 'GET' && pathname === '/api/payg/confirm') return handlePaygConfirm(req, res, url);
  if (req.method === 'POST' && pathname === '/api/account/boost') return handleAccountBoostPurchase(req, res);
  if (req.method === 'POST' && pathname === '/api/test/simulate-tokens') return handleSimulateTokenUsage(req, res);
  if (req.method === 'GET' && pathname === '/api/onboarding') return handleOnboardingGet(req, res);
  if (req.method === 'POST' && pathname === '/api/onboarding') return handleOnboardingPost(req, res);
  if (req.method === 'POST' && pathname === '/api/account') return handleAccountSettingsUpdate(req, res);
  if (req.method === 'GET' && pathname === '/api/account/payment-methods') return handlePaymentMethodsList(req, res, url);
  const paymentMethodCreateMatch = pathname.match(/^\/api\/account\/payment-methods\/create$/i);
  if (req.method === 'POST' && paymentMethodCreateMatch) return handlePaymentMethodCreate(req, res);
  const paymentMethodSetDefaultMatch = pathname.match(/^\/api\/account\/payment-methods\/([^\/]+)\/default$/i);
  if (req.method === 'POST' && paymentMethodSetDefaultMatch) return handlePaymentMethodSetDefault(req, res, paymentMethodSetDefaultMatch[1]);
  const paymentMethodDeleteMatch = pathname.match(/^\/api\/account\/payment-methods\/([^\/]+)$/i);
  if (req.method === 'DELETE' && paymentMethodDeleteMatch) return handlePaymentMethodDelete(req, res, paymentMethodDeleteMatch[1]);
  if (req.method === 'POST' && pathname === '/api/account/balance/add') return handleAccountBalanceAdd(req, res);
  if (req.method === 'POST' && pathname === '/api/select-plan') return handleSelectPlan(req, res);
  
  // Subscription API endpoints
  if (req.method === 'POST' && pathname === '/api/subscription/checkout') return handleSubscriptionCheckout(req, res);
  if (req.method === 'GET' && pathname === '/api/subscription/confirm') return handleSubscriptionConfirm(req, res, url);
  if (req.method === 'GET' && pathname === '/api/subscription/status') return handleSubscriptionStatus(req, res);
  if (req.method === 'POST' && pathname === '/api/subscription/cancel') return handleSubscriptionCancel(req, res);
  
  // Dodo webhooks
  if (req.method === 'POST' && pathname === '/webhooks/dodo') return handleDodoWebhook(req, res);
  
  if (req.method === 'GET' && pathname === '/api/verify-email') return handleVerifyEmailApi(req, res, url);
  if (req.method === 'POST' && pathname === '/api/password/forgot') return handlePasswordResetRequest(req, res);

  // Dev helper: preview branded email templates without sending
  if (req.method === 'GET' && pathname === '/debug/email/preview') {
    const type = url.searchParams.get('type') || 'verification';
    const email = url.searchParams.get('email') || 'user@example.com';
    const token = url.searchParams.get('token') || 'sample-token';
    if (type === 'verification') {
      const link = `${resolveBaseUrl(req)}/verify-email?token=${encodeURIComponent(token)}`;
      const bodyHtml = `<p style="margin-top:0;">Welcome!</p><p>Please verify your email address by clicking the button below — or copy and paste the link into your browser:</p><p style="word-break:break-all;"><a href="${escapeHtml(link)}">${escapeHtml(link)}</a></p>`;
      const html = renderBrandedEmail({ title: 'Verify your email address', preheader: 'Confirm your email to get started', bodyHtml, buttonText: 'Verify email', buttonLink: link });
      res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
      return res.end(html);
    }
    if (type === 'reset') {
      const link = `${resolveBaseUrl(req)}/reset-password?token=${encodeURIComponent(token)}`;
      const bodyHtml = `<p style="margin-top:0;">You requested a password reset.</p><p>Reset your password by clicking the button below — or copy and paste the link into your browser:</p><p style="word-break:break-all;"><a href="${escapeHtml(link)}">${escapeHtml(link)}</a></p>`;
      const html = renderBrandedEmail({ title: 'Reset your password', preheader: 'Reset access to your Plugin Compass account', bodyHtml, buttonText: 'Reset password', buttonLink: link });
      res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
      return res.end(html);
    }
    return sendJson(res, 400, { error: 'type must be verification or reset' });
  }
  if (req.method === 'POST' && pathname === '/api/password/reset') return handlePasswordReset(req, res);
  if (req.method === 'POST' && pathname === '/api/apps/upload') {
    const userId = requireUserId(req, res, url);
    if (!userId) return;
    return handleUploadApp(req, res, userId);
  }
  if (req.method === 'POST' && pathname === '/api/admin/login') return handleAdminLogin(req, res);
  if (req.method === 'POST' && pathname === '/api/admin/logout') return handleAdminLogout(req, res);
  if (req.method === 'GET' && pathname === '/api/admin/me') return handleAdminMe(req, res);
  if (req.method === 'GET' && pathname === '/api/admin/available-models') {
    const cliParam = url.searchParams.get('cli');
    return handleAdminAvailableModels(req, res, cliParam);
  }
  if (req.method === 'GET' && pathname === '/api/admin/icons') return handleAdminListIcons(req, res);
  if (req.method === 'GET' && pathname === '/api/admin/models') return handleAdminModelsList(req, res);
  if (req.method === 'POST' && pathname === '/api/admin/models') return handleAdminModelUpsert(req, res);
  if (req.method === 'POST' && pathname === '/api/admin/models/reorder') return handleAdminModelsReorder(req, res);
  if (req.method === 'GET' && pathname === '/api/admin/openrouter-settings') return handleAdminOpenRouterSettingsGet(req, res);
  if (req.method === 'POST' && pathname === '/api/admin/openrouter-settings') return handleAdminOpenRouterSettingsPost(req, res);
  if (req.method === 'GET' && pathname === '/api/admin/mistral-settings') return handleAdminMistralSettingsGet(req, res);
  if (req.method === 'POST' && pathname === '/api/admin/mistral-settings') return handleAdminMistralSettingsPost(req, res);
  if (req.method === 'GET' && pathname === '/api/admin/plan-settings') return handleAdminPlanSettingsGet(req, res);
  if (req.method === 'POST' && pathname === '/api/admin/plan-settings') return handleAdminPlanSettingsPost(req, res);
  if (req.method === 'GET' && pathname === '/api/admin/plan-tokens') return handleAdminPlanTokensGet(req, res);
  if (req.method === 'POST' && pathname === '/api/admin/plan-tokens') return handleAdminPlanTokensPost(req, res);
  if (req.method === 'GET' && pathname === '/api/admin/token-rates') return handleAdminTokenRatesGet(req, res);
  if (req.method === 'POST' && pathname === '/api/admin/token-rates') return handleAdminTokenRatesPost(req, res);
  if (req.method === 'GET' && pathname === '/api/admin/provider-limits') return handleAdminProviderLimitsGet(req, res);
  if (req.method === 'POST' && pathname === '/api/admin/provider-limits') return handleAdminProviderLimitsPost(req, res);
  // Admin-only env debug endpoint
  if (req.method === 'GET' && pathname === '/api/admin/env-config') return handleAdminEnvConfig(req, res);
  if (req.method === 'GET' && pathname === '/api/admin/accounts') return handleAdminAccountsList(req, res);
  if (req.method === 'POST' && pathname === '/api/admin/accounts/plan') return handleAdminAccountPlanUpdate(req, res);
  if (req.method === 'POST' && pathname === '/api/admin/accounts/tokens') return handleAdminAccountTokensPost(req, res);
  if (req.method === 'DELETE' && pathname === '/api/admin/accounts') return handleAdminAccountDelete(req, res);
  if (req.method === 'GET' && pathname === '/api/admin/affiliates') return handleAdminAffiliatesList(req, res);
  if (req.method === 'DELETE' && pathname === '/api/admin/affiliates') return handleAdminAffiliateDelete(req, res);
  if (req.method === 'GET' && pathname === '/api/admin/withdrawals') return handleAdminWithdrawalsList(req, res);
  if (req.method === 'PUT' && pathname === '/api/admin/withdrawals') return handleAdminWithdrawalUpdate(req, res);
  if (req.method === 'GET' && pathname === '/api/admin/tracking') return handleAdminTrackingStats(req, res);
  if (req.method === 'GET' && pathname === '/api/admin/resources') return handleAdminResources(req, res);
  if (req.method === 'GET' && pathname === '/api/admin/external-testing-status') return handleAdminExternalTestingStatus(req, res);
  if (req.method === 'POST' && pathname === '/api/admin/external-testing-self-test') return handleAdminExternalTestingSelfTest(req, res);
  if (req.method === 'POST' && pathname === '/api/upgrade-popup-tracking') return handleUpgradePopupTracking(req, res);
  if (req.method === 'POST' && pathname === '/api/admin/cancel-messages') return handleAdminCancelMessages(req, res);
  if (req.method === 'POST' && pathname === '/api/admin/ollama-test') return handleAdminOllamaTest(req, res);
  const adminDeleteMatch = pathname.match(/^\/api\/admin\/models\/([a-f0-9\-]+)$/i);
  if (req.method === 'DELETE' && adminDeleteMatch) return handleAdminModelDelete(req, res, adminDeleteMatch[1]);
  if (req.method === 'GET' && pathname === '/api/models') {
    const cliParam = url.searchParams.get('cli');
    return handleModels(req, res, cliParam);
  }
  if (req.method === 'POST' && pathname === '/api/plan') {
    const userId = requireUserId(req, res, url);
    if (!userId) return;
    return handlePlanMessage(req, res, userId);
  }
  if (req.method === 'GET' && pathname === '/api/sessions') {
    const userId = requireUserId(req, res, url);
    if (!userId) return;
    return handleListSessions(req, res, userId);
  }
  if (req.method === 'POST' && pathname === '/api/sessions') {
    const userId = requireUserId(req, res, url);
    if (!userId) return;
    return handleNewSession(req, res, userId);
  }
  const sessionMatch = pathname.match(/^\/api\/sessions\/([a-f0-9\-]+)$/i);
  if (req.method === 'DELETE' && sessionMatch) {
    const userId = requireUserId(req, res, url);
    if (!userId) return;
    return handleDeleteSession(req, res, sessionMatch[1], userId);
  }
  if (req.method === 'PATCH' && sessionMatch) {
    const userId = requireUserId(req, res, url);
    if (!userId) return;
    return handleUpdateSession(req, res, sessionMatch[1], userId);
  }
  if (req.method === 'GET' && sessionMatch) {
    const userId = requireUserId(req, res, url);
    if (!userId) return;
    return handleGetSession(req, res, sessionMatch[1], userId);
  }

  // GET todos for a session
  const todosMatch = pathname.match(/^\/api\/sessions\/([a-f0-9\-]+)\/todos$/i);
  if (req.method === 'GET' && todosMatch) {
    const userId = requireUserId(req, res, url);
    if (!userId) return;
    return handleGetSessionTodos(req, res, todosMatch[1], userId);
  }

  const messageMatch = pathname.match(/^\/api\/sessions\/([a-f0-9\-]+)\/messages$/i);
  if (req.method === 'POST' && messageMatch) {
    const userId = requireUserId(req, res, url);
    if (!userId) return;
    return handleNewMessage(req, res, messageMatch[1], userId);
  }
  const streamMatch = pathname.match(/^\/api\/sessions\/([a-f0-9\-]+)\/messages\/([a-f0-9\-]+)\/stream$/i);
  if (req.method === 'GET' && streamMatch) {
    const userId = requireUserId(req, res, url);
    if (!userId) return;
    return handleMessageStream(req, res, streamMatch[1], streamMatch[2], userId);
  }
  const statusMatch = pathname.match(/^\/api\/sessions\/([a-f0-9\-]+)\/messages\/([a-f0-9\-]+)\/status$/i);
  if (req.method === 'GET' && statusMatch) {
    const userId = requireUserId(req, res, url);
    if (!userId) return;
    return handleRunningStatus(req, res, statusMatch[1], statusMatch[2], userId);
  }
  // Undo route for opencode messages
  const undoMatch = pathname.match(/^\/api\/sessions\/([a-f0-9\-]+)\/messages\/([a-f0-9\-]+)\/undo$/i);
  if (req.method === 'POST' && undoMatch) {
    const userId = requireUserId(req, res, url);
    if (!userId) return;
    return handleUndoMessage(req, res, undoMatch[1], undoMatch[2], userId);
  }
  // Redo route for opencode messages
  const redoMatch = pathname.match(/^\/api\/sessions\/([a-f0-9\-]+)\/messages\/([a-f0-9\-]+)\/redo$/i);
  if (req.method === 'POST' && redoMatch) {
    const userId = requireUserId(req, res, url);
    if (!userId) return;
    return handleRedoMessage(req, res, redoMatch[1], redoMatch[2], userId);
  }
  const gitMatch = pathname.match(/^\/api\/git\/([a-z]+)$/i);
  if (gitMatch) return handleGit(req, res, gitMatch[1]);
  if (req.method === 'GET' && pathname === '/api/templates') return handleListTemplates(req, res);

  if (req.method === 'GET' && pathname === '/api/diagnostics') return handleDiagnostics(req, res);
  if (req.method === 'GET' && pathname === '/api/posthog-config.js') {
    res.writeHead(200, { 'Content-Type': 'application/javascript' });
    res.end(`window.posthogConfig = ${JSON.stringify({ apiKey: POSTHOG_API_KEY, apiHost: POSTHOG_API_HOST })};`);
    return;
  }
  if (req.method === 'GET' && pathname === '/api/export/zip') {
    const userId = requireUserId(req, res, url);
    if (!userId) return;
    const sessionId = url.searchParams.get('sessionId');
    if (!sessionId) return sendJson(res, 400, { error: 'sessionId is required for export' });
    return handleExportZip(req, res, sessionId, userId);
  }

  // Serve apps list UI - check if user has selected a plan
  if (pathname === '/apps' || pathname === '/apps/') {
    const session = getUserSession(req);
    if (session) {
      const user = findUserById(session.userId);
      const hasPlan = normalizePlanSelection(user?.plan);
      if (!hasPlan) {
        res.writeHead(302, { Location: '/select-plan' });
        res.end();
        return;
      }
    }
    return serveFile(res, safeStaticPath('apps.html'), 'text/html');
  }

  // Serve builder UI
  if (pathname === '/builder' || pathname === '/builder/') return serveFile(res, safeStaticPath('builder.html'), 'text/html');
  if (pathname === '/chat' || pathname === '/chat/') return serveFile(res, safeStaticPath('builder.html'), 'text/html');
  if (pathname.startsWith('/chat/')) {
    try {
      const filePath = safeStaticPath(pathname.replace('/chat/', ''));
      return serveFile(res, filePath, guessContentType(filePath));
    } catch (_) { }
  }
  if (pathname.startsWith('/uploads/')) {
    try {
      const uploadMatch = pathname.match(/^\/uploads\/([a-f0-9\-]+)\/([a-f0-9\-]+)\/(.+)$/i);
      if (!uploadMatch) throw new Error('Invalid upload path');
      const userId = requireUserId(req, res, url);
      if (!userId) return;
      const sessionId = uploadMatch[1];
      const attachmentKey = uploadMatch[2];
      const requestedName = uploadMatch[3].replace(/\.\.+/g, '');
      const session = getSession(sessionId, userId);
      if (!session) return sendJson(res, 404, { error: 'File not found' });
      await ensureSessionPaths(session);
      if (session.attachmentKey && session.attachmentKey !== attachmentKey) return sendJson(res, 403, { error: 'Invalid attachment token' });
      const filePath = path.join(session.uploadsDir, requestedName);
      const resolvedPath = path.resolve(filePath);
      const uploadsRoot = path.resolve(session.uploadsDir);
      if (!resolvedPath.startsWith(uploadsRoot)) throw new Error('Invalid path');
      const stat = await fs.stat(resolvedPath);
      if (!stat.isFile()) throw new Error('Not a file');
      const content = await fs.readFile(resolvedPath);
      const contentType = guessContentTypeFromExt(path.extname(resolvedPath));
      res.writeHead(200, { 'Content-Type': contentType });
      res.end(content);
      return;
    } catch (_) { }
  }
  if (pathname === '/admin/login') return serveFile(res, safeStaticPath('admin-login.html'), 'text/html');
  if (pathname === '/admin' || pathname === '/admin/') {
    const session = getAdminSession(req);
    if (!session) return serveFile(res, safeStaticPath('admin-login.html'), 'text/html');
    return serveFile(res, safeStaticPath('admin.html'), 'text/html');
  }
  if (pathname === '/admin/build') {
    const session = getAdminSession(req);
    if (!session) return serveFile(res, safeStaticPath('admin-login.html'), 'text/html');
    return serveFile(res, safeStaticPath('admin.html'), 'text/html');
  }
  if (pathname === '/admin/plan') {
    const session = getAdminSession(req);
    if (!session) return serveFile(res, safeStaticPath('admin-login.html'), 'text/html');
    return serveFile(res, safeStaticPath('admin-plan.html'), 'text/html');
  }
  if (pathname === '/admin/plans') {
    const session = getAdminSession(req);
    if (!session) return serveFile(res, safeStaticPath('admin-login.html'), 'text/html');
    return serveFile(res, safeStaticPath('admin-plans.html'), 'text/html');
  }
  if (pathname === '/admin/accounts') {
    const session = getAdminSession(req);
    if (!session) return serveFile(res, safeStaticPath('admin-login.html'), 'text/html');
    return serveFile(res, safeStaticPath('admin-accounts.html'), 'text/html');
  }
  if (pathname === '/admin/affiliates') {
    const session = getAdminSession(req);
    if (!session) return serveFile(res, safeStaticPath('admin-login.html'), 'text/html');
    return serveFile(res, safeStaticPath('admin-affiliates.html'), 'text/html');
  }
  if (pathname === '/admin/withdrawals') {
    const session = getAdminSession(req);
    if (!session) return serveFile(res, safeStaticPath('admin-login.html'), 'text/html');
    return serveFile(res, safeStaticPath('admin-withdrawals.html'), 'text/html');
  }
  if (pathname === '/admin/tracking') {
    const session = getAdminSession(req);
    if (!session) return serveFile(res, safeStaticPath('admin-login.html'), 'text/html');
    return serveFile(res, safeStaticPath('admin-tracking.html'), 'text/html');
  }
  if (pathname === '/admin/resources') {
    const session = getAdminSession(req);
    if (!session) return serveFile(res, safeStaticPath('admin-login.html'), 'text/html');
    return serveFile(res, safeStaticPath('admin-resources.html'), 'text/html');
  }
  if (pathname === '/admin/external-testing') {
    const session = getAdminSession(req);
    if (!session) return serveFile(res, safeStaticPath('admin-login.html'), 'text/html');
    return serveFile(res, safeStaticPath('admin-external-testing.html'), 'text/html');
  }
  if (pathname === '/admin/contact-messages') {
    const session = getAdminSession(req);
    if (!session) return serveFile(res, safeStaticPath('admin-login.html'), 'text/html');
    return serveFile(res, safeStaticPath('admin-contact-messages.html'), 'text/html');
  }
  // Homepage serves landing page, /index.html serves homepage
  if (pathname === '/' || pathname === '/index.html') return serveFile(res, safeStaticPath('home.html'), 'text/html');
  if (pathname === '/login') {
    const session = getUserSession(req);
    if (session) {
      const user = findUserById(session.userId);
      const hasPlan = normalizePlanSelection(user?.plan);
      if (hasPlan) {
        res.writeHead(302, { Location: '/apps' });
        res.end();
        return;
      }
    }
    return serveFile(res, safeStaticPath('login.html'), 'text/html');
  }
  if (pathname === '/signup') {
    const session = getUserSession(req);
    if (session) {
      const user = findUserById(session.userId);
      const hasPlan = normalizePlanSelection(user?.plan);
      if (hasPlan) {
        res.writeHead(302, { Location: '/apps' });
        res.end();
        return;
      }
    }
    return serveFile(res, safeStaticPath('signup.html'), 'text/html');
  }
  if (pathname === '/signup-success') return serveFile(res, safeStaticPath('signup-success.html'), 'text/html');
  if (pathname === '/select-plan') {
    const session = getUserSession(req);
    if (!session) return serveFile(res, safeStaticPath('login.html'), 'text/html');
    const user = findUserById(session.userId);
    const hasPlan = normalizePlanSelection(user?.plan);
    if (hasPlan) {
      res.writeHead(302, { Location: '/apps' });
      res.end();
      return;
    }
    return serveFile(res, safeStaticPath('select-plan.html'), 'text/html');
  }
  if (pathname === '/verify-email') return serveFile(res, safeStaticPath('verify-email.html'), 'text/html');
  if (pathname === '/reset-password') return serveFile(res, safeStaticPath('reset-password.html'), 'text/html');
  if (pathname === '/topup' || pathname === '/topup/') return serveFile(res, safeStaticPath('topup.html'), 'text/html');
  if (pathname === '/test-checkout' || pathname === '/test-checkout/') {
    const adminSession = getAdminSession(req);
    if (!adminSession) {
      res.writeHead(302, { Location: '/admin/login?next=' + encodeURIComponent('/test-checkout') });
      res.end();
      return;
    }
    return serveFile(res, safeStaticPath('test-checkout.html'), 'text/html');
  }
  if (pathname === '/upgrade' || pathname === '/upgrade/') return serveFile(res, safeStaticPath('upgrade.html'), 'text/html');
  if (pathname === '/pricing') return serveFile(res, safeStaticPath('pricing.html'), 'text/html');
  if (pathname === '/credits') return serveFile(res, safeStaticPath('credits.html'), 'text/html');
  if (pathname === '/features') return serveFile(res, safeStaticPath('features.html'), 'text/html');
  if (pathname === '/subscription-success') return serveFile(res, safeStaticPath('subscription-success.html'), 'text/html');
  if (pathname === '/affiliate' || pathname === '/affiliates') return serveFile(res, safeStaticPath('affiliate.html'), 'text/html');
  if (pathname === '/affiliate-login') return serveFile(res, safeStaticPath('affiliate-login.html'), 'text/html');
  if (pathname === '/affiliate-signup') return serveFile(res, safeStaticPath('affiliate-signup.html'), 'text/html');
  if (pathname === '/affiliate-dashboard') return serveFile(res, safeStaticPath('affiliate-dashboard.html'), 'text/html');
  if (pathname === '/affiliate-withdrawal') return serveFile(res, safeStaticPath('affiliate-withdrawal.html'), 'text/html');
  if (pathname === '/affiliate-transactions') return serveFile(res, safeStaticPath('affiliate-transactions.html'), 'text/html');
  if (pathname === '/docs' || pathname === '/documentation') return serveFile(res, safeStaticPath('docs.html'), 'text/html');
  if (pathname === '/faq' || pathname === '/faqs') return serveFile(res, safeStaticPath('faq.html'), 'text/html');
  if (pathname === '/settings') return serveFile(res, safeStaticPath('settings.html'), 'text/html');
  if (pathname === '/feature-requests') return serveFile(res, safeStaticPath('feature-requests.html'), 'text/html');
  // Blog pages
  if (pathname === '/news' || pathname === '/news/') return serveFile(res, safeStaticPath('news.html'), 'text/html');
  const blogPostMatch = pathname.match(/^\/blog\/(.+)$/i);
  if (blogPostMatch) return serveFile(res, safeStaticPath('blog.html'), 'text/html');
  if (pathname === '/admin/blogs') {
    const session = getAdminSession(req);
    if (!session) return serveFile(res, safeStaticPath('admin-login.html'), 'text/html');
    return serveFile(res, safeStaticPath('admin-blogs.html'), 'text/html');
  }
  // Serve legal pages with proper caching headers
  if (pathname === '/terms') return serveFile(res, safeStaticPath('terms.html'), 'text/html');
  if (pathname === '/privacy') return serveFile(res, safeStaticPath('privacy.html'), 'text/html');
  // Redirect legacy /contact.html to canonical /contact
  if (pathname === '/contact.html' || pathname === '/contact.html/') {
    res.writeHead(301, { Location: '/contact' });
    res.end();
    return;
  }
  // Contact page (serve contact.html at /contact)
  if (pathname === '/contact' || pathname === '/contact/') return serveFile(res, safeStaticPath('contact.html'), 'text/html');
  // Serve sitemap.xml with proper caching headers for SEO
  if (pathname === '/sitemap.xml') {
    try {
      const content = await fs.readFile(safeStaticPath('sitemap.xml'));
      res.writeHead(200, {
        'Content-Type': 'application/xml',
        'Cache-Control': 'public, max-age=86400', // Cache for 24 hours
      });
      res.end(content);
      return;
    } catch (_) { }
  }
  // Serve robots.txt with proper caching headers for SEO
  if (pathname === '/robots.txt') {
    try {
      const content = await fs.readFile(safeStaticPath('robots.txt'));
      res.writeHead(200, {
        'Content-Type': 'text/plain',
        'Cache-Control': 'public, max-age=86400', // Cache for 24 hours
      });
      res.end(content);
      return;
    } catch (_) { }
  }
  try {
    const staticFile = safeStaticPath(pathname.replace(/^\//, ''));
    const stat = await fs.stat(staticFile);
    if (stat.isFile()) return serveFile(res, staticFile, guessContentType(staticFile));
  } catch (_) { }
  try {
    const content = await fs.readFile(safeStaticPath('404.html'));
    res.writeHead(404, { 'Content-Type': 'text/html' });
    res.end(content);
  } catch (_) {
    res.writeHead(404, { 'Content-Type': 'text/html' });
    res.end('<h1>404 Not Found</h1><p>The page you are looking for does not exist.</p>');
  }
}

async function bootstrap() {
  // Production environment validation
  const isProduction = process.env.NODE_ENV === 'production';
  const criticalEnvVars = [];
  
  if (isProduction) {
    if (!process.env.USER_SESSION_SECRET && !process.env.SESSION_SECRET) {
      criticalEnvVars.push('USER_SESSION_SECRET or SESSION_SECRET');
    }
    if (!process.env.DODO_PAYMENTS_API_KEY && !process.env.DODO_API_KEY) {
      criticalEnvVars.push('DODO_PAYMENTS_API_KEY');
    }
    
    if (criticalEnvVars.length > 0) {
      console.error('❌ CRITICAL: Missing required environment variables for production:');
      criticalEnvVars.forEach(v => console.error(`   - ${v}`));
      console.error('Please set these environment variables before running in production.');
      process.exit(1);
    }
  }

  process.on('uncaughtException', async (error) => {
    log('Uncaught Exception, saving state before exit', { error: String(error), stack: error.stack });
    try {
      await persistAllState();
    } catch (saveError) {
      log('Failed to save state during uncaughtException', { error: String(saveError) });
    }
    process.exit(1);
  });

  process.on('unhandledRejection', (reason, promise) => {
    log('Unhandled Rejection', { reason: String(reason), promise: String(promise) });
  });

  process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
  process.on('SIGINT', () => gracefulShutdown('SIGINT'));

  // Load admin models FIRST (before sessions) so ensureOpencodeConfig can use them
  await loadAdminModelStore();
  await loadOpenRouterSettings();
  await loadMistralSettings();
  await loadPlanSettings();
  await loadPlanTokenLimits();
  await loadProviderLimits();
  await loadProviderUsage();
  await loadTokenUsage();
  await loadExternalTestUsage();
  await loadTopupSessions();
  await loadPendingTopups();
  await loadPaygSessions();
  await loadState(); // Load sessions LAST so adminModels is ready
  await loadPendingPayg();
  await loadSubscriptionSessions();
  await loadPendingSubscriptions();
  await loadInvoicesDb();
  await loadUsersDb(); // Load user authentication database
  await loadUserSessions(); // Load user sessions
  await loadAffiliatesDb();
  await loadWithdrawalsDb();
  await loadTrackingData();
  await loadProcessedWebhooks();
  await loadFeatureRequestsDb();
  contactMessagesDb = await loadContactMessagesDb();
  await ensureAssetsDir();

  // Clean up orphaned workspace directories from deleted sessions
  await cleanupOrphanedWorkspaces();

  // Initialize admin password hashing
  if (ADMIN_USER && ADMIN_PASSWORD) {
    try {
      adminPasswordHash = await bcrypt.hash(ADMIN_PASSWORD, PASSWORD_SALT_ROUNDS);
      log('admin password hashed successfully');
    } catch (error) {
      log('failed to hash admin password', { error: String(error) });
    }
  }

  log('Resource limits detected', {
    memoryBytes: RESOURCE_LIMITS.memoryBytes,
    memoryMb: Math.round((RESOURCE_LIMITS.memoryBytes || 0) / (1024 * 1024)),
    cpuCores: RESOURCE_LIMITS.cpuCores
  });

  // Log provider configuration
  console.log('=== PROVIDER CONFIGURATION ===');
  console.log('[CONFIG] OpenRouter:', {
    configured: !!OPENROUTER_API_KEY,
    apiUrl: OPENROUTER_API_URL,
    primaryModel: openrouterSettings.primaryModel || 'not set',
    hasApiKey: !!OPENROUTER_API_KEY
  });
  console.log('[CONFIG] Mistral:', {
    configured: !!MISTRAL_API_KEY,
    apiUrl: MISTRAL_API_URL,
    primaryModel: mistralSettings.primaryModel || 'not set',
    backupModel1: mistralSettings.backupModel1 || 'not set',
    backupModel2: mistralSettings.backupModel2 || 'not set',
    backupModel3: mistralSettings.backupModel3 || 'not set',
    hasApiKey: !!MISTRAL_API_KEY,
    apiKeyPrefix: MISTRAL_API_KEY ? MISTRAL_API_KEY.substring(0, 8) + '...' : 'none',
    defaultModel: MISTRAL_DEFAULT_MODEL
  });
  console.log('[CONFIG] Groq:', {
    configured: !!GROQ_API_KEY,
    apiUrl: GROQ_API_URL,
    hasApiKey: !!GROQ_API_KEY,
    apiKeyPrefix: GROQ_API_KEY ? GROQ_API_KEY.substring(0, 8) + '...' : 'none'
  });
  console.log('[CONFIG] Planning Settings:', {
    provider: planSettings.provider,
    freePlanModel: planSettings.freePlanModel || 'not set',
    planningChainLength: planSettings.planningChain?.length || 0,
    planningChain: planSettings.planningChain
  });

  const mailConfig = summarizeMailConfig();
  console.log('[CONFIG] MailPilot Email:');
  console.log('  - MailPilot Configured:', mailConfig.configured ? 'YES ✓' : 'NO ✗');
  console.log('  - MailPilot URL:', MAILPILOT_URL);
  console.log('');
  if (!mailConfig.configured) {
    console.log('  ⚠️  WARNING: MailPilot is NOT configured. Password reset and verification');
    console.log('      emails will be logged to console only. To enable real emails:');
    console.log('      1. Edit the .env file');
    console.log('      2. Set MAILPILOT_TOKEN to your transactional email token');
    console.log('      3. Restart the server');
    console.log('  💡 Tip: Use /debug/email/preview?type=reset to preview the email template');
  } else {
    console.log('  ✓ MailPilot is configured and ready to send emails');
  }
  console.log('');
  console.log('==============================');

  log('Environment PATH', { PATH: process.env.PATH });
  try {
    const versionCheck = await runCommand('opencode', ['--version'], { timeout: 5000 }).catch((e) => ({ stdout: e.stdout, stderr: e.stderr, code: e.code }));
  log('opencode version', { version: versionCheck.stdout || versionCheck.stderr });
  } catch (_) {
    log('opencode version check failed');
  }
  
  // Initialize OpenCode process manager
  log('Initializing OpenCode process manager...');
  try {
    await opencodeManager.start();
    log('OpenCode process manager initialized', opencodeManager.getStats());
  } catch (err) {
    log('OpenCode process manager initialization failed (will fall back to per-message spawning)', { error: String(err) });
  }

  // Initialize blog system
  log('Initializing blog system...');
  try {
    await blogSystem.initBlogStorage();
    log('Blog system initialized successfully');
  } catch (err) {
    log('Blog system initialization failed', { error: String(err) });
  }
  
  // Restore interrupted sessions after restart
  await restoreInterruptedSessions();
  
  startAutoSave();
  
  // Start memory cleanup scheduler
  startMemoryCleanup();
  
  // Start periodic resource monitoring for analytics
  startPeriodicMonitoring();

  server = http.createServer((req, res) => { route(req, res); });

  // Disable Node's built-in request timeouts so long-running SSE streams / long LLM "thinking" phases
  // don't get cut off (Node defaults can be ~5 minutes depending on version).
  server.requestTimeout = 0;
  server.headersTimeout = 0;
  server.timeout = 0;

  server.listen(PORT, HOST, () => { log(`${OPENROUTER_APP_NAME} listening on http://${HOST}:${PORT}`); });
}

function startPeriodicMonitoring() {
  // Track system health metrics every 5 minutes
  setInterval(() => {
    try {
      trackResourceUtilization();
      
      // Update system health
      trackingData.technicalMetrics.systemHealth.uptime = process.uptime();
      
      // Calculate queue wait times (simplified)
      const activeSessions = state.sessions.filter(s => s.pending > 0).length;
      if (activeSessions > 0) {
        const estimatedWaitTime = activeSessions * 1000; // 1 second per active session
        trackQueueMetrics(estimatedWaitTime, activeSessions);
      }
      
    } catch (error) {
      log('Periodic monitoring error', { error: String(error) });
    }
  }, 5 * 60 * 1000); // 5 minutes

  // Clean up old data daily
  setInterval(() => {
    try {
      // Clean up old resource utilization data (keep last 7 days)
      const sevenDaysAgo = Date.now() - (7 * 24 * 60 * 60 * 1000);
      const recentResourceData = {};
      for (const [timestamp, data] of Object.entries(trackingData.userAnalytics.resourceUtilization)) {
        if (parseInt(timestamp) > sevenDaysAgo) {
          recentResourceData[timestamp] = data;
        }
      }
      trackingData.userAnalytics.resourceUtilization = recentResourceData;
      
      // Clean up old queue metrics (keep last 3 days)
      const threeDaysAgo = Date.now() - (3 * 24 * 60 * 60 * 1000);
      const recentQueueData = {};
      for (const [timestamp, data] of Object.entries(trackingData.userAnalytics.queueMetrics)) {
        if (parseInt(timestamp) > threeDaysAgo) {
          recentQueueData[timestamp] = data;
        }
      }
      trackingData.userAnalytics.queueMetrics = recentQueueData;
      
      // Clean up old active user data (keep last 30 days)
      const thirtyDaysAgo = Date.now() - (30 * 24 * 60 * 60 * 1000);
      const recentDAU = {};
      for (const [date, users] of Object.entries(trackingData.userAnalytics.dailyActiveUsers)) {
        const dateTime = new Date(date).getTime();
        if (dateTime > thirtyDaysAgo) {
          recentDAU[date] = users;
        }
      }
      trackingData.userAnalytics.dailyActiveUsers = recentDAU;
      
      scheduleTrackingPersist();
      
    } catch (error) {
      log('Data cleanup error', { error: String(error) });
    }
  }, 24 * 60 * 60 * 1000); // 24 hours
}


async function handleListTemplates(req, res) {
  try {
    const templatesPath = path.join(__dirname, 'templates', 'templates.json');
    const content = await fs.readFile(templatesPath, 'utf-8');
    const templates = JSON.parse(content);
    sendJson(res, 200, { templates });
  } catch (error) {
    if (error.code === 'ENOENT') {
      return sendJson(res, 200, { templates: [] });
    }
    log('Failed to list templates', { error: String(error) });
    sendJson(res, 500, { error: 'Failed to list templates' });
  }
}

bootstrap().catch((error) => { log(`Failed to start ${OPENROUTER_APP_NAME} service`, { error: String(error) }); process.exit(1); });
