const express = require('express');
const path = require('path');
const cors = require('cors');
const fetch = require('node-fetch');
const crypto = require('crypto');
const { exec, execFile } = require('child_process');
const compression = require('compression');

// Load environment variables if dotenv is available
try {
    require('dotenv').config({ path: require('path').join(__dirname, '..', '.env') });
} catch (err) {
    // dotenv not installed, continue without it (production uses env vars)
}

const fs = require('fs');
const fsp = require('fs/promises');
const seasonsUtil = require('./utils/seasons');

// ── i18n: load translation files ──
const locales = {
    en: JSON.parse(fs.readFileSync(path.join(__dirname, 'locales', 'en.json'), 'utf8')),
    ru: JSON.parse(fs.readFileSync(path.join(__dirname, 'locales', 'ru.json'), 'utf8')),
    fr: JSON.parse(fs.readFileSync(path.join(__dirname, 'locales', 'fr.json'), 'utf8')),
    it: JSON.parse(fs.readFileSync(path.join(__dirname, 'locales', 'it.json'), 'utf8')),
    uk: JSON.parse(fs.readFileSync(path.join(__dirname, 'locales', 'uk.json'), 'utf8')),
    de: JSON.parse(fs.readFileSync(path.join(__dirname, 'locales', 'de.json'), 'utf8')),
    es: JSON.parse(fs.readFileSync(path.join(__dirname, 'locales', 'es.json'), 'utf8')),
    pl: JSON.parse(fs.readFileSync(path.join(__dirname, 'locales', 'pl.json'), 'utf8')),
    cs: JSON.parse(fs.readFileSync(path.join(__dirname, 'locales', 'cs.json'), 'utf8')),
    'zh-CN': JSON.parse(fs.readFileSync(path.join(__dirname, 'locales', 'zh-CN.json'), 'utf8'))
};

function mergeLocale(base, override) {
    if (!override || typeof override !== 'object' || Array.isArray(override)) {
        return override ?? base;
    }
    const out = { ...base };
    for (const [key, val] of Object.entries(override)) {
        const baseVal = base && base[key];
        out[key] = val && typeof val === 'object' && !Array.isArray(val)
            ? mergeLocale(baseVal && typeof baseVal === 'object' ? baseVal : {}, val)
            : val;
    }
    return out;
}

function getLang(req) {
    // 1. query param  2. cookie  3. default
    if (req.query.lang && locales[req.query.lang]) return req.query.lang;
    const cookies = req.headers.cookie || '';
    const match = cookies.match(/(?:^|;\s*)lang=([\w-]+)/);
    if (match && locales[match[1]]) return match[1];
    return 'en';
}

function t(lang, key) {
    const parts = key.split('.');
    let val = locales[lang];
    for (const p of parts) { val = val && val[p]; }
    if (val !== undefined) return val;
    // fallback to English
    val = locales.en;
    for (const p of parts) { val = val && val[p]; }
    return val !== undefined ? val : key;
}

function getRosterMemberByUid(uid) {
    return new Promise((resolve) => {
        if (!squadronsDb || !uid) return resolve(null);
        squadronsDb.get(
            `
                SELECT sm.uid, sm.nick, sm.clan_id, sm.points, sd.tag_name, sd.short_name, sd.long_name
                FROM squadron_members sm
                LEFT JOIN squadrons_data sd ON sm.clan_id = sd.clan_id
                WHERE sm.uid = ?
                ORDER BY sm.updated_at DESC
                LIMIT 1
            `,
            [uid],
            (err, row) => {
                if (err) {
                    log.error('Failed to load roster member by UID', err, { uid });
                    return resolve(null);
                }
                resolve(row || null);
            }
        );
    });
}

const sqlite3 = require('sqlite3').verbose();
const STORAGE_ROOT = (process.env.STORAGE_VOL_PATH || '').trim();
if (!STORAGE_ROOT) {
    throw new Error('STORAGE_VOL_PATH must be set');
}
const REPLAYS_ROOT = path.join(STORAGE_ROOT, 'REPLAYS');
fs.mkdirSync(REPLAYS_ROOT, { recursive: true });
const LEGACY_REPLAYS_ROOT = path.join(__dirname, '..', 'replays');
const ENTITLEMENTS_DB_PATH = path.join(STORAGE_ROOT, 'entitlements.db');
const ENTITLEMENTS_DIRTY_PATH = path.join(STORAGE_ROOT, 'entitlements.dirty');
const SQUADRONS_DB_PATH = path.join(STORAGE_ROOT, 'squadrons.db');
const SQUADRONS_JSON_PATH = path.join(STORAGE_ROOT, 'SQUADRONS.json');
const entitlementsDb = new sqlite3.Database(ENTITLEMENTS_DB_PATH);

// Bump the mtime on a marker file so the bot process (which caches
// entitlements in memory, see BOT/utils.py refresh_entitled_guilds) picks up
// writes to guild_entitlements within a few seconds instead of waiting for
// its hourly fallback refresh. The bot polls this file's mtime rather than
// us calling into it directly, since the bot and web/webhook run as
// separate pm2 processes with no other IPC between them.
function touchEntitlementsDirty() {
    fs.writeFile(ENTITLEMENTS_DIRTY_PATH, String(Date.now()), (err) => {
        if (err) log.error('[WHOP] Failed to write entitlements dirty flag:', err);
    });
}
const squadronsDb = fs.existsSync(SQUADRONS_DB_PATH)
    ? new sqlite3.Database(SQUADRONS_DB_PATH, sqlite3.OPEN_READONLY)
    : null;
entitlementsDb.run(`CREATE TABLE IF NOT EXISTS guild_entitlements (
    guild_id TEXT PRIMARY KEY,
    whop_membership_id TEXT,
    status TEXT DEFAULT 'active',
    renewed_at INTEGER DEFAULT (strftime('%s','now')),
    tier TEXT
)`);
entitlementsDb.run(`CREATE TABLE IF NOT EXISTS manual_entitlements (
    guild_id TEXT PRIMARY KEY,
    expires_at INTEGER NOT NULL,
    created_at INTEGER DEFAULT (strftime('%s','now')),
    tier TEXT
)`);
entitlementsDb.run(`CREATE TABLE IF NOT EXISTS discord_entitlements (
    guild_id TEXT PRIMARY KEY,
    sku_id TEXT,
    updated_at INTEGER DEFAULT (strftime('%s','now')),
    tier TEXT
)`);

// Ensure `tier` column exists on pre-existing DBs, then backfill NULL rows to 'standard'.
// Both operations are idempotent and safe to run on every boot.
entitlementsDb.serialize(() => {
    for (const table of ['guild_entitlements', 'manual_entitlements', 'discord_entitlements']) {
        // ALTER fails with "duplicate column" if the column already exists — ignore that error.
        entitlementsDb.run(`ALTER TABLE ${table} ADD COLUMN tier TEXT`, (err) => {
            if (err && !/duplicate column/i.test(err.message)) {
                console.error(`[ENTITLEMENTS] ALTER ${table} failed:`, err.message);
            }
        });
        entitlementsDb.run(
            `UPDATE ${table} SET tier='standard' WHERE tier IS NULL OR tier=''`,
            (err) => {
                if (err) console.error(`[ENTITLEMENTS] backfill ${table} failed:`, err.message);
            }
        );
    }
});

// Production-safe logging
const isDev = process.env.NODE_ENV !== 'production';
const log = {
    info: (...args) => isDev && console.log(...args),
    warn: (...args) => console.warn(...args),
    error: (...args) => console.error(...args),
    debug: (...args) => isDev && console.log('[DEBUG]', ...args)
};

const REPO_ROOT    = process.env.BOT_REPO_ROOT || path.join(__dirname, '..');
const SQUADRON_RECAP_CACHE_DIR = path.join(STORAGE_ROOT, 'RECAPS', 'squadrons');
const PLAYER_RECAP_CACHE_DIR   = path.join(STORAGE_ROOT, 'RECAPS', 'players');
const RECAP_TTL_MS    = 24 * 60 * 60 * 1000; // in-progress season TTL
const RECAP_RENDER_TIMEOUT_MS = 30_000;
const PYTHON_BIN   = process.env.PYTHON_BIN || path.join(REPO_ROOT, '..', 'SHARED', '.venv', 'bin', 'python');
const RECAP_SCRIPT = process.env.RECAP_SCRIPT || path.join(REPO_ROOT, 'BOT', 'render_recap.py');

function resolveReplaySessionDir(sessionId) {
    const sid = String(sessionId).toLowerCase();
    const candidates = [
        path.join(REPLAYS_ROOT, 'SRE', sid),
        path.join(REPLAYS_ROOT, 'TSS', sid),
        path.join(REPLAYS_ROOT, sid),
        path.join(REPLAYS_ROOT, `0${sid}`),
        path.join(LEGACY_REPLAYS_ROOT, sid),
        path.join(LEGACY_REPLAYS_ROOT, `0${sid}`)
    ];

    for (const dir of candidates) {
        if (fs.existsSync(dir) && fs.statSync(dir).isDirectory()) return dir;
    }

    return path.join(REPLAYS_ROOT, sid);
}

const app = express();
const PORT = process.env.SREBOT_WEB_PORT || 3001;
const IS_PRIMARY_WORKER = !process.env.NODE_APP_INSTANCE || process.env.NODE_APP_INSTANCE === '0';

// Enable compression for all responses
app.use(compression({
    level: 6, // Good balance of compression vs CPU
    threshold: 1024, // Only compress responses > 1KB
    filter: (req, res) => {
        if (req.headers['x-no-compression']) {
            return false;
        }
        return compression.filter(req, res);
    }
}));

// CORS Configuration
const corsOptions = {
    origin: function (origin, callback) {
        const allowedOrigins = process.env.NODE_ENV === 'production' 
            ? [process.env.PRODUCTION_DOMAIN || 'https://sre.pawjob.us'] 
            : ['http://localhost:3000', 'http://localhost:3001'];
        

        if (!origin || allowedOrigins.includes(origin)) {
            callback(null, true);
        } else {
            log.warn(`[CORS] Blocked general request from origin: ${origin}`);
            callback(new Error('Not allowed by CORS policy'));
        }
    },
    credentials: true,
    methods: ['GET', 'POST', 'HEAD', 'OPTIONS'],
    allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'],
    exposedHeaders: ['X-Total-Count'],
    maxAge: 86400 // 24 hours
};


const apiCorsOptions = {
    origin: function (origin, callback) {
        const allowedOrigins = process.env.NODE_ENV === 'production' 
            ? [process.env.PRODUCTION_DOMAIN || 'https://sre.pawjob.us'] 
            : ['http://localhost:3000', 'http://localhost:3001'];
        

        // Allow our domains AND same-origin requests (null origin from legitimate browser AJAX)
        if (!origin || allowedOrigins.includes(origin)) {
            callback(null, true);
        } else {
            log.warn(`[CORS] Blocked API request from origin: ${origin || 'null/direct'}`);
            callback(new Error('API access restricted to authorized domains only'));
        }
    },
    credentials: false,
    methods: ['GET', 'OPTIONS'], // Only allow GET and preflight
    allowedHeaders: ['Content-Type', 'X-Requested-With'],
    maxAge: 3600
};

app.use(cors(corsOptions));
app.use(express.json({
    limit: '10mb',
    verify: (req, res, buf) => { req.rawBody = buf; }
}));
app.use(express.urlencoded({ extended: true, limit: '10mb' }));

// Serve obfuscated JS in production
if (process.env.NODE_ENV === 'production') {
    app.use('/js', (req, res, next) => {
        if (req.path.endsWith('.js')) {
            const obfuscatedPath = path.join(__dirname, 'public', 'js', 'dist', path.basename(req.path));
            if (require('fs').existsSync(obfuscatedPath)) {
                return res.sendFile(obfuscatedPath);
            }
        }
        next();
    });
}

// Serve static files with proper MIME types and caching
app.use(express.static('public', {
    etag: true,
    lastModified: true,
    setHeaders: (res, filePath) => {
        // Fonts never change in-place — safe to cache forever
        if (filePath.match(/\.(ttf|woff|woff2)$/)) {
            res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
        // CSS, JS, and images are updated in-place — allow revalidation after 10 minutes
        } else if (filePath.match(/\.(css|js|jpg|jpeg|png|gif|webp|svg|ico)$/)) {
            res.setHeader('Cache-Control', 'public, max-age=600, must-revalidate');
        // Don't cache HTML
        } else if (filePath.endsWith('.html')) {
            res.setHeader('Cache-Control', 'no-cache, must-revalidate');
        }
    }
}));

// Font files with proper headers and CORS
app.use('/fonts', express.static('Fonts', {
    setHeaders: (res, path) => {
        if (path.endsWith('.ttf')) {
            res.setHeader('Content-Type', 'font/ttf');
            res.setHeader('Cache-Control', 'public, max-age=31536000');
            res.setHeader('Access-Control-Allow-Origin', '*');
        } else if (path.endsWith('.woff')) {
            res.setHeader('Content-Type', 'font/woff');
            res.setHeader('Cache-Control', 'public, max-age=31536000');
            res.setHeader('Access-Control-Allow-Origin', '*');
        } else if (path.endsWith('.woff2')) {
            res.setHeader('Content-Type', 'font/woff2');
            res.setHeader('Cache-Control', 'public, max-age=31536000');
            res.setHeader('Access-Control-Allow-Origin', '*');
        }
    }
}));
app.use('/Fonts', express.static('Fonts', {
    setHeaders: (res, path) => {
        if (path.endsWith('.ttf')) {
            res.setHeader('Content-Type', 'font/ttf');
            res.setHeader('Cache-Control', 'public, max-age=31536000');
            res.setHeader('Access-Control-Allow-Origin', '*');
        } else if (path.endsWith('.woff')) {
            res.setHeader('Content-Type', 'font/woff');
            res.setHeader('Cache-Control', 'public, max-age=31536000');
            res.setHeader('Access-Control-Allow-Origin', '*');
        } else if (path.endsWith('.woff2')) {
            res.setHeader('Content-Type', 'font/woff2');
            res.setHeader('Cache-Control', 'public, max-age=31536000');
            res.setHeader('Access-Control-Allow-Origin', '*');
        }
    }
}));

app.use('/ICONS', express.static('ICONS'));
app.use('/MAPS', express.static('MAPS'));
app.use('/constants', express.static('constants'));

// Leaderboard Cache System (5-minute intervals)
const leaderboardCache = {
    players: { data: null, lastUpdated: 0, updating: false },
    vehicles: { data: null, lastUpdated: 0, updating: false },
    stats: { data: null, lastUpdated: 0, updating: false },
    squadrons: { data: null, lastUpdated: 0, updating: false }
};

const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes in milliseconds

// LRU Cache for search results
class LRUCache {
    constructor(maxSize = 500, ttlMs = 15 * 60 * 1000) {
        this.cache = new Map();
        this.maxSize = maxSize;
        this.ttlMs = ttlMs;
    }
    
    get(key) {
        if (!this.cache.has(key)) return null;
        
        const item = this.cache.get(key);
        // Check if expired
        if (Date.now() - item.timestamp > this.ttlMs) {
            this.cache.delete(key);
            return null;
        }
        
        // Move to end (most recently used)
        this.cache.delete(key);
        this.cache.set(key, item);
        return item.data;
    }
    
    set(key, data) {
        // Remove if already exists
        if (this.cache.has(key)) {
            this.cache.delete(key);
        }
        
        // Remove oldest if at capacity
        if (this.cache.size >= this.maxSize) {
            const firstKey = this.cache.keys().next().value;
            this.cache.delete(firstKey);
        }
        
        this.cache.set(key, {
            data,
            timestamp: Date.now()
        });
    }
    
    size() {
        return this.cache.size;
    }
    
    clear() {
        this.cache.clear();
    }
}

const searchCache = new LRUCache(500); // Cache up to 500 unique searches
const squadronProfileCache = new LRUCache(1000, 10 * 60 * 1000); // 10 minute TTL

// Search request deduplication
const pendingSearchRequests = new Map();
const pendingSquadronRequests = new Map();
const SQUADRON_API_TIMEOUT_MS = 120000;

// Function to update cache for a specific type
async function updateCache(type) {
    const cache = leaderboardCache[type];
    
    // Prevent multiple simultaneous updates
    if (cache.updating) {
        log.debug(`[CACHE] Cache update for ${type} already in progress, skipping...`);
        return;
    }
    
    cache.updating = true;
    log.info(`[CACHE] Updating ${type} cache...`);
    
    try {
        // For the player leaderboard, send a bounded date range so the backend
        // doesn't reject the request.  The backend's /api/leaderboard/players
        // refuses uncached all-time queries because the DB aggregation is
        // expensive (grouping every player across all time).  A one-year
        // window covers all active squadrons for the homepage search without
        // scanning the full history.
        let urlSuffix = `/api/leaderboard/${type}`;
        if (type === 'players') {
            const since = new Date();
            since.setFullYear(since.getFullYear() - 1);
            // Truncate to date-only so all 3 web instances hit the same API cache key
            urlSuffix += `?start_date=${since.toISOString().slice(0, 10)}`;
        }
        if (type === 'vehicles') {
            const since = new Date();
            since.setFullYear(since.getFullYear() - 1);
            // Truncate to date-only so all 3 web instances share one 133s cache entry
            urlSuffix += `?start_date=${since.toISOString().slice(0, 10)}&limit=500`;
        }
        const apiUrl = `${process.env.EXTERNAL_API_URL || 'http://127.0.0.1:6000'}${urlSuffix}`;
        log.debug(`[CACHE] Fetching from: ${apiUrl}`);
        const response = await fetch(apiUrl, { signal: AbortSignal.timeout(30000) });
        
        if (!response.ok) {
            log.error(`[CACHE] API Error Response:`, {
                status: response.status,
                statusText: response.statusText,
                url: apiUrl,
                headers: Object.fromEntries(response.headers.entries())
            });
            
            // Try to get error details from response body
            try {
                const errorText = await response.text();
                log.error(`[CACHE] API Error Body:`, errorText);
            } catch (e) {
                log.error(`[CACHE] Could not read error response body`);
            }
            
            throw new Error(`API returned status ${response.status} for ${type} endpoint`);
        }
        
        const data = await response.json();
        
        // Store the data and update timestamp
        cache.data = data;
        cache.lastUpdated = Date.now();
        
        log.debug(`[CACHE] ${type} cache updated successfully. Data contains:`, {
            players: data.players?.length || 0,
            vehicles: data.vehicles?.length || 0,
            total_players: data.total_players || 0,
            total_vehicles_used: data.total_vehicles_used || 0
        });
        
    } catch (error) {
        log.error(`[CACHE] Error updating ${type} cache:`, error);
        
        // For critical debugging, let's also log what we know about the backend
        if (error.message.includes('500')) {
            log.error(`[CACHE] Backend API is returning 500 errors. Possible causes:`);
            log.error(`[CACHE]    - Database connection issues`);
            log.error(`[CACHE]    - Backend API server errors`);
            log.error(`[CACHE]    - Invalid SQL queries in backend`);
            log.error(`[CACHE]    - Check backend logs for more details`);
        }
        // Keep existing data if update fails
    } finally {
        cache.updating = false;
    }
}

// Function to get cached data or trigger update if needed
async function getCachedData(type) {
    const cache = leaderboardCache[type];
    const now = Date.now();
    const isExpired = now - cache.lastUpdated > CACHE_DURATION;
    
    // If cache is empty or expired, update it
    if (!cache.data || isExpired) {
        if (!cache.updating) {
            // Don't await - let it update in background
            updateCache(type).catch(log.error);
        }
        
        // If we have old data, return it while updating
        if (cache.data) {
            log.debug(`[CACHE] Serving slightly stale ${type} cache while updating...`);
            return cache.data;
        }
        
        // If no data at all, wait for the update. The API fetch itself has a
        // 30s timeout, so wait up to ~32s — longer than the API can take but
        // short enough to surface a real error to the user.
        if (cache.updating) {
            log.debug(`[CACHE] Waiting for initial ${type} cache...`);
            const waitStart = Date.now();
            const waitMs = 32000;
            while (cache.updating && Date.now() - waitStart < waitMs) {
                await new Promise(resolve => setTimeout(resolve, 100));
            }
            if (cache.updating) {
                log.warn(`[CACHE] Timed out waiting for ${type} cache after ${waitMs / 1000}s`);
            }
        }
    }
    
    return cache.data;
}

// Initialize only lightweight caches on the primary worker.
// Player and vehicle leaderboards are too expensive to refresh in the background:
// recent production runs took 140s+ and kept the API CPU saturated after callers timed out.
async function initializeCache() {
    if (!IS_PRIMARY_WORKER) return;

    log.info('[CACHE] Initializing lightweight leaderboard cache (staggered)...');
    await updateCache('stats');
    log.info('[CACHE] Stats cache ready');
    await updateCache('squadrons');
    log.info('[CACHE] Squadrons cache ready');
}

// Auto-refresh only lightweight caches from the primary worker. Heavy caches are
// refreshed lazily by request so background work cannot pin SQLite indefinitely.
if (IS_PRIMARY_WORKER) {
    const cacheTypes = ['stats', 'squadrons'];
    cacheTypes.forEach((type, i) => {
        setInterval(async () => {
            log.debug(`[CACHE] Auto-refreshing ${type} cache...`);
            await updateCache(type).catch(log.error);
        }, CACHE_DURATION + i * 15000);
    });
}

// Log search cache statistics every 10 minutes
setInterval(() => {
    log.info('[SEARCH CACHE] Statistics:', {
        size: searchCache.size(),
        maxSize: 500,
        utilization: `${((searchCache.size() / 500) * 100).toFixed(1)}%`,
        pendingRequests: pendingSearchRequests.size
    });
}, 10 * 60 * 1000); // 10 minutes

const rateLimitMap = new Map();

// Cleanup old rate limit entries every 5 minutes to prevent memory leaks
setInterval(() => {
    const now = Date.now();
    const toDelete = [];
    for (const [ip, data] of rateLimitMap.entries()) {
        if (now > data.resetTime + 300000) { // 5 minutes after reset time
            toDelete.push(ip);
        }
    }
    toDelete.forEach(ip => rateLimitMap.delete(ip));
    if (toDelete.length > 0) {
        log.debug(`[RATE LIMIT] Cleaned up ${toDelete.length} old entries`);
    }
}, 300000); // 5 minutes

const rateLimit = (req, res, next) => {
    const clientIP = req.ip || req.connection.remoteAddress;
    const now = Date.now();
    const windowMs = 60000; // 1 minute
    const maxRequests = 100; // Max requests per window
    
    if (!rateLimitMap.has(clientIP)) {
        rateLimitMap.set(clientIP, { count: 1, resetTime: now + windowMs });
        return next();
    }
    
    const clientData = rateLimitMap.get(clientIP);
    
    if (now > clientData.resetTime) {
        clientData.count = 1;
        clientData.resetTime = now + windowMs;
        return next();
    }
    
    if (clientData.count >= maxRequests) {
        return res.status(429).json({ error: 'Too many requests, please try again later.' });
    }
    
    clientData.count++;
    next();
};

app.use(rateLimit);

app.use((req, res, next) => {
    // Enhanced security headers
    res.setHeader('X-Frame-Options', 'DENY');
    res.setHeader('X-Content-Type-Options', 'nosniff');
    res.setHeader('X-XSS-Protection', '1; mode=block');
    res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
    
    // Remove server signature
    res.removeHeader('X-Powered-By');
    
    // HSTS (only in production with HTTPS)
    if (process.env.NODE_ENV === 'production') {
        res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
    }
    
    // Content Security Policy
    if (req.path.startsWith('/api/')) {
        res.setHeader('Content-Security-Policy', "default-src 'none'; frame-ancestors 'none';");
    } else {
        res.setHeader('Content-Security-Policy', 
            "default-src 'self'; " +
            "script-src 'self' 'unsafe-inline' https://static.cloudflareinsights.com https://cdn.jsdelivr.net; " +
            "style-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com https://fonts.googleapis.com; " +
            "font-src 'self' data: https://cdnjs.cloudflare.com https://fonts.gstatic.com; " +
            "img-src 'self' data: https:; " +
            "media-src 'self' blob:; " +
            "connect-src 'self' https://cloudflareinsights.com; " +
            "frame-ancestors 'none';"
        );
    }
    
    // Additional security headers
    res.setHeader('X-DNS-Prefetch-Control', 'off');
    res.setHeader('X-Download-Options', 'noopen');
    res.setHeader('X-Permitted-Cross-Domain-Policies', 'none');
    
    next();
});

// Enhanced API security with rotation and signing
const API_SECRET = process.env.API_SECRET || 'your-super-secret-key-change-this-in-production';

// Cache for API keys to avoid regenerating on every request
let apiKeyCache = {
    key: null,
    date: null
};

// Generate daily rotating API key with caching
const generateDailyAPIKey = () => {
    const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
    
    // Return cached key if still valid for today
    if (apiKeyCache.key && apiKeyCache.date === today) {
        return apiKeyCache.key;
    }
    
    // Generate new key and cache it
    const newKey = crypto.createHash('sha256').update(`frontend-${API_SECRET}-${today}`).digest('hex').substring(0, 32);
    apiKeyCache = { key: newKey, date: today };
    return newKey;
};

// Generate simple hash signature for request validation (matches client-side)
const signRequest = (data, timestamp) => {
    // Match the client-side simple hashing algorithm
    let hash = 0;
    const combined = `${data}-${timestamp}-${API_SECRET}`;
    for (let i = 0; i < combined.length; i++) {
        const char = combined.charCodeAt(i);
        hash = ((hash << 5) - hash) + char;
        hash = hash & hash; // Convert to 32bit integer
    }
    return Math.abs(hash).toString(16);
};

// Validate request signature to prevent replay attacks
const validateSignature = (data, timestamp, signature) => {
    const expectedSignature = signRequest(data, timestamp);
    const now = Date.now();
    const requestTime = parseInt(timestamp);
    
    // Reject requests older than 5 minutes or from future
    if (now - requestTime > 300000 || requestTime > now + 60000) {
        return false;
    }
    
    // Simple string comparison for now (client-side signing is basic)
    return signature === expectedSignature;
};

// IP-based restrictions for API keys
const validateAPIKeyForIP = (apiKey, clientIP) => {
    // Allow current daily key
    const currentKey = generateDailyAPIKey();
    
    // Allow previous day's key for 1 hour overlap during rotation
    const yesterday = new Date();
    yesterday.setDate(yesterday.getDate() - 1);
    const yesterdayKey = crypto.createHash('sha256')
        .update(`frontend-${API_SECRET}-${yesterday.toISOString().split('T')[0]}`)
        .digest('hex').substring(0, 32);
    
    // Debug logging in development
    if (process.env.NODE_ENV !== 'production') {
        log.debug(`API Key validation:`);
        log.debug(`  Received: ${apiKey?.substring(0, 8)}...`);
        log.debug(`  Expected current: ${currentKey.substring(0, 8)}...`);
        log.debug(`  Expected yesterday: ${yesterdayKey.substring(0, 8)}...`);
    }
    
    if (apiKey === currentKey || apiKey === yesterdayKey) {
        // In production, you could add IP whitelist here
        if (process.env.NODE_ENV === 'production' && process.env.ALLOWED_IPS) {
            const allowedIPs = process.env.ALLOWED_IPS.split(',');
            return allowedIPs.some(ip => {
                // Handle IPv6-mapped IPv4 addresses
                const normalizedClientIP = clientIP.replace('::ffff:', '');
                return normalizedClientIP.includes(ip.trim()) || ip.trim().includes(normalizedClientIP);
            });
        }
        return true;
    }
    
    return false;
};

const FRONTEND_API_KEY = generateDailyAPIKey();

// Only log API key in development
if (process.env.NODE_ENV !== 'production') {
    log.info(`Frontend API Key: ${FRONTEND_API_KEY}`);
    log.info(`Key rotates daily at midnight UTC`);
}

const apiSecurityCheck = (req, res, next) => {
    // Only apply to API routes
    if (!req.path.startsWith('/api/')) {
        return next();
    }

    // Special case: allow /api-key and /api/stats without authentication.
    // Some proxies/routers may preserve a trailing slash or otherwise normalize
    // the path, so match by prefix after trimming any trailing slash.
    const normalizedPath = req.path.replace(/\/+$/, '');
    if (normalizedPath === '/api-key' || normalizedPath.startsWith('/api/stats')) {
        return next();
    }

    // Allow video/minimap/icon endpoints without auth (loaded via <img>/<video>/<canvas> tag, not JS client)
    if (req.path.match(/^\/api\/match\/[0-9a-fA-F]+\/(video|replay-canvas)$/) ||
        req.path.match(/^\/api\/match\/minimap\/[a-zA-Z0-9_]+$/) ||
        req.path.match(/^\/api\/icons\/type\/[a-zA-Z0-9_\-]+$/)) {
        return next();
    }
    
    const apiKey = req.get('X-API-Key');
    const signature = req.get('X-Request-Signature');
    const timestamp = req.get('X-Request-Timestamp');
    const clientIP = req.ip || req.connection.remoteAddress || req.socket.remoteAddress;
    
    // Enhanced validation with IP checking
    if (!validateAPIKeyForIP(apiKey, clientIP)) {
        log.warn(`[SECURITY] Invalid API key: ${req.path} from ${clientIP}`);
        log.warn(`  Expected daily key, got: ${apiKey ? apiKey.substring(0, 8) + '...' : 'none'}`);
        return res.status(403).json({
            error: 'Access denied',
            message: 'Insufficient permissions'
        });
    }
    
    // Note: Signature validation disabled for now since we can't safely do HMAC client-side
    // The API key rotation and IP validation provide sufficient security
    
    // Optional: Add timestamp validation to prevent very old requests
    if (timestamp) {
        const now = Date.now();
        const requestTime = parseInt(timestamp);
        if (now - requestTime > 300000) { // 5 minutes
            log.warn(`[SECURITY] Stale request: ${req.path} from ${clientIP} (${Math.floor((now - requestTime) / 1000)}s old)`);
            return res.status(403).json({
                error: 'Access denied',
                message: 'Request expired'
            });
        }
    }
    
    next();
};

app.use(apiSecurityCheck);

app.use((err, req, res, next) => {
    if (err.message === 'Not allowed by CORS policy' || err.message === 'API access restricted to authorized domains only') {
        return res.status(403).json({
            error: 'Access denied',
            message: 'Resource not available'
        });
    }
    next(err);
});

app.set('view engine', 'ejs');
app.set('views', './views');

const validateInput = (input, type = 'string', maxLength = 100) => {
    if (!input) return false;
    if (typeof input !== type) return false;
    if (type === 'string' && input.length > maxLength) return false;
    return true;
};

// ── i18n middleware ──
app.use((req, res, next) => {
    const lang = getLang(req);
    if (req.query.lang && locales[req.query.lang]) {
        res.cookie('lang', lang, { maxAge: 365 * 24 * 60 * 60 * 1000, path: '/', sameSite: 'Lax' });
    }
    res.locals.lang = lang;
    // Second param is an optional params object: t('key.name', { cap: 10 })
    // substitutes `{cap}` placeholders inside the translated string.
    res.locals.t = (key, params) => {
        let val = t(lang, key);
        if (params && typeof val === 'string') {
            for (const k of Object.keys(params)) {
                val = val.replace(new RegExp('\\{' + k + '\\}', 'g'), String(params[k]));
            }
        }
        return val;
    };
    // Only serialize locale JSON for page renders, not API requests
    if (!req.path.startsWith('/api/') && !req.path.startsWith('/api-key')) {
        res.locals.localeJson = JSON.stringify(mergeLocale(locales.en, locales[lang]));
    }
    next();
});

// ── Supporters (paying squadrons) ────────────────────────────────────────────
let _supportersCache = null;
let _supportersCacheAt = 0;
const SUPPORTERS_TTL = 15 * 60 * 1000;

function getSupporters() {
    if (_supportersCache && Date.now() - _supportersCacheAt < SUPPORTERS_TTL) {
        return Promise.resolve(_supportersCache);
    }
    return new Promise((resolve) => {
        let squads;
        try {
            squads = JSON.parse(fs.readFileSync(SQUADRONS_JSON_PATH, 'utf8'));
        } catch {
            return resolve([]);
        }

        entitlementsDb.all(
            `SELECT guild_id FROM guild_entitlements WHERE status='active'
             UNION SELECT guild_id FROM discord_entitlements
             UNION SELECT guild_id FROM manual_entitlements WHERE expires_at > strftime('%s','now')`,
            [],
            (err, rows) => {
                if (err) return resolve([]);
                const paying = new Set(rows.map(r => r.guild_id));
                const seen = new Map();
                for (const [gid, sq] of Object.entries(squads)) {
                    if (paying.has(gid) && !seen.has(sq.SQ_ShortHand_Name)) {
                        seen.set(sq.SQ_ShortHand_Name, sq.SQ_LongHandName);
                    }
                }
                const result = [...seen.entries()]
                    .map(([short, long]) => ({ short, long }))
                    .sort((a, b) => a.long.localeCompare(b.long));
                _supportersCache = result;
                _supportersCacheAt = Date.now();
                resolve(result);
            }
        );
    });
}

// Routes
app.get('/', async (req, res) => {
    const siteUrl = (process.env.PRODUCTION_DOMAIN || 'https://sre.pawjob.us').replace(/\/$/, '');
    const supporters = await getSupporters().catch(() => []);

    res.render('index', {
        botName: 'Toothless SQB Bot',
        supporters,
        metaTitle: "Toothless' SRE Bot",
        metaDescription: 'The Best Squadron Battles Bot.',
        metaUrl: `${siteUrl}/`,
        metaThemeColor: '#90EE90',
        serverCount: '1,234',
        userCount: '50,000+',
        commandCount: '25+'
    });
});

app.get('/terms', (req, res) => {
    res.render('terms', {
        botName: 'Toothless SQB Bot'
    });
});

app.get('/docs', (req, res) => {
    res.render('docs', {
        botName: 'Toothless SQB Bot'
    });
});

app.get('/timeline', (req, res) => {
    let serverCount = 0;
    try {
        const report = fs.readFileSync(path.join(STORAGE_ROOT, 'SREBOT_GUILDS.txt'), 'utf8').trim();
        serverCount = report ? report.split('\n').length : 0;
    } catch { /* file may not exist yet */ }
    res.render('timeline', {
        botName: 'Toothless SQB Bot',
        serverCount
    });
});

app.get('/premium', (req, res) => {
    const stdCheckout = process.env.WHOP_PLAN_ID_STANDARD || null;
    const proCheckout = process.env.WHOP_PLAN_ID_PRO || null;
    const maxCheckout = process.env.WHOP_PLAN_ID_MAX || null;

    res.render('premium', {
        botName: 'Toothless SQB Bot',
        whopPlanId: stdCheckout,  // back-compat for any template still reading this
        plans: {
            standard: {
                whopPlanId: stdCheckout,
                price: '2.99',
                squadCap: 10,
            },
            pro: {
                whopPlanId: proCheckout,
                price: process.env.WHOP_PLAN_PRICE_PRO || null,
                squadCap: 25,
            },
            max: {
                whopPlanId: maxCheckout,
                price: process.env.WHOP_PLAN_PRICE_MAX || null,
                squadCap: null,
            },
        },
        success: req.query.success === 'true'
    });
});

// Highest-rank tier across the 3 entitlement sources — mirrors the bot's resolver.
function pickHigherTier(a, b) {
    const order = { standard: 0, pro: 1, max: 2 };
    if (!a) return b || null;
    if (!b) return a;
    return order[b] > order[a] ? b : a;
}

app.get('/api/check-premium/:guildId', (req, res) => {
    const { guildId } = req.params;
    if (!/^\d{17,19}$/.test(guildId)) return res.json({ premium: false, tier: null });
    const nowSec = Math.floor(Date.now() / 1000);
    entitlementsDb.get(
        "SELECT tier FROM guild_entitlements WHERE guild_id=? AND status='active'",
        [guildId],
        (err, row) => {
            if (err) return res.json({ premium: false, tier: null });
            let tier = row ? (row.tier || 'standard') : null;
            entitlementsDb.get(
                "SELECT tier FROM manual_entitlements WHERE guild_id=? AND expires_at > ?",
                [guildId, nowSec],
                (err2, manualRow) => {
                    if (err2) return res.json({ premium: !!tier, tier });
                    if (manualRow) tier = pickHigherTier(tier, manualRow.tier || 'standard');
                    entitlementsDb.get(
                        "SELECT tier FROM discord_entitlements WHERE guild_id=?",
                        [guildId],
                        (err3, discordRow) => {
                            if (err3) return res.json({ premium: !!tier, tier });
                            if (discordRow) tier = pickHigherTier(tier, discordRow.tier || 'standard');
                            res.json({ premium: !!tier, tier });
                        }
                    );
                }
            );
        }
    );
});

const RECAP_THEMES = new Set(['light', 'dark']);
const RECAP_DEFAULT_THEME = 'dark';
const RECAP_LANGS = new Set(['cs', 'de', 'en', 'es', 'fr', 'it', 'pl', 'ru', 'uk', 'zh-CN']);
const RECAP_DEFAULT_LANG = 'en';

function spawnRender({ mode, clanId, uid, season, seasonStart, seasonEnd,
                       weekBoundaries, outPath, theme, lang }) {
    return new Promise((resolve, reject) => {
        const args = [
            RECAP_SCRIPT,
            '--mode', mode,
            '--season', season,
            '--season-start', String(seasonStart),
            '--season-end', String(seasonEnd),
            '--week-boundaries', weekBoundaries.join(','),
            '--theme', theme,
            '--lang', lang,
            '--out', outPath,
        ];
        if (mode === 'squadron') args.push('--clan-id', String(clanId));
        if (mode === 'player')   args.push('--uid', String(uid));
        const child = execFile(PYTHON_BIN, args, {
            cwd: REPO_ROOT,
            timeout: RECAP_RENDER_TIMEOUT_MS,
            maxBuffer: 2 * 1024 * 1024,
        }, (err, stdout, stderr) => {
            if (err) {
                err.stderr = stderr;
                err.stdout = stdout;
                return reject(err);
            }
            resolve({ stdout, stderr });
        });
    });
}

const SEASON_NAME_RE = /^\d{4}-(I{1,3}|IV|VI{0,3}|IX|X)$/;

app.get('/squadron/:clan_id/recap/:season.png', async (req, res) => {
    const t0 = Date.now();
    const clanId = parseInt(req.params.clan_id, 10);
    const season = req.params.season;
    if (!Number.isInteger(clanId) || clanId <= 0) {
        return res.status(400).json({ error: 'invalid clan_id' });
    }
    if (!SEASON_NAME_RE.test(season)) {
        return res.status(400).json({ error: 'invalid season format' });
    }
    const range = seasonsUtil.getSeasonRange(season);
    if (!range) {
        return res.status(400).json({ error: 'unknown season' });
    }
    const themeParam = typeof req.query.theme === 'string' ? req.query.theme : '';
    const theme = RECAP_THEMES.has(themeParam) ? themeParam : RECAP_DEFAULT_THEME;
    // getLang() checks ?lang= then the lang cookie then falls back to 'en'.
    const resolvedLang = getLang(req);
    const lang = RECAP_LANGS.has(resolvedLang) ? resolvedLang : RECAP_DEFAULT_LANG;

    const cacheDir  = path.join(SQUADRON_RECAP_CACHE_DIR, season);
    const cachePath = path.join(cacheDir, `${clanId}-${theme}-${lang}.png`);

    let serveFromCache = false;
    try {
        const stat = await fsp.stat(cachePath);
        if (range.status === 'completed') {
            // Only trust a completed-season cache if it was rendered after the
            // season ended; mid-season files are stale snapshots (range.end is
            // epoch seconds, mtimeMs is ms).
            serveFromCache = stat.mtimeMs > range.end * 1000;
        } else if (Date.now() - stat.mtimeMs < RECAP_TTL_MS) {
            serveFromCache = true;
        }
    } catch (_) { /* cache miss */ }

    if (!serveFromCache) {
        try {
            await fsp.mkdir(cacheDir, { recursive: true });
            const weeks = seasonsUtil.getWeekBoundaries(season);
            await spawnRender({
                mode: 'squadron',
                clanId, season,
                seasonStart: range.start,
                seasonEnd: range.end,
                weekBoundaries: weeks,
                outPath: cachePath,
                theme,
                lang,
            });
        } catch (err) {
            console.error(`(RECAP) render failed clan_id=${clanId} season=${season} theme=${theme} lang=${lang}:`,
                          err.message, '\nstderr:', err.stderr);
            // Best-effort cleanup of any orphan .tmp from a crashed subprocess.
            fsp.unlink(cachePath + '.tmp').catch(() => {});
            return res.status(err.killed ? 504 : 500).json({ error: 'render failed' });
        }
    }

    const download = req.query.download === '1';
    if (download) {
        res.setHeader('Content-Disposition',
                      `attachment; filename="${clanId}-${season}-${theme}-${lang}.png"`);
    }
    res.setHeader('Content-Type', 'image/png');
    res.setHeader('Cache-Control', range.status === 'completed' ? 'public, max-age=31536000' : 'public, max-age=86400');
    res.sendFile(cachePath, (err) => {
        if (err) console.error('(RECAP) sendFile error:', err);
        console.log(`(RECAP) clan_id=${clanId} season=${season} theme=${theme} lang=${lang} result=${serveFromCache ? 'cache' : 'fresh'} ms=${Date.now() - t0}`);
    });
});

app.get('/players/:uid/recap/:season.png', async (req, res) => {
    const t0 = Date.now();
    const uid = req.params.uid;
    const season = req.params.season;
    if (!/^\d+$/.test(uid)) {
        return res.status(400).json({ error: 'invalid uid' });
    }
    if (!SEASON_NAME_RE.test(season)) {
        return res.status(400).json({ error: 'invalid season format' });
    }
    const range = seasonsUtil.getSeasonRange(season);
    if (!range) {
        return res.status(400).json({ error: 'unknown season' });
    }
    const themeParam = typeof req.query.theme === 'string' ? req.query.theme : '';
    const theme = RECAP_THEMES.has(themeParam) ? themeParam : RECAP_DEFAULT_THEME;
    const resolvedLang = getLang(req);
    const lang = RECAP_LANGS.has(resolvedLang) ? resolvedLang : RECAP_DEFAULT_LANG;

    const cacheDir  = path.join(PLAYER_RECAP_CACHE_DIR, season);
    const cachePath = path.join(cacheDir, `${uid}-${theme}-${lang}.png`);

    let serveFromCache = false;
    try {
        const stat = await fsp.stat(cachePath);
        if (range.status === 'completed') {
            // Only trust a completed-season cache if it was rendered after the
            // season ended; mid-season files are stale snapshots (range.end is
            // epoch seconds, mtimeMs is ms).
            serveFromCache = stat.mtimeMs > range.end * 1000;
        } else if (Date.now() - stat.mtimeMs < RECAP_TTL_MS) {
            serveFromCache = true;
        }
    } catch (_) { /* cache miss */ }

    if (!serveFromCache) {
        try {
            await fsp.mkdir(cacheDir, { recursive: true });
            const weeks = seasonsUtil.getWeekBoundaries(season);
            await spawnRender({
                mode: 'player',
                uid, season,
                seasonStart: range.start,
                seasonEnd: range.end,
                weekBoundaries: weeks,
                outPath: cachePath,
                theme,
                lang,
            });
        } catch (err) {
            console.error(`(RECAP) player render failed uid=${uid} season=${season} theme=${theme} lang=${lang}:`,
                          err.message, '\nstderr:', err.stderr);
            fsp.unlink(cachePath + '.tmp').catch(() => {});
            return res.status(err.killed ? 504 : 500).json({ error: 'render failed' });
        }
    }

    const download = req.query.download === '1';
    if (download) {
        res.setHeader('Content-Disposition',
                      `attachment; filename="player-${uid}-${season}-${theme}-${lang}.png"`);
    }
    res.setHeader('Content-Type', 'image/png');
    res.setHeader('Cache-Control', range.status === 'completed' ? 'public, max-age=31536000' : 'public, max-age=86400');
    res.sendFile(cachePath, (err) => {
        if (err) console.error('(RECAP) player sendFile error:', err);
        console.log(`(RECAP) player uid=${uid} season=${season} theme=${theme} lang=${lang} result=${serveFromCache ? 'cache' : 'fresh'} ms=${Date.now() - t0}`);
    });
});

app.get('/api/seasons', (req, res) => {
    try {
        res.json(seasonsUtil.getSeasonDetails());
    } catch (err) {
        console.error('(RECAP) /api/seasons error:', err);
        res.status(500).json({ error: 'seasons parse failed' });
    }
});

// Add missing support route
app.get('/support', (req, res) => {
    const supportUrl = 'https://discord.gg/BCvkK8JhPe';
    res.redirect(supportUrl);
});

// Leaderboard routes
app.get('/leaderboard/players', (req, res) => {
    res.render('leaderboard-players', {
        botName: 'Toothless SQB Bot'
    });
});

app.get('/leaderboard/vehicles', (req, res) => {
    res.render('leaderboard-vehicles', {
        botName: 'Toothless SQB Bot'
    });
});

app.get('/leaderboard/stats', (req, res) => {
    res.render('leaderboard-stats', {
        botName: 'Toothless SQB Bot'
    });
});

app.get('/leaderboard/comparison', (req, res) => {
    res.render('leaderboard-comparison', {
        botName: 'Toothless SQB Bot'
    });
});

app.get('/leaderboard/squadrons', (req, res) => {
    res.render('leaderboard-squadrons', {
        botName: 'Toothless SQB Bot'
    });
});

// Redirect /leaderboard to players leaderboard by default
app.get('/leaderboard', (req, res) => {
    res.redirect('/leaderboard/players');
});

app.get('/analytics', (req, res) => {
    res.render('analytics', {
        botName: 'Toothless SQB Bot'
    });
});

// Squadron routes
app.get('/squadrons', (req, res) => {
    res.render('squadrons', {
        botName: 'Toothless SQB Bot'
    });
});

app.get('/squadrons/:squadronname', async (req, res) => {
    const squadronName = req.params.squadronname;

    if (!squadronName) {
        return res.status(400).render('404', {
            statusCode: 400,
            error: 'Invalid squadron name.',
            botName: 'Toothless SQB Bot'
        });
    }

    const apiBase = process.env.EXTERNAL_API_URL || 'http://127.0.0.1:6000';

    // Canonical URLs are /squadrons/<clan_id> (numeric). When the param is
    // non-numeric we treat it as a (possibly historical) squadron name and
    // 301-redirect to the canonical clan_id URL. The API's resolve endpoint
    // with `name=` is alias-aware (squadron_name_aliases + squadron_name_history
    // + player_games_hist), so renamed squadrons resolve to their current id.
    if (!/^\d+$/.test(squadronName)) {
        try {
            const lookupUrl = `${apiBase}/api/squadrons/resolve?name=${encodeURIComponent(squadronName)}`;
            const lookupResp = await fetch(lookupUrl);
            if (lookupResp.ok) {
                const { results } = await lookupResp.json();
                const hit = Array.isArray(results) ? results.find(r => r && r.resolved && r.clan_id != null) : null;
                if (hit) {
                    const qs = req.url.includes('?') ? req.url.slice(req.url.indexOf('?')) : '';
                    return res.redirect(301, `/squadrons/${hit.clan_id}${qs}`);
                }
            }
        } catch (err) {
            log.error('[Squadron Route] clan_id lookup failed', err);
        }
        return res.status(404).render('404', {
            error: `Squadron "${squadronName}" not found.`,
            botName: 'Toothless SQB Bot'
        });
    }

    try {
        // Build query parameters for date filtering
        const queryParams = new URLSearchParams();
        if (req.query.start_date) queryParams.append('start_date', req.query.start_date);
        if (req.query.end_date) queryParams.append('end_date', req.query.end_date);

        const cacheKey = `${squadronName}:${queryParams.toString()}`;
        const cachedSquadronData = squadronProfileCache.get(cacheKey);
        if (cachedSquadronData) {
            log.debug(`[Squadron Cache] HIT for ${cacheKey}`);
            return res.render('squadron-profile', {
                botName: 'Toothless SQB Bot',
                squadronData: cachedSquadronData
            });
        }

        log.debug(`[Squadron Cache] MISS for ${cacheKey}`);

        if (pendingSquadronRequests.has(cacheKey)) {
            log.debug(`[Squadron Cache] Waiting for in-flight request: ${cacheKey}`);
            const inFlightData = await pendingSquadronRequests.get(cacheKey);
            return res.render('squadron-profile', {
                botName: 'Toothless SQB Bot',
                squadronData: inFlightData
            });
        }

        // Fetch squadron data from the API with query parameters
        const apiUrl = `${apiBase}/api/squadrons/${encodeURIComponent(squadronName)}${queryParams.toString() ? '?' + queryParams.toString() : ''}`;

        const requestPromise = (async () => {
            // Log filter parameters for debugging
            if (queryParams.toString()) {
                log.debug(`[Squadron Route] Fetching squadron ${squadronName} with filters: ${queryParams.toString()}`);
            } else {
                log.debug(`[Squadron Route] Fetching squadron ${squadronName} without filters`);
            }

            const controller = new AbortController();
            const timeoutId = setTimeout(() => controller.abort(), SQUADRON_API_TIMEOUT_MS);

            try {
                const response = await fetch(apiUrl, { signal: controller.signal });

                if (!response.ok) {
                    if (response.status === 404) {
                        const err = new Error(`Squadron "${squadronName}" not found.`);
                        err.statusCode = 404;
                        throw err;
                    }
                    throw new Error(`API returned status ${response.status}`);
                }

                const squadronData = await response.json();
                squadronProfileCache.set(cacheKey, squadronData);
                return squadronData;
            } finally {
                clearTimeout(timeoutId);
                pendingSquadronRequests.delete(cacheKey);
            }
        })();

        pendingSquadronRequests.set(cacheKey, requestPromise);

        const squadronData = await requestPromise;
        
        res.render('squadron-profile', {
            botName: 'Toothless SQB Bot',
            squadronData: squadronData
        });
        
    } catch (error) {
        log.error('Error fetching squadron data:', error);

        let errorMessage = 'Unable to fetch squadron data. Please try again later.';
        let statusCode = 503;

        if (error.statusCode === 404) {
            errorMessage = error.message;
            statusCode = 404;
        }

        if (error.name === 'AbortError') {
            errorMessage = 'Squadron data request timed out. This usually means the backend API is processing a large filtered query. Try without filters or try again in a moment.';
            statusCode = 504;
            log.error(`[Squadron Route] Request timed out for ${squadronName} with params: ${req.query.start_date ? 'filtered' : 'unfiltered'}`);
        } else if (error.message.includes('ECONNREFUSED')) {
            errorMessage = 'Backend API is not running. Please ensure the API server on port 6000 is started.';
        }

        res.status(statusCode).render('404', {
            statusCode,
            error: errorMessage,
            botName: 'Toothless SQB Bot'
        });
    }
});

// Player profile route - must come before the 404 handler
app.get('/players/:uid', async (req, res) => {
    const uid = req.params.uid;
    
    // Validate UID format (should be numeric)
    if (!uid || !/^\d+$/.test(uid)) {
        return res.status(400).render('404', {
            statusCode: 400,
            error: 'Invalid player UID format. UID must be numeric.',
            botName: 'Toothless SQB Bot'
        });
    }

    try {
        // Build query parameters for date filtering
        const queryParams = new URLSearchParams();
        if (req.query.start_date) queryParams.append('start_date', req.query.start_date);
        if (req.query.end_date) queryParams.append('end_date', req.query.end_date);
        
        // Fetch player data from the API with query parameters
        const apiUrl = `${process.env.EXTERNAL_API_URL || 'http://127.0.0.1:6000'}/api/player/${uid}${queryParams.toString() ? '?' + queryParams.toString() : ''}`;
        
        // Log filter parameters for debugging
        if (queryParams.toString()) {
            log.debug(`[Player Route] Fetching player ${uid} with filters: ${queryParams.toString()}`);
        }

        const response = await fetch(apiUrl, { signal: AbortSignal.timeout(30000) });
        
        if (!response.ok) {
            if (response.status === 404) {
                const rosterMember = await getRosterMemberByUid(uid);
                if (rosterMember) {
                    const playerData = {
                        uid,
                        nick: rosterMember.nick || uid,
                        squadron_name: rosterMember.tag_name || rosterMember.short_name || '',
                        squadron_long_name: rosterMember.long_name || rosterMember.tag_name || rosterMember.short_name || '',
                        squadron_clan_id: rosterMember.clan_id != null ? Number(rosterMember.clan_id) : null,
                        performance: 0,
                        vehicles: [],
                        total_vehicles: 0,
                        no_stats_yet: true
                    };

                    const totals = {
                        totalBattles: 0,
                        totalWins: 0,
                        totalGroundKills: 0,
                        totalAirKills: 0,
                        totalKills: 0,
                        totalAssists: 0,
                        totalCaptures: 0,
                        totalDeaths: 0,
                        overallKDR: 0,
                        overallWinRate: '0.0%'
                    };

                    return res.render('player', {
                        botName: 'Toothless SQB Bot',
                        playerData,
                        totals
                    });
                }

                return res.status(404).render('404', {
                    error: `Player with UID ${uid} not found.`,
                    botName: 'Toothless SQB Bot'
                });
            }
            throw new Error(`API returned status ${response.status}`);
        }
        
        const playerData = await response.json();
        
        // Squadron data is now included in the player API response
        playerData.squadron_long_name = playerData.squadron_long_name || playerData.squadron_name;
        playerData.squadron_tag_name = playerData.squadron_name;
        
        // Calculate totals for summary display
        const totals = {
            totalBattles: 0,
            totalWins: 0,
            totalGroundKills: 0,
            totalAirKills: 0,
            totalKills: 0,
            totalAssists: 0,
            totalCaptures: 0,
            totalDeaths: 0,
            overallKDR: 0,
            overallWinRate: '0.0%'
        };
        
        if (playerData.vehicles && playerData.vehicles.length > 0) {
            playerData.vehicles.forEach(vehicle => {
                totals.totalBattles += vehicle.stats.total_battles || 0;
                totals.totalWins += vehicle.stats.wins || 0;
                totals.totalGroundKills += vehicle.stats.ground_kills || 0;
                totals.totalAirKills += vehicle.stats.air_kills || 0;
                totals.totalAssists += vehicle.stats.assists || 0;
                totals.totalCaptures += vehicle.stats.captures || 0;
                totals.totalDeaths += vehicle.stats.deaths || 0;
            });
            
            // Calculate total kills, overall KDR, and overall win rate
            totals.totalKills = totals.totalGroundKills + totals.totalAirKills;
            totals.overallKDR = totals.totalDeaths > 0 ? 
                (totals.totalKills / totals.totalDeaths).toFixed(2) : 
                (totals.totalKills > 0 ? totals.totalKills.toFixed(2) : '0.00');
            totals.overallWinRate = totals.totalBattles > 0 ? 
                ((totals.totalWins / totals.totalBattles) * 100).toFixed(1) + '%' : '0.0%';
        }
        
        // Debug logging
        log.debug('Player data totals:', totals);
        
        res.render('player', {
            botName: 'Toothless SQB Bot',
            playerData: playerData,
            totals: totals
        });
        
    } catch (error) {
        log.error('Error fetching player data:', error);

        let errorMessage = 'Unable to fetch player data. Please try again later.';
        let statusCode = 503;

        if (error.name === 'AbortError') {
            errorMessage = 'Player data request timed out. The backend API is taking too long to respond — try again in a moment.';
            statusCode = 504;
        } else if (error.message && error.message.includes('ECONNREFUSED')) {
            errorMessage = 'Backend API is not running. Please ensure the API server on port 6000 is started.';
        }

        res.status(statusCode).render('404', {
            statusCode,
            error: errorMessage,
            botName: 'Toothless SQB Bot'
        });
    }
});

// Squadron profile route - must come before the 404 handler

// Player data API proxy - returns player profile and vehicle stats
app.get('/api/player/:uid', cors(apiCorsOptions), async (req, res) => {
    const uid = req.params.uid;
    
    // Validate UID format (should be numeric)
    if (!uid || !/^\d+$/.test(uid)) {
        return res.status(400).json({
            error: 'Invalid UID format',
            message: 'UID must be numeric',
            uid: uid
        });
    }
    
    try {
        // Fetch player data from the external API, forwarding any date filter params
        const backendParams = new URLSearchParams();
        if (req.query.start_date) backendParams.append('start_date', req.query.start_date);
        if (req.query.end_date) backendParams.append('end_date', req.query.end_date);
        const apiUrl = `${process.env.EXTERNAL_API_URL || 'http://127.0.0.1:6000'}/api/player/${uid}${backendParams.toString() ? '?' + backendParams.toString() : ''}`;
        log.debug(`Attempting to fetch player data from: ${apiUrl}`);

        const response = await fetch(apiUrl, { signal: AbortSignal.timeout(30000) });
        log.debug(`Player API response status: ${response.status}`);
        
        if (!response.ok) {
            if (response.status === 404) {
                return res.status(404).json({
                    error: 'Player not found',
                    message: 'No player found with the provided UID',
                    uid: uid
                });
            }
            
            const errorText = await response.text();
            log.error(`Player API error response: ${errorText}`);
            
            return res.status(response.status).json({
                error: 'External API error',
                message: 'Failed to fetch player data from external API',
                details: errorText,
                uid: uid
            });
        }
        
        const playerData = await response.json();
        log.debug(`Successfully fetched data for player ${uid} with ${playerData.vehicles?.length || 0} vehicles`);
        
        res.json(playerData);
        
    } catch (error) {
        log.error(`Error fetching data for player ${uid}:`, error);
        if (error && error.name === 'AbortError') {
            return res.status(504).json({
                error: 'Upstream timeout',
                message: 'Player data request timed out',
                uid: uid
            });
        }
        if (error && (error.code === 'ECONNREFUSED' || (error.message || '').includes('fetch'))) {
            return res.status(503).json({
                error: 'Upstream unavailable',
                message: 'Unable to connect to the stats API',
                uid: uid
            });
        }
        res.status(500).json({
            error: 'Internal server error',
            message: 'An error occurred while fetching player data',
            uid: uid
        });
    }
});

// Player games API proxy - returns individual game records
app.get('/api/player/:uid/games', cors(apiCorsOptions), async (req, res) => {
    const uid = req.params.uid;
    
    // Validate UID format (should be numeric)
    if (!uid || !/^\d+$/.test(uid)) {
        return res.status(400).json({
            error: 'Invalid player UID format. UID must be numeric.',
            uid: uid,
            nick: '',
            games: [],
            total_games_returned: 0
        });
    }
    
    try {
        // Fetch games data from the external API, forwarding any active date filters
        const backendParams = new URLSearchParams();
        if (req.query.start_date) backendParams.append('start_date', req.query.start_date);
        if (req.query.end_date) backendParams.append('end_date', req.query.end_date);
        const apiUrl = `${process.env.EXTERNAL_API_URL || 'http://127.0.0.1:6000'}/api/player/${uid}/games${backendParams.toString() ? '?' + backendParams.toString() : ''}`;
        log.debug(`Attempting to fetch games from: ${apiUrl}`);

        const response = await fetch(apiUrl, { signal: AbortSignal.timeout(30000) });
        log.debug(`Games API response status: ${response.status}`);
        
        if (!response.ok) {
            if (response.status === 404) {
                log.debug('Games API returned 404, no games found');
                return res.status(404).json({
                    error: `No games found for player with UID ${uid}`,
                    uid: uid,
                    nick: '',
                    games: [],
                    total_games_returned: 0
                });
            }
            throw new Error(`Games API returned status ${response.status}`);
        }
        
        const gamesData = await response.json();
        log.debug('Games data received:', gamesData);
        res.json(gamesData);
        
    } catch (error) {
        log.error('Error in games proxy:', error);

        if (error && error.name === 'AbortError') {
            return res.status(504).json({
                error: 'Player games request timed out. The backend API took too long to respond.',
                uid: uid,
                nick: '',
                games: [],
                total_games_returned: 0
            });
        }

        // Check if it's a connection error
        if (error.code === 'ECONNREFUSED' || (error.message || '').includes('fetch')) {
            return res.status(503).json({
                error: 'Unable to connect to the stats API. Please ensure the API server is running on port 6000.',
                uid: uid,
                nick: '',
                games: [],
                total_games_returned: 0
            });
        }

        res.status(500).json({
            error: 'Failed to fetch player games data',
            uid: uid,
            nick: '',
            games: [],
            total_games_returned: 0
        });
    }
});

app.get('/api/player/:uid/history', cors(apiCorsOptions), async (req, res) => {
    try {
        const apiUrl = `${process.env.EXTERNAL_API_URL || 'http://127.0.0.1:6000'}/api/player/${req.params.uid}/history`;
        const response = await fetch(apiUrl, { signal: AbortSignal.timeout(30000) });
        if (!response.ok) return res.status(response.status).json({ error: 'API error' });
        res.json(await response.json());
    } catch (err) {
        if (err && err.name === 'AbortError') {
            return res.status(504).json({ error: 'Player history request timed out' });
        }
        res.status(500).json({ error: 'Failed to fetch player history' });
    }
});

// Player search API proxy with caching and request deduplication
app.get('/api/search/:nickname', cors(apiCorsOptions), async (req, res) => {
    const nickname = req.params.nickname;
    
    // Validate nickname
    if (!nickname || nickname.length < 2) {
        return res.status(400).json({ 
            error: 'Nickname must be at least 2 characters long',
            search_term: nickname,
            results: [],
            total_found: 0,
            limited_to: 50
        });
    }
    
    // Normalize search term for cache key (lowercase, trimmed)
    const cacheKey = nickname.toLowerCase().trim();
    
    try {
        // Check cache first
        const cachedResult = searchCache.get(cacheKey);
        if (cachedResult) {
            log.debug(`[SEARCH CACHE] Cache HIT for: ${nickname}`);
            return res.json(cachedResult);
        }
        
        log.debug(`[SEARCH CACHE] Cache MISS for: ${nickname}`);
        
        // Check if there's already a pending request for this search
        if (pendingSearchRequests.has(cacheKey)) {
            log.debug(`[SEARCH DEDUP] Waiting for existing request: ${nickname}`);
            // Wait for the existing request to complete
            const result = await pendingSearchRequests.get(cacheKey);
            return res.json(result);
        }
        
        // Create a new request promise
        const requestPromise = (async () => {
            try {
                // Fetch from the external API
                const apiUrl = `${process.env.EXTERNAL_API_URL || 'http://127.0.0.1:6000'}/api/search/${encodeURIComponent(nickname)}`;
                log.debug(`[SEARCH] Fetching from: ${apiUrl}`);
                
                const response = await fetch(apiUrl);
                log.debug(`[SEARCH] API response status: ${response.status}`);
                
                if (!response.ok) {
                    if (response.status === 404) {
                        log.debug('[SEARCH] API returned 404, no players found');
                        const emptyResult = {
                            search_term: nickname,
                            results: [],
                            total_found: 0,
                            limited_to: 50
                        };
                        // Cache empty results too (prevents repeated failed lookups)
                        searchCache.set(cacheKey, emptyResult);
                        return emptyResult;
                    }
                    throw new Error(`API returned status ${response.status}`);
                }
                
                const searchData = await response.json();
                log.debug(`[SEARCH] Found ${searchData.results?.length || 0} results for: ${nickname}`);
                
                // Cache the successful result
                searchCache.set(cacheKey, searchData);
                
                return searchData;
            } finally {
                // Remove from pending requests
                pendingSearchRequests.delete(cacheKey);
            }
        })();
        
        // Store the pending request
        pendingSearchRequests.set(cacheKey, requestPromise);
        
        // Wait for and return the result
        const searchData = await requestPromise;
        res.json(searchData);
        
    } catch (error) {
        log.error('[SEARCH] Error in search proxy:', error);
        
        // Clean up pending request on error
        pendingSearchRequests.delete(cacheKey);
        
        // Check if it's a connection error
        if (error.code === 'ECONNREFUSED' || error.message.includes('fetch')) {
            return res.status(503).json({ 
                error: 'Unable to connect to the stats API. Please ensure the API server is running on port 6000.',
                search_term: nickname,
                results: [],
                total_found: 0,
                limited_to: 50
            });
        }
        
        res.status(500).json({ 
            error: 'Failed to search players',
            search_term: nickname,
            results: [],
            total_found: 0,
            limited_to: 50
        });
    }
});


// Vehicle icons API - returns mapping of vehicle names to icon paths
app.get('/api/vehicle-icons', cors(apiCorsOptions), async (req, res) => {
    try {
        const fs = require('fs').promises;
        const path = require('path');
        
        // Scan the ICONS/VEHICLES directory
        const iconsDir = path.join(__dirname, 'ICONS', 'VEHICLES');
        
        try {
            const files = await fs.readdir(iconsDir);
            const vehicleIcons = {};
            
            // Create mappings for each icon file
            files.forEach(file => {
                if (file.endsWith('.png')) {
                    const iconName = file.replace('.png', '');
                    
                    // Create multiple possible lookup keys for this icon
                    const keys = [
                        iconName,                                    // Original name: germ_begleitpanzer_57
                        iconName.replace(/_/g, ' '),               // Underscores to spaces: germ begleitpanzer 57
                        iconName.replace(/_/g, '-'),               // Underscores to hyphens: germ-begleitpanzer-57
                        iconName.replace(/^[a-z]+_/, ''),          // Remove country prefix: begleitpanzer_57
                        iconName.replace(/^[a-z]+_/, '').replace(/_/g, ' '), // Remove prefix + spaces: begleitpanzer 57
                        iconName.replace(/^[a-z]+_/, '').replace(/_/g, '-'), // Remove prefix + hyphens: begleitpanzer-57
                    ];
                    
                    // Add all variations as keys pointing to the same icon path
                    keys.forEach(key => {
                        if (key && key.length > 2 && typeof key === 'string') { // Ensure it's a valid string
                            vehicleIcons[key.toLowerCase()] = `/ICONS/VEHICLES/${file}`;
                        }
                    });
                }
            });
            
            log.debug(`Serving ${Object.keys(vehicleIcons).length} vehicle icon mappings`);
            res.json({
                success: true,
                count: files.length,
                mappings: vehicleIcons
            });
            
        } catch (dirError) {
            log.warn('[WARN] ICONS/VEHICLES directory not found, returning empty mappings');
            res.json({
                success: true,
                count: 0,
                mappings: {}
            });
        }
        
    } catch (error) {
        log.error('Error generating vehicle icon mappings:', error);
        res.status(500).json({
            success: false,
            error: 'Failed to generate vehicle icon mappings',
            mappings: {}
        });
    }
});

// ── Single match detail proxy ──
app.get('/api/match/:sessionId', cors(apiCorsOptions), async (req, res) => {
    const { sessionId } = req.params;
    if (!sessionId || !/^[0-9a-fA-F]+$/.test(sessionId)) {
        return res.status(400).json({ error: 'Invalid session ID format' });
    }
    try {
        const apiUrl = `${process.env.EXTERNAL_API_URL || 'http://127.0.0.1:6000'}/api/match/${sessionId}`;
        const response = await fetch(apiUrl);
        if (!response.ok) {
            if (response.status === 404) return res.status(404).json({ error: 'Match not found' });
            throw new Error(`API returned ${response.status}`);
        }
        res.json(await response.json());
    } catch (error) {
        log.error('[Match API] Error:', error);
        if (error.code === 'ECONNREFUSED') return res.status(503).json({ error: 'Stats API unavailable' });
        res.status(500).json({ error: 'Failed to fetch match data' });
    }
});

// ── Match replay data proxy ──
app.get('/api/match/:sessionId/replay', cors(apiCorsOptions), async (req, res) => {
    const { sessionId } = req.params;
    if (!sessionId || !/^[0-9a-fA-F]+$/.test(sessionId)) {
        return res.status(400).json({ error: 'Invalid session ID format' });
    }
    try {
        const apiUrl = `${process.env.EXTERNAL_API_URL || 'http://127.0.0.1:6000'}/api/match/${sessionId}/replay`;
        const response = await fetch(apiUrl);
        if (!response.ok) throw new Error(`API returned ${response.status}`);
        res.json(await response.json());
    } catch (error) {
        log.error('[Replay API] Error:', error);
        if (error.code === 'ECONNREFUSED') return res.status(503).json({ error: 'Stats API unavailable' });
        res.status(500).json({ error: 'Failed to fetch replay data' });
    }
});

// ── Match replay video ──
let _videoRenderCount = 0;
const _videoRenderMax = 2;
app.get('/api/match/:sessionId/video', async (req, res) => {
    const { sessionId } = req.params;
    if (!sessionId || !/^[0-9a-fA-F]+$/.test(sessionId)) {
        return res.status(400).json({ error: 'Invalid session ID format' });
    }

    const sessionDir = resolveReplaySessionDir(sessionId);
    const videoPath = path.join(sessionDir, 'replay_video.mp4');
    const replayPath = path.join(sessionDir, 'replay_data.json.gz');

    // 1. Serve from disk if cached
    if (fs.existsSync(videoPath)) {
        return res.sendFile(videoPath, {
            headers: { 'Content-Type': 'video/mp4', 'Cache-Control': 'public, max-age=86400' }
        });
    }

    // 2. Check if replay JSON exists for generation
    if (!fs.existsSync(replayPath)) {
        return res.status(404).json({ available: false, reason: 'No replay data available for this session' });
    }

    // 3. Generate video on demand (decompress + render)
    if (_videoRenderCount >= _videoRenderMax) {
        return res.status(503).json({ available: false, reason: 'Too many videos rendering — try again shortly' });
    }
    _videoRenderCount++;
    try {
        await new Promise((resolve, reject) => {
            const pythonBin = PYTHON_BIN;
            execFile(pythonBin, ['-m', 'BOT.render_replay', replayPath, videoPath], {
                timeout: 120000,
                cwd: REPO_ROOT
            }, (error, stdout, stderr) => {
                if (error) {
                    log.error('[Video] Generation stderr:', stderr);
                    reject(new Error(stderr || error.message));
                } else {
                    resolve();
                }
            });
        });

        if (!fs.existsSync(videoPath)) {
            return res.status(500).json({ available: false, reason: 'Video generation produced no output' });
        }

        res.sendFile(videoPath, {
            headers: { 'Content-Type': 'video/mp4', 'Cache-Control': 'public, max-age=86400' }
        });
    } catch (error) {
        log.error('[Video] Error generating video:', error);
        // Clean up broken/partial mp4 so it doesn't get cached
        try { if (fs.existsSync(videoPath)) fs.unlinkSync(videoPath); } catch (_) {}
        res.status(500).json({ available: false, reason: 'Video generation failed' });
    } finally {
        _videoRenderCount--;
    }
});

// ── Match replay canvas JSON ──
let _canvasRenderCount = 0;
const _canvasRenderMax = 3;
app.get('/api/match/:sessionId/replay-canvas', async (req, res) => {
    const { sessionId } = req.params;
    if (!sessionId || !/^[0-9a-fA-F]+$/.test(sessionId)) {
        return res.status(400).json({ error: 'Invalid session ID format' });
    }

    const sessionDir = resolveReplaySessionDir(sessionId);
    const jsonPath = path.join(sessionDir, 'replay_canvas.json');
    const replayPath = path.join(sessionDir, 'replay_data.json.gz');

    // 1. Serve from disk if cached
    if (fs.existsSync(jsonPath)) {
        return res.sendFile(jsonPath, {
            headers: { 'Content-Type': 'application/json', 'Cache-Control': 'public, max-age=86400' }
        });
    }

    // 2. Check if replay JSON exists
    if (!fs.existsSync(replayPath)) {
        return res.status(404).json({ available: false, reason: 'No replay data available' });
    }

    // 3. Generate on demand
    if (_canvasRenderCount >= _canvasRenderMax) {
        return res.status(503).json({ available: false, reason: 'Too many replays processing — try again shortly' });
    }
    _canvasRenderCount++;
    try {
        await new Promise((resolve, reject) => {
            const pythonBin = PYTHON_BIN;
            execFile(pythonBin, ['-m', 'BOT.render_replay', replayPath, jsonPath], {
                timeout: 30000,
                cwd: REPO_ROOT
            }, (error, stdout, stderr) => {
                if (error) {
                    log.error('[ReplayCanvas] Generation stderr:', stderr);
                    reject(new Error(stderr || error.message));
                } else {
                    resolve();
                }
            });
        });

        if (!fs.existsSync(jsonPath)) {
            return res.status(500).json({ available: false, reason: 'JSON generation produced no output' });
        }

        res.sendFile(jsonPath, {
            headers: { 'Content-Type': 'application/json', 'Cache-Control': 'public, max-age=86400' }
        });
    } catch (error) {
        log.error('[ReplayCanvas] Error generating JSON:', error);
        try { if (fs.existsSync(jsonPath)) fs.unlinkSync(jsonPath); } catch (_) {}
        res.status(500).json({ available: false, reason: 'Replay JSON generation failed' });
    } finally {
        _canvasRenderCount--;
    }
});

// ── Vehicle type icon serving (SHARED/ICONS, SHARED/ICONS/FALLBACKS, SHARED/ICONS/MINIS) ──
app.get('/api/icons/type/:name', (req, res) => {
    const { name } = req.params;
    if (!name || !/^[a-zA-Z0-9_\-]+$/.test(name)) {
        return res.status(400).json({ error: 'Invalid icon name' });
    }
    const iconsBase = process.env.SHARED_ICONS_DIR || path.join(REPO_ROOT, '..', 'SHARED', 'ICONS');
    // Check: ICONS/ → FALLBACKS/ → MINIS/
    const candidates = [
        path.join(iconsBase, name + '.png'),
        path.join(iconsBase, 'FALLBACKS', name + '.png'),
        path.join(iconsBase, 'MINIS', name + '.png'),
    ];
    for (const iconPath of candidates) {
        if (fs.existsSync(iconPath)) {
            return res.sendFile(iconPath, {
                headers: {
                    'Content-Type': 'image/png',
                    'Cache-Control': 'public, max-age=604800',
                    'Access-Control-Allow-Origin': '*'
                }
            });
        }
    }
    res.status(404).json({ error: 'Icon not found' });
});

// ── Minimap image serving ──
app.get('/api/match/minimap/:level', (req, res) => {
    const { level } = req.params;
    if (!level || !/^[a-zA-Z0-9_]+$/.test(level)) {
        return res.status(400).json({ error: 'Invalid level name' });
    }
    const minimapsDir = process.env.SHARED_MINIMAPS_DIR || path.join(REPO_ROOT, '..', 'SHARED', 'MAPS', 'MINIMAPS');
    const names = req.query.type === 'full'
        ? [level + '.png', level + '_map.png']
        : [level + '_tankmap.png', level + '.png', level + '_map.png'];
    let minimapPath = null;
    for (const name of [...new Set(names)]) {
        const candidate = path.join(minimapsDir, name);
        if (fs.existsSync(candidate)) {
            minimapPath = candidate;
            break;
        }
    }
    if (!minimapPath) {
        return res.status(404).json({ error: 'Minimap not found' });
    }
    res.sendFile(minimapPath, {
        headers: {
            'Content-Type': 'image/png',
            'Cache-Control': 'public, max-age=604800',
            'Access-Control-Allow-Origin': '*'
        }
    });
});

// ── Games search proxy ──
app.get('/api/games/search', cors(apiCorsOptions), async (req, res) => {
    try {
        const params = new URLSearchParams();
        if (req.query.player) params.append('player', req.query.player);
        if (req.query.map) params.append('map', req.query.map);
        if (req.query.squadron) params.append('squadron', req.query.squadron);
        if (req.query.time_from) params.append('time_from', req.query.time_from);
        if (req.query.time_to) params.append('time_to', req.query.time_to);
        if (req.query.limit) params.append('limit', req.query.limit);
        const apiUrl = `${process.env.EXTERNAL_API_URL || 'http://127.0.0.1:6000'}/api/games/search?${params}`;
        const response = await fetch(apiUrl);
        if (!response.ok) throw new Error(`API returned ${response.status}`);
        res.json(await response.json());
    } catch (error) {
        log.error('[Games Search] Error:', error);
        if (error.code === 'ECONNREFUSED') return res.status(503).json({ error: 'Stats API unavailable' });
        res.status(500).json({ error: 'Failed to search games' });
    }
});

// ── Analytics proxy ──
app.get('/api/analytics/:view/:squadron', cors(apiCorsOptions), async (req, res) => {
    try {
        const { view, squadron } = req.params;
        const allowedViews = new Set(['maps', 'time', 'comps', 'consistency', 'matchup']);
        if (!allowedViews.has(view)) {
            return res.status(400).json({ error: 'Unknown analytics view' });
        }
        const qs = new URLSearchParams(req.query).toString();
        const base = process.env.EXTERNAL_API_URL || 'http://127.0.0.1:6000';
        const apiUrl = `${base}/api/analytics/${view}/${encodeURIComponent(squadron)}${qs ? '?' + qs : ''}`;
        const response = await fetch(apiUrl);
        const body = await response.json();
        res.status(response.status).json(body);
    } catch (error) {
        log.error('[Analytics Proxy] Error:', error);
        if (error.code === 'ECONNREFUSED') return res.status(503).json({ error: 'Stats API unavailable' });
        res.status(500).json({ error: 'Failed to fetch analytics' });
    }
});

app.get('/api/i18n/vehicles', cors(apiCorsOptions), async (req, res) => {
    try {
        const base = process.env.EXTERNAL_API_URL || 'http://127.0.0.1:6000';
        const r = await fetch(`${base}/api/i18n/vehicles`);
        const body = await r.text();
        res.set('Cache-Control', 'public, max-age=3600');
        res.status(r.status).type('application/json').send(body);
    } catch (error) {
        log.error('[i18n vehicles proxy] Error:', error);
        if (error.code === 'ECONNREFUSED') return res.status(503).json({ error: 'Stats API unavailable' });
        res.status(500).json({ error: 'Failed to fetch vehicle translations' });
    }
});

app.get('/api/i18n/vehicle-types', cors(apiCorsOptions), async (req, res) => {
    try {
        const base = process.env.EXTERNAL_API_URL || 'http://127.0.0.1:6000';
        const r = await fetch(`${base}/api/i18n/vehicle-types`);
        const body = await r.text();
        res.set('Cache-Control', 'public, max-age=3600');
        res.status(r.status).type('application/json').send(body);
    } catch (error) {
        log.error('[i18n vehicle types proxy] Error:', error);
        if (error.code === 'ECONNREFUSED') return res.status(503).json({ error: 'Stats API unavailable' });
        res.status(500).json({ error: 'Failed to fetch vehicle types' });
    }
});

app.get('/api/analytics/vehicle-list', cors(apiCorsOptions), async (req, res) => {
    try {
        const base = process.env.EXTERNAL_API_URL || 'http://127.0.0.1:6000';
        const r = await fetch(`${base}/api/analytics/vehicle-list`);
        res.status(r.status).json(await r.json());
    } catch (error) {
        log.error('[Analytics Vehicle List] Error:', error);
        if (error.code === 'ECONNREFUSED') return res.status(503).json({ error: 'Stats API unavailable' });
        res.status(500).json({ error: 'Failed to fetch vehicle list' });
    }
});

app.get('/api/analytics/vehicle/:view/:vehicle', cors(apiCorsOptions), async (req, res) => {
    try {
        const { view, vehicle } = req.params;
        const allowedViews = new Set(['stats', 'players', 'squadrons', 'maps']);
        if (!allowedViews.has(view)) return res.status(400).json({ error: 'Unknown analytics view' });
        const qs = new URLSearchParams(req.query).toString();
        const base = process.env.EXTERNAL_API_URL || 'http://127.0.0.1:6000';
        const apiUrl = `${base}/api/analytics/vehicle/${view}/${encodeURIComponent(vehicle)}${qs ? '?' + qs : ''}`;
        const response = await fetch(apiUrl);
        const body = await response.json();
        res.status(response.status).json(body);
    } catch (error) {
        log.error('[Analytics Vehicle Proxy] Error:', error);
        if (error.code === 'ECONNREFUSED') return res.status(503).json({ error: 'Stats API unavailable' });
        res.status(500).json({ error: 'Failed to fetch analytics' });
    }
});

app.get('/api/analytics/player/:view/:uid', cors(apiCorsOptions), async (req, res) => {
    try {
        const { view, uid } = req.params;
        const allowedViews = new Set(['maps', 'squadmates', 'time', 'timeline', 'matchup']);
        if (!allowedViews.has(view)) {
            return res.status(400).json({ error: 'Unknown analytics view' });
        }
        const qs = new URLSearchParams(req.query).toString();
        const base = process.env.EXTERNAL_API_URL || 'http://127.0.0.1:6000';
        const apiUrl = `${base}/api/analytics/player/${view}/${encodeURIComponent(uid)}${qs ? '?' + qs : ''}`;
        const response = await fetch(apiUrl);
        const body = await response.json();
        res.status(response.status).json(body);
    } catch (error) {
        log.error('[Analytics Player Proxy] Error:', error);
        if (error.code === 'ECONNREFUSED') return res.status(503).json({ error: 'Stats API unavailable' });
        res.status(500).json({ error: 'Failed to fetch analytics' });
    }
});

// ── Maps list proxy ──
app.get('/api/maps', cors(apiCorsOptions), async (req, res) => {
    try {
        const apiUrl = `${process.env.EXTERNAL_API_URL || 'http://127.0.0.1:6000'}/api/maps`;
        const response = await fetch(apiUrl);
        if (!response.ok) throw new Error(`API returned ${response.status}`);
        res.json(await response.json());
    } catch (error) {
        log.error('[Maps API] Error:', error);
        if (error.code === 'ECONNREFUSED') return res.status(503).json({ error: 'Stats API unavailable' });
        res.status(500).json({ error: 'Failed to fetch maps' });
    }
});

// Squadron history API proxy (for client-side chart)
app.get('/api/squadrons/:name', cors(apiCorsOptions), async (req, res) => {
    try {
        const backendParams = new URLSearchParams();
        if (req.query.start_date) backendParams.append('start_date', req.query.start_date);
        if (req.query.end_date) backendParams.append('end_date', req.query.end_date);
        if (req.query.season) backendParams.append('season', req.query.season);
        if (req.query.week) backendParams.append('week', req.query.week);
        const qs = backendParams.toString();
        const apiUrl = `${process.env.EXTERNAL_API_URL || 'http://127.0.0.1:6000'}/api/squadrons/${encodeURIComponent(req.params.name)}${qs ? '?' + qs : ''}`;
        const response = await fetch(apiUrl);
        if (!response.ok) {
            if (response.status === 404) {
                return res.status(404).json({ error: 'Squadron not found', players: [] });
            }
            return res.status(response.status).json({ error: 'Failed to fetch squadron details' });
        }
        const data = await response.json();
        res.json(data);
    } catch (err) {
        log.error('Error fetching squadron details', err);
        res.status(500).json({ error: 'Failed to fetch squadron details' });
    }
});

app.get('/api/squadrons/:name/history', cors(apiCorsOptions), async (req, res) => {
    try {
        const apiUrl = `${process.env.EXTERNAL_API_URL || 'http://127.0.0.1:6000'}/api/squadrons/${encodeURIComponent(req.params.name)}/history`;
        const response = await fetch(apiUrl);
        if (!response.ok) {
            return res.status(response.status).json({ error: 'Failed to fetch squadron history' });
        }
        const data = await response.json();
        res.json(data);
    } catch (err) {
        log.error('Error fetching squadron history', err);
        res.status(500).json({ error: 'Failed to fetch squadron history' });
    }
});

app.get('/api/squadrons/:name/games', cors(apiCorsOptions), async (req, res) => {
    try {
        const backendParams = new URLSearchParams();
        if (req.query.start_date) backendParams.append('start_date', req.query.start_date);
        if (req.query.end_date) backendParams.append('end_date', req.query.end_date);
        if (req.query.season) backendParams.append('season', req.query.season);
        if (req.query.week) backendParams.append('week', req.query.week);
        const qs = backendParams.toString();
        const apiUrl = `${process.env.EXTERNAL_API_URL || 'http://127.0.0.1:6000'}/api/squadrons/${encodeURIComponent(req.params.name)}/games${qs ? '?' + qs : ''}`;
        const response = await fetch(apiUrl);
        if (!response.ok) {
            if (response.status === 404) {
                return res.status(404).json({ games: [], total_games_returned: 0, error: 'Squadron not found' });
            }
            return res.status(response.status).json({ error: 'Failed to fetch squadron games' });
        }
        const data = await response.json();
        res.json(data);
    } catch (err) {
        log.error('Error fetching squadron games', err);
        res.status(500).json({ error: 'Failed to fetch squadron games', games: [], total_games_returned: 0 });
    }
});

// ── TSS proxy routes ───────────────────────────────────────────────────────
// Mirrors the /api/squadrons/* proxy block above but targets the upstream
// /api/tss/* endpoints (defined in SREBOT/server.js) which read tss_battles.db
// and tss_teams.db. Both /api/tss/teams/* and /api/tss/squadrons/* are
// supported upstream — we proxy the canonical /teams/* path here.
const TSS_BACKEND = () => process.env.EXTERNAL_API_URL || 'http://127.0.0.1:6000';

function _forwardDateParams(req) {
    const qp = new URLSearchParams();
    if (req.query.start_date) qp.append('start_date', req.query.start_date);
    if (req.query.end_date) qp.append('end_date', req.query.end_date);
    if (req.query.season) qp.append('season', req.query.season);
    if (req.query.week) qp.append('week', req.query.week);
    return qp;
}

app.get('/api/tss/teams/resolve', cors(apiCorsOptions), async (req, res) => {
    try {
        const name = req.query.name || req.query.q || req.query.team || '';
        const apiUrl = `${TSS_BACKEND()}/api/tss/teams/resolve?name=${encodeURIComponent(name)}`;
        const response = await fetch(apiUrl);
        if (!response.ok) return res.status(response.status).json({ error: 'Failed to resolve TSS team' });
        res.json(await response.json());
    } catch (err) {
        log.error('Error resolving TSS team', err);
        res.status(500).json({ error: 'Failed to resolve TSS team' });
    }
});

app.get('/api/tss/leaderboard/teams', cors(apiCorsOptions), async (req, res) => {
    try {
        const qp = _forwardDateParams(req);
        if (req.query.limit) qp.append('limit', req.query.limit);
        const qs = qp.toString();
        const apiUrl = `${TSS_BACKEND()}/api/tss/leaderboard/teams${qs ? '?' + qs : ''}`;
        const response = await fetch(apiUrl);
        if (!response.ok) return res.status(response.status).json({ error: 'Failed to fetch TSS leaderboard', teams: [], total_teams: 0 });
        res.json(await response.json());
    } catch (err) {
        log.error('Error in TSS leaderboard proxy', err);
        res.status(500).json({ error: 'Failed to fetch TSS leaderboard', teams: [], total_teams: 0 });
    }
});

app.get('/api/tss/teams/:name', cors(apiCorsOptions), async (req, res) => {
    try {
        const qs = _forwardDateParams(req).toString();
        const apiUrl = `${TSS_BACKEND()}/api/tss/teams/${encodeURIComponent(req.params.name)}${qs ? '?' + qs : ''}`;
        const response = await fetch(apiUrl);
        if (!response.ok) {
            if (response.status === 404) {
                return res.status(404).json({ error: 'TSS team not found', players: [] });
            }
            return res.status(response.status).json({ error: 'Failed to fetch TSS team details' });
        }
        res.json(await response.json());
    } catch (err) {
        log.error('Error fetching TSS team details', err);
        res.status(500).json({ error: 'Failed to fetch TSS team details' });
    }
});

app.get('/api/tss/teams/:name/history', cors(apiCorsOptions), async (req, res) => {
    try {
        const apiUrl = `${TSS_BACKEND()}/api/tss/teams/${encodeURIComponent(req.params.name)}/history`;
        const response = await fetch(apiUrl);
        if (!response.ok) return res.status(response.status).json({ error: 'Failed to fetch TSS team history' });
        res.json(await response.json());
    } catch (err) {
        log.error('Error fetching TSS team history', err);
        res.status(500).json({ error: 'Failed to fetch TSS team history' });
    }
});

app.get('/api/tss/teams/:name/games', cors(apiCorsOptions), async (req, res) => {
    try {
        const qs = _forwardDateParams(req).toString();
        const apiUrl = `${TSS_BACKEND()}/api/tss/teams/${encodeURIComponent(req.params.name)}/games${qs ? '?' + qs : ''}`;
        const response = await fetch(apiUrl);
        if (!response.ok) {
            if (response.status === 404) {
                return res.status(404).json({ games: [], total_games_returned: 0, error: 'TSS team not found' });
            }
            return res.status(response.status).json({ error: 'Failed to fetch TSS team games' });
        }
        res.json(await response.json());
    } catch (err) {
        log.error('Error fetching TSS team games', err);
        res.status(500).json({ error: 'Failed to fetch TSS team games', games: [], total_games_returned: 0 });
    }
});

// API Routes with better error handling
let statsCache = null;
let statsCacheTime = 0;
const STATS_CACHE_TTL = 5 * 60 * 1000; // 5 minutes

app.get('/api/stats', cors(apiCorsOptions), async (req, res) => {
    try {
        const now = Date.now();
        if (statsCache && (now - statsCacheTime) < STATS_CACHE_TTL) {
            return res.json({ ...statsCache, uptime: Math.floor(process.uptime()) });
        }

        // Guild count from SREBOT_GUILDS.txt (bot writes one line per guild on startup)
        let servers = 0;
        const reportPath = path.join(STORAGE_ROOT, 'SREBOT_GUILDS.txt');
        try {
            const report = fs.readFileSync(reportPath, 'utf8').trim();
            servers = report ? report.split('\n').length : 0;
        } catch { /* file may not exist yet */ }

        // Player count + battle count from backend API
        let users = 0;
        let battles = 0;
        try {
            const apiUrl = `${process.env.EXTERNAL_API_URL || 'http://127.0.0.1:6000'}/api/leaderboard/stats`;
            const response = await fetch(apiUrl);
            if (response.ok) {
                const data = await response.json();
                users = data.total_players || 0;
                battles = data.total_battles || 0;
            }
        } catch { /* backend may be down */ }

        statsCache = { servers, users, battles, commands: 28 };
        statsCacheTime = now;

        res.json({ ...statsCache, uptime: Math.floor(process.uptime()) });
    } catch (error) {
        log.error('Error in /api/stats:', error);
        res.status(500).json({ error: 'Failed to fetch stats' });
    }
});

app.get('/api/invite', cors(apiCorsOptions), (req, res) => {
    try {
        // Real bot invite URL with proper permissions
        const inviteUrl = 'https://discord.com/oauth2/authorize?client_id=1254679514466877540&permissions=2048&scope=bot%20applications.commands';
        res.json({ inviteUrl });
    } catch (error) {
        log.error('Error in /api/invite:', error);
        res.status(500).json({ error: 'Failed to get invite URL' });
    }
});

app.get('/api/support', cors(apiCorsOptions), (req, res) => {
    try {
        // Real support server invite URL
        const supportUrl = 'https://discord.gg/BCvkK8JhPe';
        res.json({ supportUrl });
    } catch (error) {
        log.error('Error in /api/support:', error);
        res.status(500).json({ error: 'Failed to get support URL' });
    }
});

app.get('/api/bot-health', cors(apiCorsOptions), (req, res) => {
    try {
        const healthPath = path.join(STORAGE_ROOT, 'bot_health.json');
        const data = JSON.parse(fs.readFileSync(healthPath, 'utf8'));
        res.json(data);
    } catch (error) {
        res.status(503).json({ error: 'Health data unavailable' });
    }
});

// Leaderboard API proxy routes
app.get('/api/leaderboard/players', cors(apiCorsOptions), async (req, res) => {
    try {
        const { start_date, end_date } = req.query;

        // If date filter params are present, bypass cache and forward to API server
        if (start_date || end_date) {
            const backendParams = new URLSearchParams();
            if (start_date) backendParams.append('start_date', start_date);
            if (end_date) backendParams.append('end_date', end_date);
            const apiUrl = `${process.env.EXTERNAL_API_URL || 'http://127.0.0.1:6000'}/api/leaderboard/players?${backendParams.toString()}`;
            const response = await fetch(apiUrl, { signal: AbortSignal.timeout(30000) });
            if (!response.ok) throw new Error(`API error: ${response.status}`);
            return res.json(await response.json());
        }

        log.debug('[CACHE] Serving player leaderboard from cache...');
        const cachedData = await getCachedData('players');

        if (!cachedData) {
            throw new Error('No cached data available');
        }

        log.debug(`[CACHE] Player leaderboard served from cache: ${cachedData.players?.length || 0} players`);
        res.json(cachedData);

    } catch (error) {
        log.error('[ERROR] Error serving cached player leaderboard:', error);
        res.status(500).json({
            error: 'Failed to fetch player leaderboard data',
            players: [],
            pagination: {
                current_page: 1,
                total_pages: 0,
                has_next: false,
                has_previous: false
            }
        });
    }
});

app.get('/api/leaderboard/vehicles', cors(apiCorsOptions), async (req, res) => {
    try {
        const { start_date, end_date, page = 1, limit = 25, sort = 'total_kills', order = 'desc', search, squadron, min_kills = 0, vehicle_internal } = req.query;

        const pageNum = Math.max(1, parseInt(page) || 1);
        const limitNum = Math.min(100, Math.max(1, parseInt(limit) || 25));
        const minKills = parseInt(min_kills) || 0;

        let fullData;

        // Get full dataset (from cache or backend for date-filtered requests)
        if (start_date || end_date) {
            const backendParams = new URLSearchParams();
            if (start_date) backendParams.append('start_date', start_date);
            if (end_date) backendParams.append('end_date', end_date);
            const apiUrl = `${process.env.EXTERNAL_API_URL || 'http://127.0.0.1:6000'}/api/leaderboard/vehicles?${backendParams.toString()}`;
            const response = await fetch(apiUrl, { signal: AbortSignal.timeout(30000) });
            if (!response.ok) throw new Error(`API error: ${response.status}`);
            fullData = await response.json();
        } else {
            const cachedData = await getCachedData('vehicles');
            if (!cachedData) throw new Error('No cached data available');
            fullData = cachedData;
        }

        const allVehicles = fullData.vehicles || [];

        // Build squadron list from full dataset (before filtering)
        const squadronCounts = {};
        allVehicles.forEach(v => {
            if (v.player_squadron_name) {
                squadronCounts[v.player_squadron_name] = (squadronCounts[v.player_squadron_name] || 0) + 1;
            }
        });
        const squadrons = Object.entries(squadronCounts)
            .sort((a, b) => b[1] - a[1])
            .map(([name, count]) => ({ name, count }));

        // Apply filters
        let vehicles = allVehicles;

        // Exact internal-id filter (selected from the translation-map picker on
        // the vehicles leaderboard page). Distinguishes country variants that
        // share a display name (e.g. F-16A USA vs F-16A Italy) which the old
        // free-text `search` could not.
        if (vehicle_internal) {
            const target = String(vehicle_internal).toLowerCase();
            vehicles = vehicles.filter(v =>
                (v.vehicle_internal || '').toLowerCase() === target
            );
        }

        let searchTerm = '';
        if (search) {
            searchTerm = search.replace(/\u00A0/g, ' ').toLowerCase().trim();
            vehicles = vehicles.filter(v =>
                (v.vehicle || '').replace(/\u00A0/g, ' ').toLowerCase().includes(searchTerm) ||
                (v.player_nick || '').replace(/\u00A0/g, ' ').toLowerCase().includes(searchTerm)
            );
        }

        if (squadron) {
            const sqLower = squadron.toLowerCase();
            vehicles = vehicles.filter(v =>
                (v.player_squadron_name || '').toLowerCase() === sqLower
            );
        }

        if (minKills > 0) {
            vehicles = vehicles.filter(v => (v.total_kills || 0) >= minKills);
        }

        // Sort — when searching, prioritize vehicles whose name starts with the term
        const validSorts = ['total_kills', 'ground_kills', 'air_kills', 'kdr', 'win_rate', 'battles', 'wins', 'deaths', 'assists', 'captures', 'vehicle', 'player_nick'];
        const sortField = validSorts.includes(sort) ? sort : 'total_kills';
        const sortDir = order === 'asc' ? 1 : -1;

        vehicles.sort((a, b) => {
            if (searchTerm) {
                const aName = (a.vehicle || '').replace(/\u00A0/g, ' ').toLowerCase();
                const bName = (b.vehicle || '').replace(/\u00A0/g, ' ').toLowerCase();
                const aStarts = aName.startsWith(searchTerm) ? 0 : 1;
                const bStarts = bName.startsWith(searchTerm) ? 0 : 1;
                if (aStarts !== bStarts) return aStarts - bStarts;
            }
            let va = a[sortField];
            let vb = b[sortField];
            if (sortField === 'vehicle' || sortField === 'player_nick') {
                va = (va || '').toLowerCase();
                vb = (vb || '').toLowerCase();
            } else {
                va = parseFloat(va) || 0;
                vb = parseFloat(vb) || 0;
            }
            return va > vb ? sortDir : va < vb ? -sortDir : 0;
        });

        // Paginate
        const totalRows = vehicles.length;
        const totalPages = Math.ceil(totalRows / limitNum) || 1;
        const offset = (pageNum - 1) * limitNum;
        const pageVehicles = vehicles.slice(offset, offset + limitNum);

        res.json({
            vehicles: pageVehicles,
            pagination: {
                page: pageNum,
                limit: limitNum,
                total_rows: totalRows,
                total_pages: totalPages
            },
            squadrons,
            date_filter: fullData.date_filter || null
        });

    } catch (error) {
        log.error('Error in vehicle leaderboard proxy:', error);
        res.status(500).json({
            error: 'Failed to fetch vehicle leaderboard data',
            vehicles: [],
            pagination: { page: 1, limit: 25, total_rows: 0, total_pages: 0 },
            squadrons: []
        });
    }
});

app.get('/api/leaderboard/stats', cors(apiCorsOptions), async (req, res) => {
    try {
        log.debug('[CACHE] Serving leaderboard statistics from cache...');
        const cachedData = await getCachedData('stats');
        
        if (!cachedData) {
            throw new Error('No cached data available');
        }
        
        log.debug('[CACHE] Leaderboard statistics served from cache');
        res.json(cachedData);
        
    } catch (error) {
        log.error('Error in leaderboard stats proxy:', error);
        res.status(500).json({
            error: 'Failed to fetch leaderboard statistics',
            total_players: 0,
            total_vehicles_used: 0,
            total_battles: 0,
            top_vehicles: [],
            last_updated: new Date().toISOString()
        });
    }
});

app.get('/api/leaderboard/squadrons', cors(apiCorsOptions), async (req, res) => {
    try {
        const { start_date, end_date } = req.query;

        // If date filter params are present, bypass cache and forward to API server
        if (start_date || end_date) {
            const backendParams = new URLSearchParams();
            if (start_date) backendParams.append('start_date', start_date);
            if (end_date) backendParams.append('end_date', end_date);
            const apiUrl = `${process.env.EXTERNAL_API_URL || 'http://127.0.0.1:6000'}/api/leaderboard/squadrons?${backendParams.toString()}`;
            const response = await fetch(apiUrl);
            if (!response.ok) throw new Error(`API error: ${response.status}`);
            return res.json(await response.json());
        }

        log.debug('[CACHE] Serving squadron leaderboard from cache...');
        const cachedData = await getCachedData('squadrons');

        if (!cachedData) {
            throw new Error('No cached data available');
        }

        log.debug(`[CACHE] Squadron leaderboard served from cache: ${cachedData.squadrons?.length || 0} squadrons`);
        res.json(cachedData);

    } catch (error) {
        log.error('Error in squadron leaderboard proxy:', error);
        res.status(500).json({
            error: 'Failed to fetch squadron leaderboard data',
            squadrons: [],
            total_squadrons: 0
        });
    }
});

// Endpoint to get API key for frontend (only accessible from pages, not direct API calls)
app.get('/api-key', (req, res) => {
    res.json({ 
        apiKey: generateDailyAPIKey(),
        expires: new Date(Date.now() + 86400000).toISOString() // 24 hours
    });
});

// Cache stats endpoint (useful for monitoring and debugging)
app.get('/api/cache-stats', cors(apiCorsOptions), (req, res) => {
    try {
        const stats = {
            search_cache: {
                size: searchCache.size(),
                max_size: 500,
                utilization: `${((searchCache.size() / 500) * 100).toFixed(1)}%`
            },
            leaderboard_cache: {
                players: {
                    cached: !!leaderboardCache.players.data,
                    age_seconds: leaderboardCache.players.data ? 
                        Math.floor((Date.now() - leaderboardCache.players.lastUpdated) / 1000) : null
                },
                vehicles: {
                    cached: !!leaderboardCache.vehicles.data,
                    age_seconds: leaderboardCache.vehicles.data ? 
                        Math.floor((Date.now() - leaderboardCache.vehicles.lastUpdated) / 1000) : null
                },
                squadrons: {
                    cached: !!leaderboardCache.squadrons.data,
                    age_seconds: leaderboardCache.squadrons.data ? 
                        Math.floor((Date.now() - leaderboardCache.squadrons.lastUpdated) / 1000) : null
                },
                stats: {
                    cached: !!leaderboardCache.stats.data,
                    age_seconds: leaderboardCache.stats.data ? 
                        Math.floor((Date.now() - leaderboardCache.stats.lastUpdated) / 1000) : null
                }
            },
            pending_search_requests: pendingSearchRequests.size,
            server_uptime_seconds: Math.floor(process.uptime())
        };
        
        res.json(stats);
    } catch (error) {
        log.error('[CACHE STATS] Error generating cache statistics:', error);
        res.status(500).json({ error: 'Failed to generate cache statistics' });
    }
});

// API Honeypot endpoints to detect scanning and malicious activity
const honeypotEndpoints = [
    '/api/admin',
    '/api/users',
    '/api/config',
    '/api/debug',
    '/api/internal',
    '/api/system',
    '/api/database',
    '/api/backup',
    '/api/logs',
    '/api/test'
];

honeypotEndpoints.forEach(endpoint => {
    app.all(endpoint, (req, res) => {
        const clientIP = req.ip || req.connection.remoteAddress;
        const userAgent = req.get('User-Agent');
        const referer = req.get('Referer');
        
        log.warn(`[HONEYPOT] HONEYPOT TRIGGERED: ${endpoint}`);
        log.warn(`  IP: ${clientIP}`);
        log.warn(`  Method: ${req.method}`);
        log.warn(`  User-Agent: ${userAgent?.substring(0, 200) || 'none'}`);
        log.warn(`  Referer: ${referer || 'none'}`);
        log.warn(`  Headers: ${JSON.stringify(req.headers)}`);
        
        // Add IP to suspicious list for enhanced monitoring
        suspiciousIPs.set(clientIP, {
            firstSeen: Date.now(),
            honeypotHits: (suspiciousIPs.get(clientIP)?.honeypotHits || 0) + 1,
            lastEndpoint: endpoint
        });
        
        // Return generic 404 to not reveal it's a honeypot
        res.status(404).json({
            error: 'Not found',
            message: 'The requested resource was not found'
        });
    });
});

app.post('/webhook/deploy', express.json(), (req, res) => {
    const signature = req.headers['x-hub-signature-256'];
    const webhookSecret = process.env.WEBHOOK_SECRET;
    
    if (!webhookSecret) {
        log.error('[WEBHOOK] WEBHOOK_SECRET not configured');
        return res.status(500).json({ error: 'Webhook not configured' });
    }
    
    if (signature) {
        const hmac = crypto.createHmac('sha256', webhookSecret);
        const digest = 'sha256=' + hmac.update(JSON.stringify(req.body)).digest('hex');
        
        if (signature !== digest) {
            log.error('[WEBHOOK] Invalid webhook signature');
            return res.status(401).json({ error: 'Invalid signature' });
        }
    }
    
    const event = req.headers['x-github-event'] || req.headers['x-gitlab-event'];
    
    if (event === 'push' || event === 'Push Hook') {
        const ref = req.body.ref;
        const branch = process.env.DEPLOY_BRANCH || 'main';
        
        if (!ref || !ref.endsWith(branch)) {
            return res.json({ message: `Ignoring push to ${ref}, not ${branch}` });
        }
        
        log.info(`[WEBHOOK] Webhook received: Push to ${branch} branch`);
        
        const deployScript = path.join(__dirname, 'deploy.sh');
        
        exec(`bash ${deployScript}`, (error, stdout, stderr) => {
            if (error) {
                log.error(`[WEBHOOK] Deployment error: ${error.message}`);
                log.error(`[WEBHOOK] stderr: ${stderr}`);
                return;
            }
            log.info(`[WEBHOOK] Deployment output: ${stdout}`);
            if (stderr) {
                log.warn(`[WEBHOOK] Deployment warnings: ${stderr}`);
            }
        });
        
        res.json({ message: 'Deployment started' });
    } else {
        res.json({ message: 'Event ignored' });
    }
});

// Map a Whop product_id or plan_id to a tier. Webhooks include data.product.id,
// while the periodic membership sync may only expose plan.id for some records.
function whopIdToTier(id) {
    if (!id) return null;
    if (id === process.env.WHOP_PRODUCT_ID_MAX || id === process.env.WHOP_PLAN_ID_MAX) return 'max';
    if (id === process.env.WHOP_PRODUCT_ID_PRO || id === process.env.WHOP_PLAN_ID_PRO) return 'pro';
    if (id === process.env.WHOP_PRODUCT_ID_STANDARD || id === process.env.WHOP_PLAN_ID_STANDARD) return 'standard';
    return null;
}

function whopTierIdFromMembership(data) {
    return data?.product_id
        || (typeof data?.product === 'string' ? data.product : data?.product?.id)
        || data?.plan_id
        || (typeof data?.plan === 'string' ? data.plan : data?.plan?.id)
        || data?.access_pass
        || null;
}

app.post('/webhook/whop', (req, res) => {
    const secret = process.env.WHOP_WEBHOOK_SECRET;
    const sig = req.headers['x-whop-signature'];
    if (secret && sig) {
        const expected = crypto.createHmac('sha256', secret).update(req.rawBody).digest('hex');
        if (sig !== expected) {
            log.warn(`[WHOP] Signature mismatch — rejecting`);
            return res.status(401).end();
        }
    } else if (!sig) {
        log.warn('[WHOP] No x-whop-signature header — proceeding unverified');
    }
    const { type: action, data } = req.body;
    // Try metadata first (legacy), then extract from custom field responses
    let guildId = data?.metadata?.guild_id;
    if (!guildId && Array.isArray(data?.custom_field_responses)) {
        for (const field of data.custom_field_responses) {
            const val = String(field.value ?? field.response ?? field.answer ?? '').trim();
            if (/^\d{17,19}$/.test(val)) { guildId = val; break; }
        }
    }
    log.info(`[WHOP] Webhook received: type=${action} membership=${data?.id} guild_id=${guildId || 'none'} custom_fields=${JSON.stringify(data?.custom_field_responses)}`);

    // ── Helper: deactivate a guild by guild_id or fall back to membership lookup ──
    const deactivateGuild = (reason) => {
        if (guildId) {
            entitlementsDb.run(
                `UPDATE guild_entitlements SET status='cancelled' WHERE guild_id=?`,
                [guildId],
                (err) => {
                    if (err) log.error(`[WHOP] DB write failed for guild ${guildId}:`, err);
                    else { log.info(`[WHOP] Guild ${guildId} deactivated via ${reason}`); touchEntitlementsDirty(); }
                    res.status(200).json({ success: true, guild_id: guildId, action: reason });
                }
            );
            return;
        }
        // No guild_id in payload — look up by whop_membership_id
        const membershipId = data?.membership_id || data?.membership?.id || data?.membership || data?.id;
        if (!membershipId) {
            log.warn(`[WHOP] ${reason}: no guild_id or membership_id found`);
            return res.status(200).json({ success: true, message: `${reason} received but no identifiers found` });
        }
        entitlementsDb.get(
            `SELECT guild_id FROM guild_entitlements WHERE whop_membership_id=?`,
            [membershipId],
            (err, row) => {
                if (err || !row) {
                    log.warn(`[WHOP] ${reason}: no guild found for membership ${membershipId}`);
                    return res.status(200).json({ success: true, message: 'No guild found for membership' });
                }
                entitlementsDb.run(
                    `UPDATE guild_entitlements SET status='cancelled' WHERE guild_id=?`,
                    [row.guild_id],
                    (err2) => {
                        if (err2) log.error(`[WHOP] DB write failed for guild ${row.guild_id}:`, err2);
                        else { log.info(`[WHOP] Guild ${row.guild_id} deactivated via ${reason} (membership ${membershipId})`); touchEntitlementsDirty(); }
                        res.status(200).json({ success: true, guild_id: row.guild_id, action: reason });
                    }
                );
            }
        );
    };

    // ── Membership activated ──
    if (action === 'membership.activated') {
        if (!guildId) {
            log.warn(`[WHOP] No guild_id found — skipping activation (membership ${data?.id})`);
            return res.status(200).json({ success: true, message: 'No guild_id found, skipped' });
        }
        const whopTierId = whopTierIdFromMembership(data);
        const tier = whopIdToTier(whopTierId) || 'standard';
        entitlementsDb.run(
            `INSERT INTO guild_entitlements (guild_id, whop_membership_id, status, renewed_at, tier)
             VALUES (?, ?, 'active', strftime('%s','now'), ?)
             ON CONFLICT(guild_id) DO UPDATE SET status='active',
                whop_membership_id=excluded.whop_membership_id,
                renewed_at=excluded.renewed_at,
                tier=excluded.tier`,
            [guildId, data.id, tier],
            (err) => {
                if (err) {
                    log.error(`[WHOP] DB write failed for guild ${guildId}:`, err);
                    return res.status(500).json({ success: false, error: 'DB write failed' });
                }
                log.info(`[WHOP] Guild ${guildId} activated (membership ${data.id}, tier=${tier}, product_or_plan=${whopTierId || 'unknown'})`);
                touchEntitlementsDirty();
                res.status(200).json({ success: true, guild_id: guildId, action: 'activated', tier });
            }
        );

    // ── Membership deactivated ──
    } else if (action === 'membership.deactivated') {
        deactivateGuild('membership.deactivated');

    // ── Refund issued — revoke entitlement ──
    } else if (action === 'refund.created') {
        deactivateGuild('refund.created');

    // ── Chargeback / dispute filed — revoke entitlement ──
    } else if (action === 'dispute.created') {
        deactivateGuild('dispute.created');

    // ── Early dispute warning — log only, dispute.created will follow if it escalates ──
    } else if (action === 'dispute_alert.created') {
        log.warn(`[WHOP] Dispute alert (early warning) — membership=${data?.membership_id || data?.id} guild_id=${guildId || 'none'}`);
        res.status(200).json({ success: true, action: 'dispute_alert_logged' });

    // ── Payment failed — log only, Whop retries before deactivating ──
    } else if (action === 'payment.failed') {
        log.warn(`[WHOP] Payment failed — membership=${data?.membership_id || data?.id} guild_id=${guildId || 'none'}`);
        res.status(200).json({ success: true, action: 'payment_failed_logged' });

    // ── Everything else ──
    } else {
        log.info(`[WHOP] Unhandled event: ${action}`);
        res.status(200).json({ success: true, message: 'Unhandled action type', action });
    }
});

// ── Games pages ──
app.get('/games', (req, res) => {
    res.render('games', {
        botName: 'Toothless SQB Bot'
    });
});

app.get('/games/:sessionId', (req, res) => {
    const { sessionId } = req.params;
    if (!sessionId || !/^[0-9a-fA-F]+$/.test(sessionId)) {
        return res.status(400).render('404', {
            statusCode: 400,
            error: 'Invalid session ID format.',
            botName: 'Toothless SQB Bot'
        });
    }
    res.render('game-detail', {
        botName: 'Toothless SQB Bot',
        sessionId: sessionId
    });
});

// Handle 404
app.use((req, res) => {
    res.status(404).render('404', {
        botName: 'Toothless SQB Bot'
    });
});

// Error handling
app.use((err, req, res, next) => {
    log.error(err.stack);
    res.status(500).json({ error: 'Something went wrong!' });
});

// ── Whop entitlements sync (v2 API) ──
async function syncWhopEntitlements() {
    const apiKey = process.env.WHOP_API_KEY;
    if (!apiKey) {
        console.warn('[WHOP-SYNC] Missing WHOP_API_KEY, skipping sync');
        return;
    }

    console.log('[WHOP-SYNC] Starting entitlements sync...');

    try {
        // Fetch all memberships from Whop v2 API (paginated)
        const allMemberships = [];
        let page = 1;

        while (true) {
            const resp = await fetch(`https://api.whop.com/api/v2/memberships?per=50&page=${page}&valid=true`, {
                headers: { 'Authorization': `Bearer ${apiKey}` }
            });

            if (!resp.ok) {
                console.error(`[WHOP-SYNC] API error: ${resp.status} ${resp.statusText}`);
                const body = await resp.text();
                console.error(`[WHOP-SYNC] Response: ${body}`);
                return;
            }

            const json = await resp.json();
            if (!json.data || !Array.isArray(json.data)) {
                console.error('[WHOP-SYNC] Unexpected response format:', JSON.stringify(json).slice(0, 200));
                return;
            }

            for (const mem of json.data) {
                // Extract guild ID from custom_field_responses (v2 uses "answer" field)
                let guildId = null;
                if (Array.isArray(mem.custom_field_responses)) {
                    for (const field of mem.custom_field_responses) {
                        const val = String(field.answer ?? '').trim();
                        if (/^\d{17,19}$/.test(val)) { guildId = val; break; }
                    }
                }
                const whopTierId = whopTierIdFromMembership(mem);
                allMemberships.push({
                    guildId,
                    membershipId: mem.id,
                    status: mem.status,
                    tier: whopIdToTier(whopTierId),
                });
            }

            if (page >= (json.pagination?.total_page || 1)) break;
            page++;
        }

        const activeMemberships = allMemberships.filter(m => m.status === 'active' && m.guildId);
        console.log(`[WHOP-SYNC] Fetched ${allMemberships.length} total, ${activeMemberships.length} active with guild IDs (${page} page(s))`);

        // Get current DB state
        const dbEntitlements = await new Promise((resolve, reject) => {
            entitlementsDb.all('SELECT guild_id, whop_membership_id, status, tier FROM guild_entitlements', (err, rows) => {
                if (err) reject(err); else resolve(rows || []);
            });
        });

        const dbByMembership = new Map(dbEntitlements.filter(r => r.whop_membership_id).map(r => [r.whop_membership_id, r]));
        const whopActiveGuilds = new Set(activeMemberships.map(m => m.guildId));

        let activated = 0, deactivated = 0, unchanged = 0;

        // Activate/update memberships that Whop says are active
        for (const { guildId, membershipId, tier: mappedTier } of activeMemberships) {
            const existing = dbByMembership.get(membershipId);
            const tier = mappedTier || existing?.tier || 'standard';
            if (!existing || existing.status !== 'active' || existing.tier !== tier) {
                await new Promise((resolve, reject) => {
                    entitlementsDb.run(
                        `INSERT INTO guild_entitlements (guild_id, whop_membership_id, status, renewed_at, tier)
                         VALUES (?, ?, 'active', strftime('%s','now'), ?)
                         ON CONFLICT(guild_id) DO UPDATE SET status='active',
                            whop_membership_id=excluded.whop_membership_id,
                            renewed_at=excluded.renewed_at,
                            tier=excluded.tier`,
                        [guildId, membershipId, tier],
                        (err) => { if (err) reject(err); else resolve(); }
                    );
                });
                activated++;
            } else {
                unchanged++;
            }
        }

        // Deactivate DB entries that are 'active' but no longer active on Whop
        for (const row of dbEntitlements) {
            if (row.status === 'active' && !whopActiveGuilds.has(row.guild_id)) {
                await new Promise((resolve, reject) => {
                    entitlementsDb.run(
                        `UPDATE guild_entitlements SET status='cancelled' WHERE guild_id=?`,
                        [row.guild_id],
                        (err) => { if (err) reject(err); else resolve(); }
                    );
                });
                deactivated++;
            }
        }

        if (activated > 0 || deactivated > 0) touchEntitlementsDirty();
        console.log(`[WHOP-SYNC] Sync complete: ${activated} activated, ${deactivated} deactivated, ${unchanged} unchanged`);
    } catch (err) {
        console.error('[WHOP-SYNC] Sync failed:', err);
    }
}

// Run sync on startup (after 10s delay) and then every 6 hours
const WHOP_SYNC_INTERVAL = 30 * 60 * 1000;

app.listen(PORT, async () => {
    log.info(`[SERVER] Server running on port ${PORT}`);
    log.info(`[SERVER] Visit: http://localhost:${PORT}`);

    if (IS_PRIMARY_WORKER) {
        // Defer cache warmup so we don't pile 4 sequential full-table-scan
        // aggregations onto the API while it's still opening DB connections
        // and warming its own caches. The API's readiness gate would fast-fail
        // these with 503 inside the cold-start window anyway, leaving the
        // cache empty until the next 5-minute auto-refresh tick. Waiting 30s
        // lets the API's readiness signal go green first; cold-start user
        // requests in that window lazy-fill via getCachedData (32s wait).
        setTimeout(() => {
            void initializeCache().catch(err => {
                log.error('[CACHE] Startup cache warmup failed:', err);
            });
        }, 30000);

        // Initial entitlements sync after short delay
        setTimeout(() => syncWhopEntitlements(), 10000);
        // Recurring sync every 6 hours
        setInterval(() => syncWhopEntitlements(), WHOP_SYNC_INTERVAL);
    }
});
