require('dotenv').config();
const express = require('express');
const sqlite3 = require('sqlite3').verbose();
const cors = require('cors');
const fs = require('fs');
const path = require('path');
const zlib = require('zlib');
const seasonsUtil = require('./web/utils/seasons');

/** Parse a JSON column that may be gzip-compressed (Buffer) or plain text (string). */
function parseJsonColumn(data) {
    if (data === null || data === undefined) return null;
    if (Buffer.isBuffer(data)) return JSON.parse(zlib.gunzipSync(data).toString('utf-8'));
    return JSON.parse(data);
}

const STORAGE_ROOT = (process.env.STORAGE_VOL_PATH || '').trim();
if (!STORAGE_ROOT) {
    throw new Error('STORAGE_VOL_PATH must be set');
}
const REPLAYS_PATH = path.join(STORAGE_ROOT, 'REPLAYS');
const COMPS_PATH = path.join(STORAGE_ROOT, 'COMPS');
const WL_DB_PATH = path.join(STORAGE_ROOT, 'wl.db');
const POINTS_DB_PATH = path.join(STORAGE_ROOT, 'points.db');
fs.mkdirSync(REPLAYS_PATH, { recursive: true });

function replayDataPath(sessionId) {
    const sid = String(sessionId).toLowerCase();
    const candidates = [
        path.join(REPLAYS_PATH, 'SRE', sid, 'replay_data.json.gz'),
        path.join(REPLAYS_PATH, sid, 'replay_data.json.gz'),
    ];
    for (const p of candidates) {
        if (fs.existsSync(p)) return p;
    }
    return candidates[0];
}

const app = express();
const PORT = process.env.SREBOT_API_PORT || 6000;
const API_BEARER_TOKEN = process.env.SREBOT_API_BEARER_TOKEN || '';
const ADMIN_BEARER_TOKEN = process.env.SREBOT_ADMIN_BEARER_TOKEN || null;
const wlDb = new sqlite3.Database(WL_DB_PATH);
const pointsDb = new sqlite3.Database(POINTS_DB_PATH);

const log = {
    info: (message, extra = {}) => {
        console.log(`[${new Date().toISOString()}] [INFO] ${message}`, 
                   extra && Object.keys(extra).length > 0 ? JSON.stringify(extra) : '');
    },
    error: (message, error = null, extra = {}) => {
        console.error(`[${new Date().toISOString()}] [ERROR] ${message}`, 
                     error ? error.message || error : '',
                     extra && Object.keys(extra).length > 0 ? JSON.stringify(extra) : '');
    },
    warn: (message, extra = {}) => {
        console.warn(`[${new Date().toISOString()}] [WARN] ${message}`, 
                    extra && Object.keys(extra).length > 0 ? JSON.stringify(extra) : '');
    },
    debug: (message, extra = {}) => {
        if (process.env.NODE_ENV === 'development') {
            console.log(`[${new Date().toISOString()}] [DEBUG] ${message}`, 
                       extra && Object.keys(extra).length > 0 ? JSON.stringify(extra) : '');
        }
    }
};

app.use(cors());
app.use(express.json());

function requireApiBearer(req, res, next) {
    if (!API_BEARER_TOKEN) return next();

    const auth = req.get('authorization') || '';
    if (auth === `Bearer ${API_BEARER_TOKEN}`) {
        return next();
    }

    return res.status(401).json({ error: 'Unauthorized' });
}

function requireAdminBearer(req, res, next) {
    if (!ADMIN_BEARER_TOKEN) return res.status(403).json({ error: 'Admin access not configured' });
    const auth = req.get('authorization') || '';
    if (auth === `Bearer ${ADMIN_BEARER_TOKEN}`) return next();
    return res.status(403).json({ error: 'Forbidden' });
}

app.use('/api', requireApiBearer);

// Readiness gate: heavy aggregation endpoints sit behind this so cold-start
// requests don't pile up on the read connection while the DB is still opening
// indexes and the vehicle-list cache is warming. Resolves when boot work
// completes; falls back to a 15s safety timer.
let serverReady = false;
const STARTUP_GRACE_MS = 15000;
const startupReadyAt = Date.now();
let resolveReady;
const serverReadyPromise = new Promise((resolve) => { resolveReady = resolve; });
function markServerReady(reason) {
    if (serverReady) return;
    serverReady = true;
    log.info('Server ready', { reason, ms: Date.now() - startupReadyAt });
    resolveReady();
}
setTimeout(() => markServerReady('grace_timer'), STARTUP_GRACE_MS);

const HEAVY_PATH_PATTERN = /^\/api\/(leaderboard|analytics)\b/;
app.use((req, res, next) => {
    if (serverReady || !HEAVY_PATH_PATTERN.test(req.path)) return next();
    const waitStart = Date.now();
    const onReady = () => {
        if (res.headersSent) return;
        next();
    };
    const timer = setTimeout(() => {
        if (res.headersSent) return;
        res.set('Retry-After', '5');
        res.status(503).json({
            error: 'API warming up, retry shortly',
            errorCode: 'API_NOT_READY',
            waited_ms: Date.now() - waitStart,
        });
    }, 8000);
    serverReadyPromise.then(() => { clearTimeout(timer); onReady(); });
});

const responseCache = new Map();
const CACHE_TTL = 5 * 60 * 1000;
const STATS_CACHE_TTL = 30 * 60 * 1000; // Global stats change slowly — cache longer to avoid frequent 6s+ rebuilds
const inflightRequests = new Map(); // dedup: key -> Promise
let squadronLookupCache = null;
let squadronLookupCacheTime = 0;
const SQUADRON_CACHE_TTL = 30 * 60 * 1000;
let hasSquadronColumn = false;
let nickLookupCache = null;
let nickLookupCacheTime = 0;
const performanceBenchmarkCache = new Map();
const performanceBenchmarkInFlight = new Map();
const PERFORMANCE_BENCHMARK_CACHE_TTL = 60 * 60 * 1000;

function getCachedResponse(key, ttl = CACHE_TTL) {
    const cached = responseCache.get(key);
    if (cached && Date.now() - cached.timestamp < ttl) {
        return cached.data;
    }
    responseCache.delete(key);
    return null;
}

function setCachedResponse(key, data) {
    responseCache.set(key, { data, timestamp: Date.now() });
    if (responseCache.size > 100) {
        const oldestKey = responseCache.keys().next().value;
        responseCache.delete(oldestKey);
    }
}

function runWalCheckpoint(mode, successMessage, errorMessage, logMethod = 'info') {
    db.run('PRAGMA busy_timeout = 10000;', (busyErr) => {
        if (busyErr) {
            log.error(`${errorMessage} (busy timeout setup failed):`, busyErr);
            return;
        }
        db.run(`PRAGMA wal_checkpoint(${mode});`, (err) => {
            if (err) {
                log.error(errorMessage, err);
            } else {
                log[logMethod](successMessage);
            }
        });
    });
}

// Dedup wrapper: if a query for this key is already in flight, wait for it
function dedup(key, worker) {
    const existing = inflightRequests.get(key);
    if (existing) {
        log.info(`Dedup: waiting on in-flight request for ${key}`);
        return existing;
    }
    const promise = worker().finally(() => inflightRequests.delete(key));
    inflightRequests.set(key, promise);
    return promise;
}

// Best-effort sync lookup of a squadron name → clan_id using the warm cache.
// Returns null if the cache isn't populated yet or the input doesn't resolve.
// Callers fall back to text matching when this returns null.
function resolveClanIdSync(input) {
    if (!squadronLookupCache || !input) return null;
    const direct = squadronLookupCache[input]
        || squadronLookupCache[String(input)]
        || squadronLookupCache[String(input).toLowerCase()];
    if (direct && direct.clan_id != null) return Number(direct.clan_id);
    return null;
}

// Resolve a squadron URL parameter to { clanId, variants } using the warm
// cache. variants is the union of (current long_name, short_name, tag_name,
// the raw input) — used for text-fallback matching against pre-migration
// rows whose clan_id wasn't backfilled. clanId is the canonical id when
// known. Both fields may be null/empty if the cache hasn't loaded yet.
function resolveSquadronFilter(input) {
    const variants = new Set();
    const raw = input == null ? '' : String(input).trim();
    if (raw) variants.add(raw);
    let clanId = null;
    if (squadronLookupCache && raw) {
        const row = squadronLookupCache[raw]
            || squadronLookupCache[raw.toLowerCase()];
        if (row) {
            if (row.clan_id != null) clanId = Number(row.clan_id);
            if (row.long_name) variants.add(row.long_name);
            if (row.short_name) variants.add(row.short_name);
            if (row.tag_name) variants.add(row.tag_name);
        }
    }
    return { clanId, variants: Array.from(variants) };
}

// Build a WHERE clause for match_summary that matches matches involving the
// given squadron. Prefers winning_clan_id/losing_clan_id when available;
// falls back to text variants for rows whose clan_id wasn't backfilled.
function matchSummarySquadronWhere(filter) {
    const parts = [];
    const params = [];
    if (filter.clanId != null) {
        parts.push('winning_clan_id = ?');
        params.push(filter.clanId);
        parts.push('losing_clan_id = ?');
        params.push(filter.clanId);
    }
    if (filter.variants.length) {
        const ph = filter.variants.map(() => '?').join(',');
        const fallbackGuard = filter.clanId != null
            ? '(winning_clan_id IS NULL OR losing_clan_id IS NULL) AND '
            : '';
        parts.push(`(${fallbackGuard}(winning_sq IN (${ph}) OR losing_sq IN (${ph})))`);
        params.push(...filter.variants, ...filter.variants);
    }
    if (!parts.length) {
        return { clause: '0', params: [] };
    }
    return { clause: `(${parts.join(' OR ')})`, params };
}

// True if the row (with winning_sq/losing_sq/winning_clan_id/losing_clan_id)
// represents a win for the resolved squadron.
function rowIsWinFor(row, filter, variantSet) {
    if (filter.clanId != null && row.winning_clan_id != null) {
        return Number(row.winning_clan_id) === filter.clanId;
    }
    return !!(row.winning_sq && variantSet.has(row.winning_sq));
}

function rowIsLossFor(row, filter, variantSet) {
    if (filter.clanId != null && row.losing_clan_id != null) {
        return Number(row.losing_clan_id) === filter.clanId;
    }
    return !!(row.losing_sq && variantSet.has(row.losing_sq));
}

// Build a WHERE fragment for player_games_hist `p` that selects rows for the
// resolved squadron. Prefers p.clan_id when known; falls back to variant text.
function playerGamesHistSquadronWhere(filter, alias = 'p') {
    const a = alias ? `${alias}.` : '';
    const parts = [];
    const params = [];
    if (filter.clanId != null) {
        parts.push(`${a}clan_id = ?`);
        params.push(filter.clanId);
    }
    if (filter.variants.length) {
        const ph = filter.variants.map(() => '?').join(',');
        const fallbackGuard = filter.clanId != null
            ? `${a}clan_id IS NULL AND `
            : '';
        parts.push(`(${fallbackGuard}${a}squadron_name IN (${ph}))`);
        params.push(...filter.variants);
    }
    if (!parts.length) return { clause: '0', params: [] };
    return { clause: `(${parts.join(' OR ')})`, params };
}

function loadSquadronLookupCached(callback) {
    if (squadronLookupCache && Date.now() - squadronLookupCacheTime < SQUADRON_CACHE_TTL) {
        return callback(squadronLookupCache);
    }
    
    const squadronsDbPath = path.join(STORAGE_ROOT, 'squadrons.db');
    if (!fs.existsSync(squadronsDbPath)) {
        squadronLookupCache = {};
        squadronLookupCacheTime = Date.now();
        return callback({});
    }
    
    const squadronsDb = new sqlite3.Database(squadronsDbPath, sqlite3.OPEN_READONLY, (err) => {
        if (err) {
            log.error('Failed to open squadrons database', err);
            squadronLookupCache = {};
            squadronLookupCacheTime = Date.now();
            return callback({});
        }
        
        squadronsDb.all(`SELECT clan_id, long_name, short_name, tag_name, members, clanrating FROM squadrons_data`, [], (err, rows) => {
            if (err) {
                squadronsDb.close();
                log.error('Error loading squadron lookup data', err);
                squadronLookupCache = {};
                squadronLookupCacheTime = Date.now();
                return callback(squadronLookupCache);
            }

            const lookup = {};
            const addAlias = (key, row) => {
                if (key === undefined || key === null || key === '') return;
                lookup[key] = row;
                const lower = String(key).toLowerCase();
                if (!(lower in lookup)) {
                    lookup[lower] = row;
                }
            };
            rows.forEach(row => {
                if (row.long_name) addAlias(row.long_name, row);
                if (row.short_name) addAlias(row.short_name, row);
                if (row.tag_name) addAlias(row.tag_name, row);
                if (row.clan_id !== null && row.clan_id !== undefined) {
                    addAlias(String(row.clan_id), row);
                }
            });
            squadronLookupCache = lookup;
            squadronLookupCacheTime = Date.now();  // set immediately — prevents thundering herd of concurrent writes
            log.info('Squadron lookup cache updated', { entries: rows.length });

            // Detect renames: any (clan_id, name) pair we've never seen before
            // is recorded in squadron_name_history so old-name web URLs can
            // resolve to the canonical clan_id via 301 redirect, and so the
            // squadron profile can include pre-rename history. We track all
            // three name forms (long_name, short_name, tag_name) since
            // short tags rename more often than long names.
            const now = Math.floor(Date.now() / 1000);
            const historyRows = [];
            for (const r of rows) {
                if (r.clan_id == null) continue;
                if (r.long_name)  historyRows.push([r.clan_id, 'long',  r.long_name,  now, now]);
                if (r.short_name) historyRows.push([r.clan_id, 'short', r.short_name, now, now]);
                if (r.tag_name)   historyRows.push([r.clan_id, 'tag',   r.tag_name,   now, now]);
            }
            // Call callback immediately — the write below is best-effort name-history
            // tracking and must never block callers waiting for the lookup data.
            squadronsDb.close();
            callback(squadronLookupCache);

            if (historyRows.length) {
                // Background write: record rename history in squadrons.db.
                // Runs fully detached from the caller — errors are logged and swallowed.
                const writeDb = new sqlite3.Database(squadronsDbPath, sqlite3.OPEN_READWRITE, (werr) => {
                    if (werr) {
                        log.warn('squadron_name_history: could not open for writing', { error: werr.message });
                        return;
                    }

                    let finished = false;
                    const finalize = () => {
                        if (finished) return;
                        finished = true;
                        try { writeDb.close(); } catch (_) {}
                    };
                    writeDb.on('error', (e) => {
                        log.warn('squadron_name_history write failed (likely BUSY); skipping', { error: e.message });
                        finalize();
                    });

                    writeDb.serialize(() => {
                        writeDb.run('PRAGMA busy_timeout = 5000', () => {});

                        writeDb.run(
                            `CREATE TABLE IF NOT EXISTS squadron_name_history (
                                clan_id    INTEGER NOT NULL,
                                long_name  TEXT NOT NULL,
                                first_seen INTEGER NOT NULL,
                                last_seen  INTEGER NOT NULL,
                                PRIMARY KEY (clan_id, long_name)
                            )`,
                            () => {}
                        );
                        writeDb.run(`CREATE INDEX IF NOT EXISTS idx_snh_long_name ON squadron_name_history(long_name COLLATE NOCASE)`, () => {});
                        writeDb.run(
                            `CREATE TABLE IF NOT EXISTS squadron_name_aliases (
                                clan_id    INTEGER NOT NULL,
                                kind       TEXT NOT NULL,
                                name       TEXT NOT NULL,
                                first_seen INTEGER NOT NULL,
                                last_seen  INTEGER NOT NULL,
                                PRIMARY KEY (clan_id, kind, name)
                            )`,
                            () => {}
                        );
                        writeDb.run(`CREATE INDEX IF NOT EXISTS idx_sna_name ON squadron_name_aliases(name COLLATE NOCASE)`, () => {});
                        writeDb.run(`CREATE INDEX IF NOT EXISTS idx_sna_clanid ON squadron_name_aliases(clan_id)`, () => {});

                        const longStmt = writeDb.prepare(
                            `INSERT INTO squadron_name_history (clan_id, long_name, first_seen, last_seen)
                             VALUES (?, ?, ?, ?)
                             ON CONFLICT(clan_id, long_name) DO UPDATE SET last_seen = excluded.last_seen`
                        );
                        for (const r of rows) {
                            if (r.clan_id != null && r.long_name) {
                                longStmt.run([r.clan_id, r.long_name, now, now], () => {});
                            }
                        }
                        longStmt.finalize(() => {});

                        const aliasStmt = writeDb.prepare(
                            `INSERT INTO squadron_name_aliases (clan_id, kind, name, first_seen, last_seen)
                             VALUES (?, ?, ?, ?, ?)
                             ON CONFLICT(clan_id, kind, name) DO UPDATE SET last_seen = excluded.last_seen`
                        );
                        historyRows.forEach(args => aliasStmt.run(args, () => {}));
                        aliasStmt.finalize(() => { finalize(); });
                    });
                });
            }
        });
    });
}

function loadNickLookupCached(callback) {
    if (nickLookupCache && Date.now() - nickLookupCacheTime < SQUADRON_CACHE_TTL) {
        return callback(nickLookupCache);
    }

    const squadronsDbPath = path.join(STORAGE_ROOT, 'squadrons.db');
    if (!fs.existsSync(squadronsDbPath)) {
        nickLookupCache = {};
        nickLookupCacheTime = Date.now();
        return callback({});
    }

    const squadronsDb = new sqlite3.Database(squadronsDbPath, sqlite3.OPEN_READONLY, (err) => {
        if (err) {
            log.error('Failed to open squadrons database for nick lookup', err);
            nickLookupCache = {};
            nickLookupCacheTime = Date.now();
            return callback({});
        }

        squadronsDb.all(`
            SELECT sm.uid, sm.nick, sm.clan_id AS sm_clan_id, sd.clan_id AS sd_clan_id, sd.tag_name, sd.short_name
            FROM squadron_members sm
            LEFT JOIN squadrons_data sd ON sm.clan_id = sd.clan_id
        `, [], (err, rows) => {
            squadronsDb.close();

            if (err) {
                log.error('Error loading nick lookup data', err);
                nickLookupCache = {};
            } else {
                const lookup = {};
                rows.forEach(row => {
                    lookup[row.uid] = {
                        nick: row.nick,
                        clan_id: row.sd_clan_id || row.sm_clan_id || null,
                        tag_name: row.tag_name,
                        short_name: row.short_name
                    };
                });
                nickLookupCache = lookup;
                log.info('Nick lookup cache updated', { entries: rows.length });
            }

            nickLookupCacheTime = Date.now();
            callback(nickLookupCache);
        });
    });
}

function dbAll(query, params = []) {
    return new Promise((resolve, reject) => {
        db.all(query, params, (err, rows) => err ? reject(err) : resolve(rows || []));
    });
}

function dbAllHeavy(query, params = []) {
    return new Promise((resolve, reject) => {
        heavyDb.all(query, params, (err, rows) => err ? reject(err) : resolve(rows || []));
    });
}

function dbGet(query, params = []) {
    return new Promise((resolve, reject) => {
        db.get(query, params, (err, row) => err ? reject(err) : resolve(row || null));
    });
}

function dateFilterParams(dateFilters) {
    return [
        dateFilters.startTimestamp, dateFilters.startTimestamp,
        dateFilters.endTimestamp, dateFilters.endTimestamp
    ];
}

function buildDateClause(alias, dateFilters) {
    const prefix = alias ? `${alias}.` : '';
    const parts = [];
    const params = [];
    if (dateFilters.startTimestamp !== null && dateFilters.startTimestamp !== undefined) {
        parts.push(`${prefix}endtime_unix >= ?`);
        params.push(dateFilters.startTimestamp);
    }
    if (dateFilters.endTimestamp !== null && dateFilters.endTimestamp !== undefined) {
        parts.push(`${prefix}endtime_unix <= ?`);
        params.push(dateFilters.endTimestamp);
    }
    return {
        clause: parts.length ? ` AND ${parts.join(' AND ')}` : '',
        params
    };
}

function performanceBenchmarkKey(dateFilters) {
    return `${dateFilters.startTimestamp || 'all'}_${dateFilters.endTimestamp || 'all'}`;
}

function clamp(value, min, max) {
    return Math.max(min, Math.min(max, value));
}

function normalizeRatingStats(row) {
    const games = Math.max(0, Number(row && (row.games ?? row.total_battles)) || 0);
    const kills = Math.max(0, Number(row && row.total_kills) || 0);
    const deaths = Math.max(0, Number(row && (row.total_deaths ?? row.deaths)) || 0);
    const assists = Math.max(0, Number(row && (row.total_assists ?? row.assists)) || 0);
    const captures = Math.max(0, Number(row && (row.total_captures ?? row.captures)) || 0);
    const wins = Math.max(0, Number(row && row.wins) || 0);
    const heavyweight = Math.max(0, Number(row && row.heavy_score) || 0);
    return {
        games,
        kdr: deaths > 0 ? kills / deaths : kills,
        kills_per_game: games > 0 ? kills / games : 0,
        heavy_rate: games > 0 ? heavyweight / games : 0,
        win_rate: games > 0 ? (wins / games) * 100 : 0,
        assists_per_game: games > 0 ? assists / games : 0,
        captures_per_game: games > 0 ? captures / games : 0,
        deaths_per_game: games > 0 ? deaths / games : 0,
        games_log: Math.log1p(games)
    };
}

function sortedMetric(values, metric) {
    return values.map(value => Number(value[metric]) || 0).sort((a, b) => a - b);
}

function buildRatingBenchmark(rows) {
    const normalized = (rows || []).map(normalizeRatingStats);
    return {
        metrics: {
            kdr: sortedMetric(normalized, 'kdr'),
            kills_per_game: sortedMetric(normalized, 'kills_per_game'),
            heavy_rate: sortedMetric(normalized, 'heavy_rate'),
            win_rate: sortedMetric(normalized, 'win_rate'),
            assists_per_game: sortedMetric(normalized, 'assists_per_game'),
            captures_per_game: sortedMetric(normalized, 'captures_per_game'),
            deaths_per_game: sortedMetric(normalized, 'deaths_per_game'),
            games_log: sortedMetric(normalized, 'games_log')
        }
    };
}

function resolveSquadronRatingKey(name, squadronLookup) {
    if (!name) return null;
    const resolved = resolveSquadronIdentity(null, { squadron_name: name }, squadronLookup);
    if (resolved && resolved.clan_id) {
        return `clan:${resolved.clan_id}`;
    }
    const fallbackName = resolved?.tag_name || resolved?.short_name || name;
    return `name:${String(fallbackName).toLowerCase()}`;
}

function aggregateSquadronBenchmarkRows(rows, squadronLookup) {
    const grouped = new Map();
    for (const row of rows || []) {
        const ratingName = row?.squadron_name || row?.entity_key;
        const key = resolveSquadronRatingKey(ratingName, squadronLookup);
        if (!key) continue;
        if (!grouped.has(key)) {
            grouped.set(key, {
                clan_id: null,
                squadron_name: ratingName || null,
                games: 0,
                total_kills: 0,
                total_assists: 0,
                total_captures: 0,
                total_deaths: 0,
                wins: 0,
                heavy_score: 0
            });
        }
        const acc = grouped.get(key);
        const resolved = resolveSquadronIdentity(null, { squadron_name: ratingName }, squadronLookup);
        if (!acc.clan_id && resolved?.clan_id) {
            acc.clan_id = resolved.clan_id;
        }
        acc.games += Number(row.games) || Number(row.total_battles) || 0;
        acc.total_kills += Number(row.total_kills) || 0;
        acc.total_assists += Number(row.total_assists) || 0;
        acc.total_captures += Number(row.total_captures) || 0;
        acc.total_deaths += Number(row.total_deaths) || 0;
        acc.wins += Number(row.wins) || 0;
        acc.heavy_score += Number(row.heavy_score) || 0;
    }
    return Array.from(grouped.values());
}

function upperBound(values, value) {
    let lo = 0;
    let hi = values.length;
    while (lo < hi) {
        const mid = Math.floor((lo + hi) / 2);
        if (values[mid] <= value) lo = mid + 1;
        else hi = mid;
    }
    return lo;
}

function lowerBound(values, value) {
    let lo = 0;
    let hi = values.length;
    while (lo < hi) {
        const mid = Math.floor((lo + hi) / 2);
        if (values[mid] < value) lo = mid + 1;
        else hi = mid;
    }
    return lo;
}

function metricPercentile(value, values, lowerIsBetter = false) {
    if (!Array.isArray(values) || !values.length) return 0;
    if (lowerIsBetter) {
        return (values.length - lowerBound(values, value)) / values.length;
    }
    return upperBound(values, value) / values.length;
}

function computePerformanceScore(row, benchmarkSet) {
    const stats = normalizeRatingStats(row);
    const metrics = (benchmarkSet && benchmarkSet.metrics) || {};
    const weighted =
        (0.32 * metricPercentile(stats.kdr, metrics.kdr)) +
        (0.23 * metricPercentile(stats.heavy_rate, metrics.heavy_rate)) +
        (0.14 * metricPercentile(stats.kills_per_game, metrics.kills_per_game)) +
        (0.10 * metricPercentile(stats.games_log, metrics.games_log)) +
        (0.06 * metricPercentile(stats.win_rate, metrics.win_rate)) +
        (0.06 * metricPercentile(stats.assists_per_game, metrics.assists_per_game)) +
        (0.04 * metricPercentile(stats.captures_per_game, metrics.captures_per_game)) +
        (0.05 * metricPercentile(stats.deaths_per_game, metrics.deaths_per_game, true));
    const sample = clamp(Math.sqrt(stats.games / 10), 0, 1);
    return Number(clamp(5 * weighted * sample, 0, 5).toFixed(2));
}

function ratingCtes(dateFilters, entityExpression, targetSessionsCte = '') {
    const dateClause = buildDateClause('p', dateFilters).clause;
    return `
        WITH ${targetSessionsCte ? `${targetSessionsCte},` : ''}
        player_sessions AS (
            SELECT
                ${entityExpression} AS entity_key,
                p.UID,
                p.session_id,
                SUM(p.ground_kills + p.air_kills) AS kills,
                SUM(p.assists) AS assists,
                SUM(p.captures) AS captures,
                SUM(p.deaths) AS deaths,
                MAX(CASE WHEN UPPER(p.victor_bool) = 'WIN' THEN 1 ELSE 0 END) AS win
            FROM player_games_hist p
            ${targetSessionsCte ? 'JOIN target_sessions ts ON ts.session_id = p.session_id' : ''}
            WHERE p.UID IS NOT NULL
                AND p.nick NOT LIKE 'coop/%'
                ${dateClause}
            GROUP BY entity_key, p.UID, p.session_id
        ),
        leader_stats AS (
            SELECT ps.session_id, MAX(ps.kills) AS max_kills
            FROM player_sessions ps
            GROUP BY ps.session_id
        ),
        leader_counts AS (
            SELECT ps.session_id, COUNT(*) AS top_count
            FROM player_sessions ps
            JOIN leader_stats ls ON ls.session_id = ps.session_id AND ls.max_kills = ps.kills
            GROUP BY ps.session_id
        ),
        second_stats AS (
            SELECT ps.session_id, MAX(ps.kills) AS second_kills
            FROM player_sessions ps
            JOIN leader_stats ls ON ls.session_id = ps.session_id
            WHERE ps.kills < ls.max_kills
            GROUP BY ps.session_id
        ),
        scored_sessions AS (
            SELECT
                ps.*,
                CASE
                    WHEN ps.kills >= 3
                        AND ps.kills = ls.max_kills
                        AND lc.top_count = 1
                        AND (ps.kills - COALESCE(ss.second_kills, 0)) >= 2
                        THEN MIN(1.0, (ps.kills - 2) / 2.0)
                    WHEN ps.kills >= 3
                        AND ps.kills = ls.max_kills
                        AND lc.top_count = 1
                        THEN 0.6 * MIN(1.0, (ps.kills - 2) / 2.0)
                    ELSE 0
                END AS heavy_score
            FROM player_sessions ps
            JOIN leader_stats ls ON ls.session_id = ps.session_id
            JOIN leader_counts lc ON lc.session_id = ps.session_id
            LEFT JOIN second_stats ss ON ss.session_id = ps.session_id
        )
    `;
}

function playerBenchmarkQuery(dateFilters) {
    const dateClause = buildDateClause('p', dateFilters);
    return {
        query: `
        ${ratingCtes(dateFilters, 'p.UID')}
        SELECT
            entity_key,
            entity_key AS squadron_name,
            COUNT(*) AS games,
            SUM(kills) AS total_kills,
            SUM(assists) AS total_assists,
            SUM(captures) AS total_captures,
            SUM(deaths) AS total_deaths,
            SUM(win) AS wins,
            SUM(heavy_score) AS heavy_score
        FROM scored_sessions
        GROUP BY entity_key
        HAVING COUNT(*) >= 10
        `,
        params: dateClause.params
    };
}

function squadronBenchmarkQuery(dateFilters) {
    const dateClause = buildDateClause('p', dateFilters);
    return {
        query: `
        ${ratingCtes(dateFilters, 'p.squadron_name')},
        squadron_sessions AS (
            SELECT
                entity_key,
                session_id,
                SUM(kills) AS kills,
                SUM(assists) AS assists,
                SUM(captures) AS captures,
                SUM(deaths) AS deaths,
                MAX(win) AS win,
                SUM(heavy_score) AS heavy_score
            FROM scored_sessions
            WHERE entity_key IS NOT NULL AND entity_key != 'UNKNOWN'
            GROUP BY entity_key, session_id
        )
        SELECT
            entity_key,
            COUNT(*) AS games,
            SUM(kills) AS total_kills,
            SUM(assists) AS total_assists,
            SUM(captures) AS total_captures,
            SUM(deaths) AS total_deaths,
            SUM(win) AS wins,
            SUM(heavy_score) AS heavy_score
        FROM squadron_sessions
        GROUP BY entity_key
        HAVING COUNT(*) >= 10
        `,
        params: dateClause.params
    };
}

function queryPlayerRatingStats(uid, dateFilters) {
    const sessionDate = buildDateClause('src', dateFilters);
    const playerDate = buildDateClause('p', dateFilters);
    const targetSessionsCte = `
        target_sessions AS (
            SELECT DISTINCT session_id
            FROM player_games_hist src
            WHERE src.UID = ?
                AND src.nick NOT LIKE 'coop/%'
                ${sessionDate.clause}
        )
    `;
    const query = `
        ${ratingCtes(dateFilters, 'p.UID', targetSessionsCte)}
        SELECT
            entity_key,
            COUNT(*) AS games,
            SUM(kills) AS total_kills,
            SUM(assists) AS total_assists,
            SUM(captures) AS total_captures,
            SUM(deaths) AS total_deaths,
            SUM(win) AS wins,
            SUM(heavy_score) AS heavy_score
        FROM scored_sessions
        WHERE entity_key = ?
        GROUP BY entity_key
    `;
    return dbGet(query, [uid, ...sessionDate.params, ...playerDate.params, uid]).catch(err => {
        log.error('Failed to query player ELO stats', err, { uid });
        return null;
    });
}

function queryPlayerRatingStatsForUids(uids, dateFilters) {
    const ids = [...new Set((uids || []).map(uid => String(uid)).filter(Boolean))];
    if (!ids.length) return Promise.resolve(new Map());
    const placeholders = ids.map(() => '?').join(',');
    const sessionDate = buildDateClause('src', dateFilters);
    const playerDate = buildDateClause('p', dateFilters);
    const targetSessionsCte = `
        target_sessions AS (
            SELECT DISTINCT session_id
            FROM player_games_hist src
            WHERE src.UID IN (${placeholders})
                AND src.nick NOT LIKE 'coop/%'
                ${sessionDate.clause}
        )
    `;
    const query = `
        ${ratingCtes(dateFilters, 'p.UID', targetSessionsCte)}
        SELECT
            entity_key,
            COUNT(*) AS games,
            SUM(kills) AS total_kills,
            SUM(assists) AS total_assists,
            SUM(captures) AS total_captures,
            SUM(deaths) AS total_deaths,
            SUM(win) AS wins,
            SUM(heavy_score) AS heavy_score
        FROM scored_sessions
        WHERE entity_key IN (${placeholders})
        GROUP BY entity_key
    `;
    return dbAll(query, [...ids, ...sessionDate.params, ...playerDate.params, ...ids])
        .then(rows => {
            const map = new Map();
            rows.forEach(row => map.set(String(row.entity_key), row));
            return map;
        })
        .catch(err => {
            log.error('Failed to query roster player ELO stats', err, { count: ids.length });
            return new Map();
        });
}

function querySquadronRatingStats(variants, dateFilters, clanId = null) {
    const names = [...new Set((variants || []).filter(Boolean))];
    if (!names.length) return Promise.resolve(null);
    const placeholders = names.map(() => '?').join(',');
    const sessionDate = buildDateClause('src', dateFilters);
    const playerDate = buildDateClause('p', dateFilters);
    const targetSessionsCte = `
        target_sessions AS (
            SELECT DISTINCT session_id
            FROM player_games_hist src
            WHERE src.squadron_name IN (${placeholders})
                AND src.nick NOT LIKE 'coop/%'
                ${sessionDate.clause}
        )
    `;
    const query = `
        ${ratingCtes(dateFilters, 'p.squadron_name', targetSessionsCte)},
        squadron_sessions AS (
            SELECT
                entity_key,
                session_id,
                SUM(kills) AS kills,
                SUM(assists) AS assists,
                SUM(captures) AS captures,
                SUM(deaths) AS deaths,
                MAX(win) AS win,
                SUM(heavy_score) AS heavy_score
            FROM scored_sessions
            WHERE entity_key IN (${placeholders})
            GROUP BY entity_key, session_id
        )
        SELECT
            'squadron' AS entity_key,
            COUNT(*) AS games,
            SUM(kills) AS total_kills,
            SUM(assists) AS total_assists,
            SUM(captures) AS total_captures,
            SUM(deaths) AS total_deaths,
            SUM(win) AS wins,
            SUM(heavy_score) AS heavy_score
        FROM squadron_sessions
    `;
    return dbGet(query, [...names, ...sessionDate.params, ...playerDate.params]).then(row => {
        if (!row) return null;
        return {
            ...row,
            clan_id: clanId
        };
    }).catch(err => {
        log.error('Failed to query squadron ELO stats', err, { variants: names });
        return null;
    });
}

function loadPerformanceBenchmarksCached(dateFilters, callback) {
    const key = performanceBenchmarkKey(dateFilters);
    const cached = performanceBenchmarkCache.get(key);
    const isFresh = cached && Date.now() - cached.timestamp < PERFORMANCE_BENCHMARK_CACHE_TTL;

    if (isFresh) {
        return callback(cached.data);
    }

    // Stale-while-revalidate: return immediately (stale or empty), refresh in background.
    // The all-time benchmark query takes ~47s and must not block response time.
    const emptyBenchmarks = { players: buildRatingBenchmark([]), squadrons: buildRatingBenchmark([]) };
    callback(cached ? cached.data : emptyBenchmarks);

    if (performanceBenchmarkInFlight.has(key)) return;

    const loadSquadronLookupPromise = () => new Promise(resolve => loadSquadronLookupCached(resolve));

    const promise = Promise.all([
        loadSquadronLookupPromise(),
        (() => {
            const benchmarkQuery = playerBenchmarkQuery(dateFilters);
            return dbAllHeavy(benchmarkQuery.query, benchmarkQuery.params).catch(err => {
                log.error('Failed to load player ELO benchmarks', err);
                return [];
            });
        })(),
        (() => {
            const benchmarkQuery = squadronBenchmarkQuery(dateFilters);
            return dbAllHeavy(benchmarkQuery.query, benchmarkQuery.params).catch(err => {
                log.error('Failed to load squadron ELO benchmarks', err);
                return [];
            });
        })()
    ]).then(([squadronLookup, playerRows, squadronRows]) => {
        const data = {
            players: buildRatingBenchmark(playerRows),
            squadrons: buildRatingBenchmark(aggregateSquadronBenchmarkRows(squadronRows, squadronLookup))
        };
        performanceBenchmarkCache.set(key, { data, timestamp: Date.now() });
        if (performanceBenchmarkCache.size > 12) {
            performanceBenchmarkCache.delete(performanceBenchmarkCache.keys().next().value);
        }
        performanceBenchmarkInFlight.delete(key);
        log.info('Performance benchmarks refreshed in background', { key });
        return data;
    }).catch(err => {
        performanceBenchmarkInFlight.delete(key);
        log.error('Failed to load ELO benchmarks', err);
    });

    performanceBenchmarkInFlight.set(key, promise);
}

function normalizeVehicleName(name) {
    if (!name) return name;
    // Keep visible decoration glyphs (◢ ▄ ◊ ␗ etc.) — those are country /
    // event indicators and the website is expected to display them. Only strip
    // the Private Use Area, where older WT variants stored sprite refs that
    // render as tofu in any browser font.
    return name.replace(/[\uE000-\uF8FF]/g, '').trim();
}

function normalizeQueryList(value) {
    if (value === undefined || value === null) return [];
    const rawValues = Array.isArray(value) ? value : [value];
    return rawValues
        .flatMap(item => String(item).split(','))
        .map(item => item.trim())
        .filter(Boolean);
}

// ─── Vehicle meta cache ────────────────────────────────────────────────
// Loaded from BOT/utils.py-generated cache files. `vehicle_data_cache.json`
// is an array of [cdk, english_name, icon, tags{}] entries; we use it for
// type abbreviations (T/F/B/AA/L/H) and English fallback names.

// Tags pulled directly from unittags.blk via vehicle_data_cache.json. Helicopters
// in particular can show up under type_attack_helicopter / type_utility_helicopter,
// not the generic type_helicopter the bot's data_parser injects, so we list every
// concrete type tag we've observed in the cache.
const TAG_TO_ABBREV = {
    // SPAA
    type_spaa:                'AA',
    // Light tanks
    type_light_tank:          'L',
    // Tanks (heavy/medium/TD/missile)
    type_heavy_tank:          'T',
    type_medium_tank:         'T',
    type_tank_destroyer:      'T',
    type_missile_tank:        'T',
    type_football_tank:       'T',
    type_assault:             'T',
    tank:                     'T',
    // Fighters (incl. interceptors / strike)
    type_fighter:             'F',
    type_jet_fighter:         'F',
    type_interceptor:         'F',
    type_aa_fighter:          'F',
    type_strike_aircraft:     'F',
    type_strike_ucav:         'F',
    // Bombers
    type_bomber:              'B',
    type_jet_bomber:          'B',
    type_dive_bomber:         'B',
    type_light_bomber:        'B',
    type_frontline_bomber:    'B',
    type_longrange_bomber:    'B',
    // Helicopters
    type_attack_helicopter:   'H',
    type_utility_helicopter:  'H',
    type_helicopter:          'H',
    helicopter:               'H',
};

let _vehicleMetaCache = null;        // cdk (lower) -> { name, type, cdk }

function loadVehicleMetaCache() {
    if (_vehicleMetaCache) return _vehicleMetaCache;
    const cachePath = path.join(STORAGE_ROOT, 'CACHE', 'vehicle_data_cache_all.json');
    const fallback  = path.join(STORAGE_ROOT, 'CACHE', 'vehicle_data_cache.json');
    const target = fs.existsSync(cachePath) ? cachePath : (fs.existsSync(fallback) ? fallback : null);
    if (!target) {
        log.error(`Vehicle meta cache not found at ${cachePath} or ${fallback}`);
        _vehicleMetaCache = new Map();
        return _vehicleMetaCache;
    }
    try {
        const raw = JSON.parse(fs.readFileSync(target, 'utf-8'));
        const map = new Map();
        for (const entry of raw) {
            if (!Array.isArray(entry) || entry.length < 4) continue;
            const cdk = entry[0];
            const name = entry[1];
            const tags = entry[3] || {};
            // Best match: prefer specific type_* tags over generic ones.
            let abbrev = '?';
            let fallbackAbbrev = null;
            for (const tag of Object.keys(tags)) {
                if (!(tag in TAG_TO_ABBREV)) continue;
                if (tag.startsWith('type_')) { abbrev = TAG_TO_ABBREV[tag]; break; }
                if (fallbackAbbrev == null) fallbackAbbrev = TAG_TO_ABBREV[tag];
            }
            if (abbrev === '?' && fallbackAbbrev) abbrev = fallbackAbbrev;
            map.set(String(cdk).toLowerCase(), { name, type: abbrev, cdk });
        }
        _vehicleMetaCache = map;
        log.info(`Loaded vehicle meta for ${map.size} vehicles from ${path.basename(target)}`);
    } catch (e) {
        log.error('Failed to load vehicle meta cache', e);
        _vehicleMetaCache = new Map();
    }
    return _vehicleMetaCache;
}

function getVehicleType(internal) {
    if (!internal) return '?';
    const map = loadVehicleMetaCache();
    const hit = map.get(String(internal).toLowerCase());
    return hit ? hit.type : '?';
}

// Same order the bot's /comp uses so notation matches across surfaces.
const COMP_TYPE_ORDER = ['F', 'B', 'H', 'L', 'T', 'AA', '?'];

function buildCompNotation(typeCounts) {
    const parts = [];
    for (const code of COMP_TYPE_ORDER) {
        const n = typeCounts[code] || 0;
        if (n > 0) parts.push(`${n}${code}`);
    }
    return parts.join(' ');
}

// Canonicalize a player's squadron identity using the squadrons_data lookup.
// Prefers clan_id from the squadron_members cache (joined to squadrons_data); otherwise
// falls back to a string lookup against long_name / short_name / tag_name.
// Always returns the canonical tag_name + short_name for the matched clan_id when possible,
// so consumers can group/dedupe by clan_id (or short_name) regardless of which raw value was stored.
function resolveSquadronIdentity(cached, fallback, squadronLookup) {
    let clanId = null;
    let tagName = null;
    let shortName = null;

    if (cached && cached.clan_id) {
        clanId = cached.clan_id;
        tagName = cached.tag_name || null;
        shortName = cached.short_name || null;
    }

    const rawTag = tagName || (cached ? cached.tag_name : null) || (fallback ? fallback.squadron_name : null);
    if ((!clanId || !tagName || !shortName) && rawTag && squadronLookup) {
        const sq = squadronLookup[rawTag];
        if (sq) {
            clanId = clanId || sq.clan_id || null;
            tagName = sq.tag_name || tagName || rawTag;
            shortName = sq.short_name || shortName || null;
        } else if (!tagName) {
            tagName = rawTag;
        }
    }

    return { clan_id: clanId, tag_name: tagName, short_name: shortName };
}

function formatSquadronResolution(input, row, resolvedBy) {
    if (!row) {
        return {
            input,
            resolved: false,
            resolved_by: resolvedBy,
            clan_id: null,
            short_name: input,
            tag_name: input,
            long_name: '<unresolved>',
        };
    }

    return {
        input,
        resolved: true,
        resolved_by: resolvedBy,
        clan_id: row.clan_id ?? null,
        short_name: row.short_name || input,
        tag_name: row.tag_name || row.short_name || input,
        long_name: row.long_name || '<unresolved>',
    };
}

function buildSquadronIdentity(team, lookup) {
    const keys = [
        team?.squadron,
        team?.squadron_tagged,
        team?.squadron_long,
    ].filter(Boolean);

    let hit = null;
    for (const key of keys) {
        hit = lookup[key] || lookup[String(key).trim()] || lookup[String(key).trim().toLowerCase()] || lookup[String(key).trim().toUpperCase()];
        if (hit) break;
    }

    const clanId = hit?.clan_id ?? null;
    return {
        clan_id: clanId,
        short_name: hit?.short_name || team?.squadron || '',
        tag_name: hit?.tag_name || team?.squadron_tagged || team?.squadron || '',
        long_name: hit?.long_name || team?.squadron_long || team?.squadron || '',
    };
}

function jsonError(res, status, message, extra = {}) {
    return res.status(status).json({ error: message, ...extra });
}

function normalizeUid(value) {
    if (value === null || value === undefined || value === '') return null;
    return String(value);
}

function normalizeClanId(value) {
    if (value === null || value === undefined || value === '') return null;
    const parsed = Number(value);
    return Number.isFinite(parsed) ? parsed : null;
}

function cleanText(value) {
    if (value === null || value === undefined) return null;
    const text = String(value).trim();
    return text || null;
}

function resolveCurrentSquadronIdentity(latestRow, squadronLookup, preferredClanId = null) {
    const effectiveClanId = preferredClanId != null
        ? Number(preferredClanId)
        : normalizeClanId(latestRow?.clan_id);
    const lookupByClanId = effectiveClanId != null
        ? squadronLookup[`__cid_${effectiveClanId}`] || squadronLookup[String(effectiveClanId)]
        : null;
    const lookup = lookupByClanId || squadronLookup[latestRow?.squadron_name];
    const squadronName = cleanText(lookup?.tag_name) || cleanText(latestRow?.squadron_name) || '';
    const squadronLongName = cleanText(lookup?.long_name) || cleanText(latestRow?.squadron_name) || '';
    const currentNames = new Set();
    [
        latestRow?.squadron_name,
        lookup?.long_name,
        lookup?.short_name,
        lookup?.tag_name,
    ].forEach((name) => {
        const cleaned = cleanText(name);
        if (cleaned) currentNames.add(cleaned.toLowerCase());
    });
    return {
        squadron_name: squadronName,
        squadron_long_name: squadronLongName,
        squadron_clan_id: lookup?.clan_id != null ? Number(lookup.clan_id) : effectiveClanId,
        current_names: currentNames,
    };
}

async function loadPlayerIdentitySnapshot(uid, squadronLookup, options = {}) {
    const latestRow = options.latestRow || await dbGetAsync(
        db,
        `SELECT nick, squadron_name, clan_id
         FROM player_games_hist
         WHERE UID = ? AND nick NOT LIKE 'coop/%'
         ORDER BY session_id DESC
         LIMIT 1`,
        [uid]
    );
    if (!latestRow) return null;

    const currentIdentity = resolveCurrentSquadronIdentity(latestRow, squadronLookup, options.preferredClanId ?? null);
    const [nickRows, squadronRows] = await Promise.all([
        dbAllAsync(
            db,
            // Pre-2026-01-19 the Spectra API returned auto-generated
            // placeholder nicks (e.g. "Dietrich3657") for newly-discovered
            // profiles before Gaijin's backend resolved them. Those rows
            // pollute previous_nicks with hundreds of garbage entries per
            // affected UID, so drop any placeholder-shaped nick whose battle
            // ended before that cutoff. Post-cutoff rows are trusted as-is.
            `SELECT nick, MAX(endtime_unix) AS last_seen
             FROM player_games_hist
             WHERE UID = ?
                 AND nick IS NOT NULL
                 AND nick <> ''
                 AND nick NOT LIKE 'coop/%'
                 AND NOT (
                     endtime_unix < 1768780800
                     AND nick GLOB '[A-Z][a-z]*[0-9]'
                     AND nick NOT GLOB '*[^A-Za-z0-9]*'
                     AND LENGTH(nick) BETWEEN 5 AND 18
                 )
             GROUP BY nick
             ORDER BY last_seen DESC, nick`,
            [uid]
        ),
        dbAllAsync(
            db,
            `SELECT squadron_name, clan_id, MAX(endtime_unix) AS last_seen
             FROM player_games_hist
             WHERE UID = ?
                 AND squadron_name IS NOT NULL
                 AND squadron_name <> ''
                 AND squadron_name <> 'UNKNOWN'
             GROUP BY squadron_name, clan_id
             ORDER BY last_seen DESC, squadron_name`,
            [uid]
        ),
    ]);

    const currentNickKey = cleanText(latestRow.nick)?.toLowerCase() || null;
    const seenNicks = new Set();
    const previous_nicks = [];
    for (const row of nickRows) {
        const nick = cleanText(row.nick);
        if (!nick) continue;
        const key = nick.toLowerCase();
        if (key === currentNickKey || seenNicks.has(key)) continue;
        seenNicks.add(key);
        previous_nicks.push(nick);
    }

    const seenSquadrons = new Set();
    const previous_squadron_names = [];
    for (const row of squadronRows) {
        const lookup = (row.clan_id != null && squadronLookup[`__cid_${row.clan_id}`])
            || squadronLookup[row.squadron_name];
        const displayName = cleanText(lookup?.tag_name) || cleanText(row.squadron_name);
        if (!displayName) continue;
        const key = displayName.toLowerCase();
        if (currentIdentity.current_names.has(key) || seenSquadrons.has(key)) continue;
        seenSquadrons.add(key);
        previous_squadron_names.push(displayName);
    }

    return {
        uid: normalizeUid(uid),
        nick: latestRow.nick,
        previous_nicks,
        squadron_name: currentIdentity.squadron_name,
        squadron_long_name: currentIdentity.squadron_long_name,
        squadron_clan_id: currentIdentity.squadron_clan_id,
        previous_squadron_names,
    };
}

function normalizeMatchPlayer(player) {
    if (!player || typeof player !== 'object') return null;
    const vehicleInternal = cleanText(player.vehicle_internal) || cleanText(player.vehicle);
    const vehicleDisplay = cleanText(player.vehicle_new) || cleanText(player.vehicle);
    const normalized = {
        uid: normalizeUid(player.uid),
        nick: cleanText(player.nick) || '',
        fake_nick: player.fake_nick ?? null,
        vehicle_internal: vehicleInternal,
        vehicle: vehicleDisplay ? normalizeVehicleName(vehicleDisplay) : vehicleInternal,
        air_kills: Number(player.air_kills || 0),
        ground_kills: Number(player.ground_kills || 0),
        assists: Number(player.assists || 0),
        deaths: Number(player.deaths || 0),
        captures: Number(player.captures || 0),
        score: Number(player.score || 0),
    };
    if (player.spawn_order !== undefined) normalized.spawn_order = player.spawn_order;
    return normalized;
}

function normalizeMatchTeam(team, lookup, fallback = {}) {
    if (!team || typeof team !== 'object') return null;
    const seeded = {
        ...team,
        squadron: team.squadron ?? fallback.squadron ?? null,
        squadron_tagged: team.squadron_tagged ?? fallback.squadron_tagged ?? null,
        squadron_long: team.squadron_long ?? fallback.squadron_long ?? null,
        clan_id: team.clan_id ?? fallback.clan_id ?? null,
    };
    const identity = buildSquadronIdentity(seeded, lookup);
    const players = Array.isArray(team.players)
        ? team.players.map(normalizeMatchPlayer).filter(Boolean)
        : [];
    return {
        team_index: team.team_index ?? null,
        clan_id: normalizeClanId(identity.clan_id),
        squadron: cleanText(identity.short_name) || cleanText(seeded.squadron) || '',
        squadron_tagged: cleanText(identity.tag_name) || cleanText(seeded.squadron_tagged) || cleanText(seeded.squadron) || '',
        squadron_long: cleanText(identity.long_name),
        players,
    };
}

function dbAllAsync(database, sql, params = []) {
    return new Promise((resolve, reject) => {
        database.all(sql, params, (err, rows) => {
            if (err) return reject(err);
            resolve(rows || []);
        });
    });
}

function dbGetAsync(database, sql, params = []) {
    return new Promise((resolve, reject) => {
        database.get(sql, params, (err, row) => {
            if (err) return reject(err);
            resolve(row || null);
        });
    });
}

function readReplayJson(sessionId) {
    const replayPath = replayDataPath(sessionId);
    if (!fs.existsSync(replayPath)) {
        return null;
    }
    try {
        return JSON.parse(zlib.gunzipSync(fs.readFileSync(replayPath)));
    } catch (err) {
        log.warn('Failed to read replay JSON', { sessionId, error: err.message });
        return null;
    }
}

function normalizeCompPlayers(players) {
    const order = { F: 0, B: 1, H: 2, L: 3, T: 4, AA: 5, '?': 6 };
    return [...(players || [])].sort((a, b) => {
        const av = getVehicleType(a?.vehicle_internal);
        const bv = getVehicleType(b?.vehicle_internal);
        return (order[av] ?? 99) - (order[bv] ?? 99) || String(a?.nick || '').localeCompare(String(b?.nick || ''));
    }).map(player => ({
        uid: normalizeUid(player?.uid ?? player?.UID),
        nick: cleanText(player?.nick) || '',
        vehicle_internal: cleanText(player?.vehicle_internal),
        vehicle: normalizeVehicleName(cleanText(player?.vehicle) || cleanText(player?.vehicle_internal) || ''),
    }));
}

async function loadRecentComps(squadronName, windowSeconds = 3600, limit = 10) {
    const squadFile = path.join(COMPS_PATH, `${String(squadronName).toUpperCase()}.json`);
    if (!fs.existsSync(squadFile)) {
        return null;
    }

    let compsData;
    try {
        compsData = JSON.parse(fs.readFileSync(squadFile, 'utf-8'));
    } catch (err) {
        throw new Error(`Failed to read comp file: ${err.message}`);
    }

    const now = Math.floor(Date.now() / 1000);
    const thresholdSeconds = Math.max(0, Number(windowSeconds) || 3600);
    const maxComps = Math.max(1, Math.min(50, Number(limit) || 10));

    const comps = Object.entries(compsData || {})
        .map(([compKey, comp]) => {
            const regTs = Number(comp?.reg || 0);
            const updTs = Number(comp?.upd || 0);
            const lastSeenTs = updTs || regTs;
            const ageSeconds = now - lastSeenTs;
            const typeCounts = (comp?.Players || []).reduce((acc, player) => {
                const code = getVehicleType(player?.vehicle_internal);
                acc[code] = (acc[code] || 0) + 1;
                return acc;
            }, {});
            const notation = COMP_TYPE_ORDER
                .map(code => {
                    const count = typeCounts[code] || 0;
                    return count > 0 ? `${count}${code}` : null;
                })
                .filter(Boolean)
                .join(' / ') || 'None';

            return {
                comp_key: compKey,
                reg: regTs,
                upd: updTs,
                last_seen: lastSeenTs,
                age_seconds: ageSeconds,
                players: normalizeCompPlayers(comp?.Players || []),
                notation,
            };
        })
        .filter(comp => comp.last_seen && comp.age_seconds <= thresholdSeconds)
        .sort((a, b) => (b.last_seen - a.last_seen) || (b.reg - a.reg))
        .slice(0, maxComps);

    return {
        squadron: String(squadronName).toUpperCase(),
        window_seconds: thresholdSeconds,
        limit: maxComps,
        total_available: Object.keys(compsData || {}).length,
        total_recent: comps.length,
        comps,
    };
}

async function loadScoreboardContext(sessionId) {
    const replay = readReplayJson(sessionId);
    const matchRow = await dbGetAsync(
        db,
        `SELECT session_id, map_name, endtime_unix, received_unix, winning_sq, losing_sq, winning_team_json, losing_team_json, game_type
         FROM match_summary
         WHERE session_id = ?`,
        [sessionId]
    );
    if (!replay && !matchRow) {
        return null;
    }

    let teams = Array.isArray(replay?.teams) ? replay.teams.slice(0, 2).map(team => ({ ...(team || {}) })) : [];
    if (!teams.length && matchRow) {
        try {
            const winningTeam = matchRow.winning_team_json ? parseJsonColumn(matchRow.winning_team_json) : null;
            const losingTeam = matchRow.losing_team_json ? parseJsonColumn(matchRow.losing_team_json) : null;
            teams = [winningTeam, losingTeam].filter(Boolean).map(team => ({ ...(team || {}) }));
        } catch (err) {
            log.warn('Failed to synthesize scoreboard teams from match_summary', { sessionId, error: err.message });
        }
    }
    const squadronIdentities = await new Promise((resolve) => {
        loadSquadronLookupCached((lookup) => {
            try {
                resolve(teams.map(team => buildSquadronIdentity(team, lookup)));
            } catch (err) {
                log.warn('Failed to resolve squadron lookup for scoreboard context', { sessionId, error: err.message });
                resolve(teams.map(team => ({
                    clan_id: null,
                    short_name: team?.squadron || '',
                    tag_name: team?.squadron_tagged || team?.squadron || '',
                    long_name: team?.squadron_long || team?.squadron || '',
                })));
            }
        });
    });

    const enrichedTeams = teams.map((team, idx) => {
        const identity = squadronIdentities[idx] || {
            clan_id: null,
            short_name: team?.squadron || '',
            tag_name: team?.squadron_tagged || team?.squadron || '',
            long_name: team?.squadron_long || team?.squadron || '',
        };
        return {
            ...(normalizeMatchTeam(team, {}, {
                squadron: identity.short_name,
                squadron_tagged: identity.tag_name,
                squadron_long: identity.long_name,
                clan_id: identity.clan_id,
            }) || {}),
            squadron_identity: identity,
        };
    });

    // Prefer clan_id (rename-stable); fall back to long_name text for orphans
    // whose clan_id wasn't backfilled.
    const teamClanIds = enrichedTeams
        .map(team => (team?.clan_id != null ? Number(team.clan_id) : null))
        .filter(cid => cid != null);
    const longNames = enrichedTeams.map(team => String(team?.squadron_long || '').trim()).filter(Boolean);
    const pointsClauses = [];
    const pointsParams = [sessionId];
    if (teamClanIds.length) {
        pointsClauses.push(`clan_id IN (${teamClanIds.map(() => '?').join(',')})`);
        pointsParams.push(...teamClanIds);
    }
    if (longNames.length) {
        pointsClauses.push(`(clan_id IS NULL AND squadron IN (${longNames.map(() => '?').join(',')}))`);
        pointsParams.push(...longNames);
    }
    const pointsRows = pointsClauses.length
        ? await dbAllAsync(
            pointsDb,
            `SELECT clan_id, squadron, diffs_json, diff_total, updated_json
             FROM game_cache
             WHERE game_id = ? AND (${pointsClauses.join(' OR ')})`,
            pointsParams
        )
        : [];

    const points_diffs = {};
    for (const row of pointsRows) {
        const identity = squadronIdentities.find(entry =>
            (row.clan_id != null && entry.clan_id != null && Number(entry.clan_id) === Number(row.clan_id))
            || entry.long_name === row.squadron
        ) || {
            clan_id: row.clan_id != null ? Number(row.clan_id) : null,
            short_name: row.squadron,
            tag_name: row.squadron,
            long_name: row.squadron,
        };
        const diffKey = String(identity.short_name || row.squadron);
        try {
            points_diffs[diffKey] = {
                squadron_identity: identity,
                points_diff: parseJsonColumn(row.diffs_json),
                diff_total: Number(row.diff_total || 0),
                current_points: parseJsonColumn(row.updated_json),
                key: diffKey,
            };
        } catch (err) {
            points_diffs[diffKey] = {
                squadron_identity: identity,
                points_diff: {},
                diff_total: 0,
                current_points: {},
                key: diffKey,
            };
        }
    }

    for (const team of enrichedTeams) {
        const identity = team?.squadron_identity || {
            clan_id: null,
            short_name: team?.squadron || '',
            tag_name: team?.squadron_tagged || team?.squadron || '',
            long_name: team?.squadron_long || team?.squadron || '',
        };
        const diffKey = String(identity.short_name || team?.squadron || '');
        if (identity.long_name && !points_diffs[diffKey]) {
            points_diffs[diffKey] = {
                squadron_identity: identity,
                points_diff: {},
                diff_total: 0,
                current_points: {},
                key: diffKey,
            };
        }
    }

    // Prefer clan_id (rename-stable); the text fallback is gated on a NULL
    // clan_id so we don't pull a different clan that happens to share a short
    // tag. Post-migration wl_standings.clan_id is always populated, so the
    // fallback is only here as a safety net.
    const wlShortTexts = teams.map(team => String(team?.squadron || '').trim()).filter(Boolean);
    const wlClauses = [];
    const wlParams = [];
    if (teamClanIds.length) {
        wlClauses.push(`clan_id IN (${teamClanIds.map(() => '?').join(',')})`);
        wlParams.push(...teamClanIds);
    }
    if (wlShortTexts.length) {
        wlClauses.push(`(clan_id IS NULL AND squadron IN (${wlShortTexts.map(() => '?').join(',')}))`);
        wlParams.push(...wlShortTexts);
    }
    const wlRows = wlClauses.length
        ? await dbAllAsync(
            wlDb,
            `SELECT clan_id, squadron, wins, losses
             FROM wl_standings
             WHERE ${wlClauses.join(' OR ')}`,
            wlParams
        )
        : [];

    const wl = {};
    for (const team of enrichedTeams) {
        const identity = team?.squadron_identity || {
            clan_id: null,
            short_name: team?.squadron || '',
            tag_name: team?.squadron_tagged || team?.squadron || '',
            long_name: team?.squadron_long || team?.squadron || '',
        };
        const key = String(identity.short_name || team?.squadron || '');
        wl[key] = {
            squadron_identity: identity,
            wins: 0,
            losses: 0,
            key,
        };
    }
    for (const row of wlRows) {
        const identity = squadronIdentities.find(entry =>
            (row.clan_id != null && entry.clan_id != null && Number(entry.clan_id) === Number(row.clan_id))
            || entry.short_name === row.squadron
            || entry.long_name === row.squadron
            || entry.tag_name === row.squadron
        ) || {
            clan_id: row.clan_id != null ? Number(row.clan_id) : null,
            short_name: row.squadron,
            tag_name: row.squadron,
            long_name: row.squadron,
        };
        const key = String(identity.short_name || row.squadron);
        wl[key] = {
            squadron_identity: identity,
            wins: Number(row.wins || 0),
            losses: Number(row.losses || 0),
            key,
        };
    }

    const winner = replay?.winning_team_squadron || matchRow?.winning_sq || null;
    const loser = replay?.losing_team_squadron || matchRow?.losing_sq || null;
    const isDraw = Boolean(replay?.draw || replay?.is_draw);
    const winnerIdentity = squadronIdentities.find(entry => entry.short_name === winner || entry.tag_name === winner || entry.long_name === winner) || null;
    const loserIdentity = squadronIdentities.find(entry => entry.short_name === loser || entry.tag_name === loser || entry.long_name === loser) || null;

    return {
        session_id: String(sessionId),
        match_details: {
            utc_timestamp: Number(matchRow?.endtime_unix || replay?.end_ts || 0),
            session_id: String(sessionId),
            received_unix: Number(matchRow?.received_unix || 0) || undefined,
        },
        map_name: matchRow?.map_name || replay?.map || null,
        game_type: matchRow?.game_type || replay?.mode || null,
        winner,
        loser,
        winner_identity: winnerIdentity,
        loser_identity: loserIdentity,
        winner_clan_id: winnerIdentity?.clan_id ?? null,
        loser_clan_id: loserIdentity?.clan_id ?? null,
        is_draw: isDraw,
        teams: enrichedTeams,
        squadrons: squadronIdentities,
        replay: replay || {
            available: false,
            chat_log: [],
            battle_log: [],
            teams: enrichedTeams,
        },
        wl,
        points_diffs,
    };
}

function parseDateFilterValue(rawValue, isEndBoundary = false) {
    if (rawValue == null || rawValue === '') return null;

    const value = String(rawValue).trim();
    if (!value) return null;

    // Accept epoch timestamps from frontend filters. 10 digits = seconds,
    // 13 digits = milliseconds.
    if (/^\d+$/.test(value)) {
        const numeric = Number(value);
        if (Number.isFinite(numeric)) {
            if (value.length >= 13) return Math.floor(numeric / 1000);
            return numeric;
        }
        return null;
    }

    const date = new Date(value);
    if (isNaN(date.getTime())) return null;

    // Date-only strings should include the full UTC day for end boundaries.
    if (isEndBoundary && /^\d{4}-\d{2}-\d{2}$/.test(value)) {
        date.setUTCHours(23, 59, 59, 999);
    }

    return Math.floor(date.getTime() / 1000);
}

// Helper function to parse date filters from query parameters
function parseDateFilters(req) {
    const { start_date, end_date, season, week } = req.query;
    
    let startTimestamp = null;
    let endTimestamp = null;
    let filterDescription = null;
    
    // Frontend already converts seasons to concrete timestamps/dates, so just
    // normalize the passed values here.
    startTimestamp = parseDateFilterValue(start_date, false);
    endTimestamp = parseDateFilterValue(end_date, true);
    
    // Build description
    if (startTimestamp && endTimestamp) {
        filterDescription = `${start_date} to ${end_date}`;
    } else if (startTimestamp) {
        filterDescription = `from ${start_date}`;
    } else if (endTimestamp) {
        filterDescription = `up to ${end_date}`;
    }
    
    return { 
        startTimestamp, 
        endTimestamp,
        filterDescription,
        hasFilter: startTimestamp !== null || endTimestamp !== null
    };
}

// Helper function to create date filter metadata for responses
function createDateFilterMetadata(filters, start_date, end_date, season, week) {
    if (!filters.hasFilter) {
        return {
            applied: false,
            description: "All-time stats"
        };
    }
    
    return {
        applied: true,
        start_date: filters.startTimestamp ? new Date(filters.startTimestamp * 1000).toISOString() : null,
        end_date: filters.endTimestamp ? new Date(filters.endTimestamp * 1000).toISOString() : null,
        season: season || null,
        week: week ? parseInt(week) : null,
        description: filters.filterDescription || "Custom date range"
    };
}

app.use((req, res, next) => {
    const start = Date.now();
    log.info(`${req.method} ${req.url}`, { 
        ip: req.ip || req.connection.remoteAddress,
        userAgent: req.get('User-Agent'),
        params: req.params,
        query: req.query
    });
    
    res.on('finish', () => {
        const duration = Date.now() - start;
        log.info(`${req.method} ${req.url} - ${res.statusCode}`, { 
            duration: `${duration}ms`,
            size: res.get('Content-Length') || 'unknown'
        });
    });
    
    next();
});

const DB_PATH = path.join(STORAGE_ROOT, 'sq_battles.db');

if (!fs.existsSync(DB_PATH)) {
    log.error('Database file does not exist', null, { 
        dbPath: DB_PATH,
        message: 'Run the main bot first to create the database, or create it manually'
    });
    process.exit(1);
}

// Dedicated read-only connection for heavy leaderboard aggregations. SQLite
// serializes statements per Database object, so without this a 60-90s
// leaderboard scan blocks every other API call (player profile, squadron
// details, search). WAL mode supports concurrent readers, so a second
// connection is the cheapest fix.
const heavyDb = new sqlite3.Database(DB_PATH, sqlite3.OPEN_READONLY, (err) => {
    if (err) log.warn('Failed to open heavy reader connection, falling back to main db:', err.message);
    else log.info('Heavy-reader connection open');
});

const db = new sqlite3.Database(DB_PATH, sqlite3.OPEN_READONLY, (err) => {
    if (err) {
        log.error('Failed to open database', err, { dbPath: DB_PATH });
        process.exit(1);
    }
    log.info('Connected to SQLite database in read-only mode', { dbPath: DB_PATH });

    // Create performance indexes using a separate RW connection (one-time, idempotent)
    const rwDb = new sqlite3.Database(DB_PATH, (rwErr) => {
        if (rwErr) { log.warn('Could not open DB for index creation:', rwErr.message); return; }
        rwDb.serialize(() => {
            rwDb.run('CREATE INDEX IF NOT EXISTS idx_pgh_uid_session ON player_games_hist(UID, session_id DESC)', (e) => {
                if (!e) log.info('Index ensured: idx_pgh_uid_session');
            });
            rwDb.run('CREATE INDEX IF NOT EXISTS idx_pgh_uid_endtime ON player_games_hist(UID, endtime_unix)', (e) => {
                if (!e) log.info('Index ensured: idx_pgh_uid_endtime');
            });
            rwDb.run('CREATE INDEX IF NOT EXISTS idx_pgh_nick ON player_games_hist(nick COLLATE NOCASE)', (e) => {
                if (!e) log.info('Index ensured: idx_pgh_nick');
            });
            rwDb.run('CREATE INDEX IF NOT EXISTS idx_pgh_squadron ON player_games_hist(squadron_name)', (e) => {
                if (!e) log.info('Index ensured: idx_pgh_squadron');
            });
            rwDb.run('CREATE INDEX IF NOT EXISTS idx_pgh_vehicle_internal_nocase ON player_games_hist(vehicle_internal COLLATE NOCASE)', (e) => {
                if (!e) log.info('Index ensured: idx_pgh_vehicle_internal_nocase');
            });
            rwDb.run('CREATE INDEX IF NOT EXISTS idx_ms_session ON match_summary(session_id)', (e) => {
                if (!e) log.info('Index ensured: idx_ms_session');
            });
            rwDb.run('CREATE INDEX IF NOT EXISTS idx_ms_map_name ON match_summary(map_name)', (e) => {
                if (!e) log.info('Index ensured: idx_ms_map_name');
            });
            rwDb.run('CREATE INDEX IF NOT EXISTS idx_ms_endtime ON match_summary(endtime_unix)', (e) => {
                if (!e) log.info('Index ensured: idx_ms_endtime');
            });
            rwDb.run('CREATE INDEX IF NOT EXISTS idx_ms_winning_sq ON match_summary(winning_sq)', (e) => {
                if (!e) log.info('Index ensured: idx_ms_winning_sq');
            });
            rwDb.run('CREATE INDEX IF NOT EXISTS idx_ms_losing_sq ON match_summary(losing_sq)', (e) => {
                if (!e) log.info('Index ensured: idx_ms_losing_sq');
            });
            // Composite index for the squadron leaderboard's
            // GROUP BY clan_id, squadron_name aggregation. Without this the
            // query falls back to a temp B-tree sort over all 4M+ rows.
            rwDb.run(
                'CREATE INDEX IF NOT EXISTS idx_pgh_clanid_squadron ON player_games_hist(clan_id, squadron_name)',
                (e) => {
                    if (!e) log.info('Index ensured: idx_pgh_clanid_squadron');
                }
            );
            rwDb.run(
                'CREATE INDEX IF NOT EXISTS idx_pgh_clanid_uid ON player_games_hist(clan_id, UID)',
                (e) => {
                    if (!e) log.info('Index ensured: idx_pgh_clanid_uid');
                }
            );
            rwDb.close(() => log.info('Performance indexes ready'));
        });
    });

    // Use TRUNCATE so the WAL is actively checkpointed once readers clear.
    runWalCheckpoint('TRUNCATE', 'WAL checkpoint completed successfully', 'WAL checkpoint failed:');
    
    db.get("SELECT name FROM sqlite_master WHERE type='table' AND name='player_games_hist'", (err, row) => {
        if (err) {
            log.error('Failed to check table existence', err);
            process.exit(1);
        } else if (!row) {
            log.error('CRITICAL: player_games_hist table does not exist', null, {
                message: 'Database exists but player_games_hist table is missing',
                dbPath: DB_PATH
            });
            process.exit(1);
        } else {
            log.info('Database table verification passed', { table: 'player_games_hist' });
            db.all("PRAGMA table_info(player_games_hist)", (err, cols) => {
                if (!err) hasSquadronColumn = cols.some(c => c.name === 'squadron_name');
                log.info('Schema check complete', { hasSquadronColumn });
            });
        }
    });
    
    db.get("SELECT name FROM sqlite_master WHERE type='table' AND name='match_summary'", (err, row) => {
        if (err) {
            log.error('Failed to check match_summary table existence', err);
        } else if (!row) {
            log.warn('match_summary table does not exist - /api/live endpoint will not work', {
                message: 'Run the bot to generate match data first',
                dbPath: DB_PATH
            });
        } else {
            log.info('Database table verification passed', { table: 'match_summary' });
        }
    });

    // Pre-warm caches that gate the analytics vehicle search/load flows.
    // ensureVehicleList does a GROUP BY across player_games_hist; running it
    // at boot means the first user click pays no cold-cache penalty, and we
    // use it as the readiness signal for the heavy-path gate.
    ensureVehicleList((err, list) => {
        if (err) log.warn('Vehicle list pre-warm failed:', err.message);
        else log.info('Vehicle list cache pre-warmed', { count: list.length });
        markServerReady(err ? 'vehicle_list_error' : 'vehicle_list_warm');
    });
    try {
        const t0 = Date.now();
        const resp = buildTranslationsResponse();
        log.info('Vehicle translations cache pre-warmed', {
            source: resp.source,
            count: Object.keys(resp.vehicles || {}).length,
            ms: Date.now() - t0,
        });
    } catch (e) {
        log.warn('Vehicle translations pre-warm failed:', e.message);
    }
});

app.get('/api/player/:uid', (req, res) => {
    const { uid } = req.params;

    const cacheKey = `player_${uid}_${req.query.start_date || ''}_${req.query.end_date || ''}_${req.query.season || ''}_${req.query.week || ''}`;
    const cached = getCachedResponse(cacheKey);
    if (cached) return res.json(cached);

    if (!uid) {
        return res.status(400).json({
            error: 'UID parameter is required'
        });
    }
    
    // Parse date filters
    const dateFilters = parseDateFilters(req);
    const { start_date, end_date, season, week } = req.query;
    
    const latestNickQuery = `
        SELECT nick, squadron_name, clan_id
        FROM player_games_hist
        WHERE UID = ? AND nick NOT LIKE 'coop/%'
        ORDER BY session_id DESC
        LIMIT 1
    `;

    const aggregatedStatsQuery = `
        SELECT 
            vehicle_internal,
            vehicle,
            SUM(ground_kills) as total_ground_kills,
            SUM(air_kills) as total_air_kills,
            SUM(assists) as total_assists,
            SUM(captures) as total_captures,
            SUM(deaths) as total_deaths,
            SUM(CASE WHEN UPPER(victor_bool) = 'WIN' THEN 1 ELSE 0 END) as wins,
            SUM(CASE WHEN UPPER(victor_bool) = 'LOSS' THEN 1 ELSE 0 END) as losses,
            COUNT(*) as total_battles
        FROM player_games_hist 
        WHERE UID = ? 
            AND (? IS NULL OR endtime_unix >= ?)
            AND (? IS NULL OR endtime_unix <= ?)
        GROUP BY vehicle_internal
        ORDER BY vehicle_internal
    `;

    const queryParams = [
        uid,
        dateFilters.startTimestamp,
        dateFilters.startTimestamp,
        dateFilters.endTimestamp,
        dateFilters.endTimestamp
    ];

    const nickPromise = new Promise((resolve, reject) => {
        db.get(latestNickQuery, [uid], (err, row) => { if (err) reject(err); else resolve(row); });
    });
    const statsPromise = new Promise((resolve, reject) => {
        db.all(aggregatedStatsQuery, queryParams, (err, rows) => { if (err) reject(err); else resolve(rows); });
    });
    const lookupPromise = new Promise((resolve) => {
        loadSquadronLookupCached(resolve);
    });
    // Authoritative current-roster membership. squadron_members is updated
    // by the periodic squadron-sync, so this reflects "what squadron this
    // player belongs to right now" — beats the most-recent player_games_hist
    // row which can be stale post-rename if the player hasn't played a SQB
    // since the rename.
    const rosterPromise = new Promise((resolve) => {
        const squadronsDbPath = path.join(STORAGE_ROOT, 'squadrons.db');
        if (!fs.existsSync(squadronsDbPath)) return resolve(null);
        const sdb = new sqlite3.Database(squadronsDbPath, sqlite3.OPEN_READONLY, (err) => {
            if (err) return resolve(null);
            sdb.get(
                'SELECT clan_id FROM squadron_members WHERE uid = ? LIMIT 1',
                [uid],
                (qerr, row) => {
                    sdb.close();
                    if (qerr || !row || row.clan_id == null) return resolve(null);
                    resolve(Number(row.clan_id));
                }
            );
        });
    });

    Promise.all([nickPromise, statsPromise, lookupPromise, rosterPromise])
        .then(async ([nickRow, vehicleRows, squadronLookup, rosterClanId]) => {
            if (!nickRow) {
                return jsonError(res, 404, 'Player not found', { uid: normalizeUid(uid) });
            }

            const identity = await loadPlayerIdentitySnapshot(uid, squadronLookup, {
                latestRow: nickRow,
                preferredClanId: rosterClanId,
            });
            loadPerformanceBenchmarksCached(dateFilters, (benchmarks) => {
                const vehicles = vehicleRows.map(row => {
                    const wins = row.wins || 0;
                    const losses = row.losses || 0;
                    const totalBattles = row.total_battles || 0;
                    let winRate = '0.0';
                    if (totalBattles > 0 && wins >= 0) {
                        winRate = ((wins / totalBattles) * 100).toFixed(1);
                    }
                    return {
                        vehicle_internal: row.vehicle_internal,
                        vehicle: normalizeVehicleName(row.vehicle),
                        stats: {
                            ground_kills: row.total_ground_kills,
                            air_kills: row.total_air_kills,
                            assists: row.total_assists,
                            captures: row.total_captures,
                            deaths: row.total_deaths,
                            wins,
                            losses,
                            total_battles: totalBattles,
                            win_rate: winRate
                        }
                    };
                });

                queryPlayerRatingStats(uid, dateFilters).then(ratingStats => {
                    const playerPerformance = computePerformanceScore(ratingStats || { games: 0 }, benchmarks.players);

                    const response = {
                        date_filter: createDateFilterMetadata(dateFilters, start_date, end_date, season, week),
                        uid: normalizeUid(uid),
                        nick: identity?.nick || nickRow.nick,
                        previous_nicks: identity?.previous_nicks || [],
                        squadron_name: identity?.squadron_name || '',
                        squadron_long_name: identity?.squadron_long_name || '',
                        squadron_clan_id: identity?.squadron_clan_id ?? null,
                        previous_squadron_names: identity?.previous_squadron_names || [],
                        performance: playerPerformance,
                        vehicles,
                        total_vehicles: vehicleRows.length
                    };

                    setCachedResponse(cacheKey, response);
                    res.json(response);
                });
            });
        })
        .catch(err => {
            log.error('Database error in player query', err, { uid, endpoint: '/api/player/:uid' });
            jsonError(res, 500, 'Database error occurred', { errorCode: 'DB_PLAYER_QUERY_FAILED' });
        });
});

app.get('/api/player/:uid/games', (req, res) => {
    const { uid } = req.params;

    const cacheKey = `games_${uid}_${req.query.start_date || ''}_${req.query.end_date || ''}_${req.query.season || ''}_${req.query.week || ''}`;
    const cached = getCachedResponse(cacheKey);
    if (cached) return res.json(cached);

    if (!uid) {
        return res.status(400).json({
            error: 'UID parameter is required'
        });
    }
    
    // Parse date filters
    const dateFilters = parseDateFilters(req);
    const { start_date, end_date, season, week } = req.query;

    const latestNickQuery = `
        SELECT nick, squadron_name, clan_id
        FROM player_games_hist
        WHERE UID = ? AND nick NOT LIKE 'coop/%'
        ORDER BY session_id DESC
        LIMIT 1
    `;

    const recentGamesQuery = `
        SELECT 
            session_id,
            nick,
            squadron_name,
            vehicle_internal,
            vehicle,
            ground_kills,
            air_kills,
            assists,
            captures,
            deaths,
            victor_bool as result,
            endtime_unix
        FROM player_games_hist 
        WHERE UID = ? 
            AND (? IS NULL OR endtime_unix >= ?)
            AND (? IS NULL OR endtime_unix <= ?)
        ORDER BY session_id DESC
    `;

    db.get(latestNickQuery, [uid], (err, nickRow) => {
        if (err) {
            log.error('Database error in nick query', err, { 
                uid: uid,
                query: 'latestNickQuery',
                endpoint: '/api/player/:uid/games'
            });
            return res.status(500).json({
                error: 'Database error occurred',
                errorCode: 'DB_NICK_QUERY_FAILED'
            });
        }

        if (!nickRow) {
            return jsonError(res, 404, 'Player not found', { uid: normalizeUid(uid) });
        }

        const playerNick = nickRow.nick;
        const playerSquadron = nickRow.squadron_name;

        // Build query parameters with date filtering
        const queryParams = [
            uid,
            dateFilters.startTimestamp,
            dateFilters.startTimestamp,
            dateFilters.endTimestamp,
            dateFilters.endTimestamp
        ];

        db.all(recentGamesQuery, queryParams, (err, gameRows) => {
            if (err) {
                log.error('Database error in games query', err, {
                    uid: uid,
                    endpoint: '/api/player/:uid/games'
                });
                return res.status(500).json({
                    error: 'Database error occurred',
                    errorCode: 'DB_GAMES_QUERY_FAILED'
                });
            }

            const response = {
                date_filter: createDateFilterMetadata(dateFilters, start_date, end_date, season, week),
                uid: normalizeUid(uid),
                nick: playerNick,
                squadron_name: playerSquadron,
                games: gameRows.map(row => ({
                    session_id: row.session_id,
                    vehicle_internal: row.vehicle_internal,
                    vehicle: normalizeVehicleName(row.vehicle),
                    squadron_name: row.squadron_name,
                    timestamp: row.endtime_unix || 0,
                    stats: {
                        ground_kills: row.ground_kills || 0,
                        air_kills: row.air_kills || 0,
                        assists: row.assists || 0,
                        captures: row.captures || 0,
                        deaths: row.deaths || 0
                    },
                    result: row.result || 'Unknown'
                })),
                total_games_returned: gameRows.length
            };

            setCachedResponse(cacheKey, response);
            res.json(response);
        });
    });
});

app.get('/api/player/:uid/history', (req, res) => {
    const { uid } = req.params;

    const dateFilters = parseDateFilters(req);
    const { start_date, end_date, season, week } = req.query;
    const cacheKey = `history_${uid}_${start_date || ''}_${end_date || ''}_${season || ''}_${week || ''}`;
    const cached = getCachedResponse(cacheKey);
    if (cached) return res.json(cached);

    const historyQuery = `
        SELECT
            date(endtime_unix, 'unixepoch') as period,
            COUNT(DISTINCT session_id) as battles,
            ROUND(SUM(CASE WHEN UPPER(victor_bool) = 'WIN' THEN 1.0 ELSE 0 END) * 100.0 / NULLIF(COUNT(*), 0), 1) as win_rate,
            ROUND(CAST(SUM(ground_kills + air_kills) AS REAL) / MAX(SUM(deaths), 1), 2) as kdr
        FROM player_games_hist
        WHERE UID = ? AND endtime_unix IS NOT NULL
            AND (? IS NULL OR endtime_unix >= ?)
            AND (? IS NULL OR endtime_unix <= ?)
        GROUP BY period
        ORDER BY period ASC
    `;
    const params = [
        uid,
        dateFilters.startTimestamp,
        dateFilters.startTimestamp,
        dateFilters.endTimestamp,
        dateFilters.endTimestamp,
    ];
    db.all(historyQuery, params, (err, rows) => {
        if (err) return jsonError(res, 500, 'DB error');
        const response = {
            date_filter: createDateFilterMetadata(dateFilters, start_date, end_date, season, week),
            uid: normalizeUid(uid),
            days_with_battles_only: true,
            history: rows,
        };
        setCachedResponse(cacheKey, response);
        res.json(response);
    });
});

app.get('/api/search/:nickname', (req, res) => {
    const { nickname } = req.params;
    
    if (!nickname || nickname.trim().length === 0) {
        return res.status(400).json({
            error: 'Nickname parameter is required'
        });
    }
    
    const cacheKey = `search_${nickname.toLowerCase()}`;
    const cached = getCachedResponse(cacheKey);
    if (cached) {
        log.info('Returning cached search results');
        return res.json(cached);
    }
    
    const searchQuery = `
        SELECT UID
        FROM player_games_hist
        WHERE nick LIKE ? COLLATE NOCASE
        GROUP BY UID
        ORDER BY MAX(session_id) DESC, MAX(endtime_unix) DESC
        LIMIT 50
    `;

    const latestNickQuery = `
        SELECT nick, squadron_name, clan_id
        FROM player_games_hist
        WHERE UID = ? AND nick NOT LIKE 'coop/%'
        ORDER BY session_id DESC
        LIMIT 1
    `;

    const searchTerm = `%${nickname.trim()}%`;

    db.all(searchQuery, [searchTerm], (err, rows) => {
        if (err) {
            log.error('Database error in search query', err, {
                searchTerm: searchTerm,
                nickname: nickname,
                endpoint: '/api/search/:nickname'
            });
            return res.status(500).json({
                error: 'Database error occurred',
                errorCode: 'DB_SEARCH_QUERY_FAILED'
            });
        }

        loadSquadronLookupCached((squadronLookup) => {
            const lookups = rows.map(row => (async () => {
                const latestRow = await dbGetAsync(db, latestNickQuery, [row.UID]);
                if (!latestRow) {
                    return {
                        uid: normalizeUid(row.UID),
                        nick: '',
                        previous_nicks: [],
                        squadron_name: '',
                        squadron_long_name: '',
                        squadron_clan_id: null,
                        previous_squadron_names: [],
                    };
                }
                return loadPlayerIdentitySnapshot(row.UID, squadronLookup, { latestRow });
            })());

            Promise.all(lookups).then(results => {
                const finalResults = results.filter(Boolean);
                const response = {
                    search_term: nickname.trim(),
                    results: finalResults,
                    total_found: finalResults.length,
                    limited_to: 50
                };

                log.info('Search query executed', {
                    searchTerm: searchTerm,
                    nickname: nickname,
                    resultsFound: finalResults.length,
                    firstFewResults: finalResults.slice(0, 3)
                });

                setCachedResponse(cacheKey, response);
                res.json(response);
            }).catch(identityErr => {
                log.error('Failed to build player identity search response', identityErr, {
                    nickname,
                    endpoint: '/api/search/:nickname'
                });
                jsonError(res, 500, 'Database error occurred', { errorCode: 'DB_SEARCH_QUERY_FAILED' });
            });
        });
    });
});

// Guard debug endpoints - only allow from localhost
const debugGuard = (req, res, next) => {
    const ip = req.ip || req.connection.remoteAddress || '';
    const isLocal = ip === '127.0.0.1' || ip === '::1' || ip === '::ffff:127.0.0.1';
    if (!isLocal) {
        return res.status(404).json({ error: 'Not found' });
    }
    next();
};
app.use('/api/debug', debugGuard);

app.get('/api/debug/stats', requireAdminBearer, (req, res) => {
    const schemaQuery = `PRAGMA table_info(player_games_hist)`;
    
    db.all(schemaQuery, (err, schema) => {
        if (err) {
            log.error('Database error in schema query', err);
            return res.status(500).json({ error: 'Database schema error' });
        }
        
        const statsQuery = `
            SELECT 
                COUNT(*) as total_records,
                COUNT(DISTINCT UID) as unique_players,
                COUNT(DISTINCT session_id) as unique_sessions,
                MIN(session_id) as oldest_session,
                MAX(session_id) as newest_session
            FROM player_games_hist
        `;
        
        const sampleQuery = `
            SELECT *
            FROM player_games_hist 
            ORDER BY session_id DESC 
            LIMIT 3
        `;
        
        db.get(statsQuery, (err, stats) => {
            if (err) {
                log.error('Database error in stats query', err);
                return res.status(500).json({ error: 'Database stats error' });
            }
            
            db.all(sampleQuery, (err, samples) => {
                if (err) {
                    log.error('Database error in sample query', err);
                    return res.status(500).json({ error: 'Database sample error' });
                }
                
                res.json({
                    table_schema: schema.map(col => ({
                        name: col.name,
                        type: col.type,
                        notnull: col.notnull,
                        default_value: col.dflt_value
                    })),
                    database_stats: stats,
                    sample_records: samples,
                    timestamp: new Date().toISOString()
                });
            });
        });
    });
});

app.get('/health', (req, res) => {
    res.json({
        status: 'OK',
        timestamp: new Date().toISOString(),
        database: 'Connected',
        ready: serverReady,
        uptime_ms: Date.now() - startupReadyAt,
    });
});

app.get('/api/live', (req, res) => {
    const requestStart = Date.now();
    const clientIP = req.ip || req.connection.remoteAddress;
    const userAgent = req.get('User-Agent') || 'Unknown';
    
    log.info('API request received', {
        endpoint: '/api/live',
        method: 'GET',
        clientIP: clientIP,
        userAgent: userAgent,
        queryParams: req.query
    });

    // Parse date filters
    const dateFilters = parseDateFilters(req);
    const { start_date, end_date, season, week } = req.query;

    const limit = parseInt(req.query.limit) || 15;
    const maxLimit = 200; 
    
    log.debug('Parsed request parameters', {
        requestedLimit: req.query.limit,
        parsedLimit: limit,
        maxLimit: maxLimit
    });
    
    if (limit > maxLimit) {
        log.warn('Request rejected - limit too high', {
            requestedLimit: limit,
            maxLimit: maxLimit,
            clientIP: clientIP
        });
        return res.status(400).json({
            error: `Limit cannot exceed ${maxLimit}`
        });
    }

    const query = `
        SELECT
            session_id,
            map_name,
            endtime_unix,
            winning_sq,
            losing_sq,
            winning_team_json,
            losing_team_json,
            game_type
        FROM match_summary
        WHERE (? IS NULL OR endtime_unix >= ?)
            AND (? IS NULL OR endtime_unix <= ?)
        ORDER BY endtime_unix DESC
        LIMIT ?
    `;

    // Build query parameters with date filtering
    const queryParams = [
        dateFilters.startTimestamp,
        dateFilters.startTimestamp,
        dateFilters.endTimestamp,
        dateFilters.endTimestamp,
        limit
    ];

    log.debug('Executing database query', {
        query: query.replace(/\s+/g, ' ').trim(),
        parameters: queryParams
    });

    const queryStart = Date.now();
    db.all(query, queryParams, (err, rows) => {
        const queryTime = Date.now() - queryStart;
        
        if (err) {
            log.error('Database error in /api/live', err, {
                queryTime: `${queryTime}ms`,
                parameters: [limit],
                clientIP: clientIP
            });
            return res.status(500).json({
                error: 'Database error'
            });
        }

        log.info('Database query completed', {
            rowsReturned: rows.length,
            queryTime: `${queryTime}ms`,
            limit: limit
        });

        loadSquadronLookupCached((squadronLookup) => {
        let jsonParseErrors = 0;
        const matches = rows.map((row, index) => {
            let winningTeam, losingTeam;

            try {
                winningTeam = row.winning_team_json ? parseJsonColumn(row.winning_team_json) : null;
                log.debug('Parsed winning team JSON', {
                    session_id: row.session_id,
                    hasWinningTeam: !!winningTeam,
                    playerCount: winningTeam?.players?.length || 0
                });
            } catch (e) {
                jsonParseErrors++;
                log.warn('Failed to parse winning_team_json', {
                    session_id: row.session_id,
                    error: e.message,
                    jsonLength: row.winning_team_json?.length || 0
                });
                winningTeam = null;
            }

            try {
                losingTeam = row.losing_team_json ? parseJsonColumn(row.losing_team_json) : null;
                log.debug('Parsed losing team JSON', {
                    session_id: row.session_id,
                    hasLosingTeam: !!losingTeam,
                    playerCount: losingTeam?.players?.length || 0
                });
            } catch (e) {
                jsonParseErrors++;
                log.warn('Failed to parse losing_team_json', {
                    session_id: row.session_id,
                    error: e.message,
                    jsonLength: row.losing_team_json?.length || 0
                });
                losingTeam = null;
            }

            const matchData = {
                session_id: row.session_id,
                map_name: row.map_name,
                endtime_unix: row.endtime_unix,
                endtime_iso: new Date(row.endtime_unix * 1000).toISOString(),
                winning_squadron: row.winning_sq,
                losing_squadron: row.losing_sq,
                winning_tag: cleanText(normalizeMatchTeam(winningTeam, squadronLookup, { squadron: row.winning_sq })?.squadron_tagged) || row.winning_sq,
                losing_tag: cleanText(normalizeMatchTeam(losingTeam, squadronLookup, { squadron: row.losing_sq })?.squadron_tagged) || row.losing_sq,
                winning_team: normalizeMatchTeam(winningTeam, squadronLookup, { squadron: row.winning_sq }),
                losing_team: normalizeMatchTeam(losingTeam, squadronLookup, { squadron: row.losing_sq }),
                game_type: row.game_type || "",
            };

            log.debug('Processed match record', {
                index: index + 1,
                session_id: row.session_id,
                map: row.map_name,
                winner: row.winning_sq,
                loser: row.losing_sq,
                endtime: matchData.endtime_iso
            });

            return matchData;
        });

        const totalTime = Date.now() - requestStart;
        const response = {
            date_filter: createDateFilterMetadata(dateFilters, start_date, end_date, season, week),
            total_matches: matches.length,
            limit: limit,
            matches: matches
        };

        log.info('API response sent', {
            endpoint: '/api/live',
            matchesReturned: matches.length,
            jsonParseErrors: jsonParseErrors,
            totalRequestTime: `${totalTime}ms`,
            queryTime: `${queryTime}ms`,
            clientIP: clientIP,
            responseSize: JSON.stringify(response).length
        });

        res.json(response);
        });
    });
});

// ── Single match detail ──
app.get('/api/match/:sessionId', (req, res) => {
    const { sessionId } = req.params;
    const clientIP = req.headers['x-forwarded-for'] || req.socket.remoteAddress;
    const dateFilters = parseDateFilters(req);
    const { start_date, end_date, season, week } = req.query;

    if (!sessionId || !/^[0-9a-fA-F]+$/.test(sessionId)) {
        return jsonError(res, 400, 'Invalid session ID format. Must be hexadecimal.');
    }

    const cacheKey = `match_${sessionId}_${start_date || ''}_${end_date || ''}_${season || ''}_${week || ''}`;
    const cached = getCachedResponse(cacheKey);
    if (cached) return res.json(cached);

    const query = `
        SELECT session_id, map_name, endtime_unix, winning_sq, losing_sq,
               winning_team_json, losing_team_json, game_type
        FROM match_summary
        WHERE session_id = ?
            AND (? IS NULL OR endtime_unix >= ?)
            AND (? IS NULL OR endtime_unix <= ?)
    `;

    const queryParams = [
        sessionId,
        dateFilters.startTimestamp,
        dateFilters.startTimestamp,
        dateFilters.endTimestamp,
        dateFilters.endTimestamp,
    ];

    db.get(query, queryParams, (err, row) => {
        if (err) {
            log.error('Database error in /api/match/:sessionId', err, { sessionId });
            return jsonError(res, 500, 'Database error');
        }

        if (!row) {
            return jsonError(res, 404, 'Match not found', { session_id: sessionId });
        }

        let winningTeam = null, losingTeam = null;
        try { winningTeam = row.winning_team_json ? parseJsonColumn(row.winning_team_json) : null; } catch (e) { log.warn('Failed to parse winning_team_json', { sessionId }); }
        try { losingTeam = row.losing_team_json ? parseJsonColumn(row.losing_team_json) : null; } catch (e) { log.warn('Failed to parse losing_team_json', { sessionId }); }

        const replayPath = replayDataPath(sessionId);
        const replayAvailable = fs.existsSync(replayPath);

        loadSquadronLookupCached((squadronLookup) => {
            const response = {
                date_filter: createDateFilterMetadata(dateFilters, start_date, end_date, season, week),
                session_id: row.session_id,
                map_name: row.map_name,
                endtime_unix: row.endtime_unix,
                endtime_iso: new Date(row.endtime_unix * 1000).toISOString(),
                mode: row.game_type || '',
                winning_squadron: row.winning_sq,
                losing_squadron: row.losing_sq,
                winning_tag: cleanText(normalizeMatchTeam(winningTeam, squadronLookup, { squadron: row.winning_sq })?.squadron_tagged) || row.winning_sq,
                losing_tag: cleanText(normalizeMatchTeam(losingTeam, squadronLookup, { squadron: row.losing_sq })?.squadron_tagged) || row.losing_sq,
                winning_team: normalizeMatchTeam(winningTeam, squadronLookup, { squadron: row.winning_sq }),
                losing_team: normalizeMatchTeam(losingTeam, squadronLookup, { squadron: row.losing_sq }),
                game_type: row.game_type || '',
                replay_available: replayAvailable
            };

            setCachedResponse(cacheKey, response);
            log.info('Match detail served', { sessionId, clientIP });
            res.json(response);
        });
    });
});

// ── Match replay data (chat log, battle log, etc.) ──
app.get('/api/match/:sessionId/replay', (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 replayPath = replayDataPath(sessionId);

    if (!fs.existsSync(replayPath)) {
        return res.json({ available: false, session_id: sessionId });
    }

    try {
        const data = JSON.parse(zlib.gunzipSync(fs.readFileSync(replayPath)));
        res.json({
            available: true,
            session_id: sessionId,
            map: data.map || null,
            mode: data.mode || null,
            duration: data.duration || null,
            draw: data.draw || false,
            chat_log: data.chat_log || [],
            battle_log: data.battle_log || [],
            teams: data.teams || [],
            winning_team_squadron: data.winning_team_squadron || null,
            losing_team_squadron: data.losing_team_squadron || null
        });
    } catch (e) {
        log.error('Failed to read replay data', e, { sessionId });
        res.status(500).json({ error: 'Failed to read replay data' });
    }
});

app.get('/api/match/:sessionId/scoreboard', async (req, res) => {
    const { sessionId } = req.params;

    if (!sessionId || !/^[0-9a-fA-F]+$/.test(sessionId)) {
        return jsonError(res, 400, 'Invalid session ID format.');
    }

    try {
        const context = await loadScoreboardContext(sessionId);
        if (!context) {
            return jsonError(res, 404, 'Match not found', { session_id: sessionId });
        }

        res.json({
            session_id: context.session_id,
            match_details: context.match_details,
            map_name: context.map_name,
            game_type: context.game_type,
            mode: context.mode || context.game_type || context.replay?.mode || null,
            winner: context.winner,
            loser: context.loser,
            is_draw: context.is_draw,
            teams: context.teams,
            wl: context.wl,
            points_diffs: context.points_diffs,
            replay: context.replay,
        });
    } catch (err) {
        log.error('Failed to load scoreboard context', err, { sessionId });
        res.status(500).json({ error: 'Failed to load scoreboard context' });
    }
});

app.get('/api/squadrons/:squadronname/comps', async (req, res) => {
    const { squadronname } = req.params;
    if (!squadronname) {
        return res.status(400).json({ error: 'Squadron name parameter is required' });
    }

    const windowSeconds = parseInt(req.query.window_seconds) || 3600;
    const limit = parseInt(req.query.limit) || 10;

    try {
        const response = await loadRecentComps(squadronname, windowSeconds, limit);
        if (!response) {
            return res.status(404).json({
                error: 'Comp history not found',
                squadron: String(squadronname).toUpperCase(),
            });
        }
        res.json(response);
    } catch (err) {
        log.error('Failed to load squadron comps', err, { squadronname });
        res.status(500).json({ error: 'Failed to load squadron comps' });
    }
});

// ── Search games by player name/UID, map, squadron, and/or time ──
app.get('/api/games/search', (req, res) => {
    const { player, map, squadron, time_from, time_to } = req.query;
    const clientIP = req.headers['x-forwarded-for'] || req.socket.remoteAddress;
    let limit = parseInt(req.query.limit) || 50;
    if (limit > 200) limit = 200;

    const isUid = player && /^\d+$/.test(player);

    // Parse time filters (unix seconds)
    const timeFrom = time_from ? parseInt(time_from) : null;
    const timeTo = time_to ? parseInt(time_to) : null;

    // Build the pipeline: find session_ids from player if given, then fetch from match_summary
    const findSessions = (callback) => {
        if (!player) return callback(null, null); // null means no player filter

        if (isUid) {
            db.all(
                'SELECT DISTINCT session_id FROM player_games_hist WHERE UID = ? ORDER BY endtime_unix DESC LIMIT ?',
                [player, limit],
                (err, rows) => callback(err, rows ? rows.map(r => r.session_id) : [])
            );
        } else {
            // Try exact match first (fast, uses index), then prefix, then substring fallback
            db.all(
                'SELECT DISTINCT session_id FROM player_games_hist WHERE nick = ? COLLATE NOCASE ORDER BY endtime_unix DESC LIMIT ?',
                [player, limit],
                (err, rows) => {
                    if (err) return callback(err, []);
                    if (rows && rows.length > 0) return callback(null, rows.map(r => r.session_id));
                    // Fallback: prefix match (can use index)
                    db.all(
                        'SELECT DISTINCT session_id FROM player_games_hist WHERE nick LIKE ? COLLATE NOCASE ORDER BY endtime_unix DESC LIMIT ?',
                        [`${player}%`, limit],
                        (err2, rows2) => {
                            if (err2) return callback(err2, []);
                            if (rows2 && rows2.length > 0) return callback(null, rows2.map(r => r.session_id));
                            // Last resort: substring match (full scan, but only if exact/prefix found nothing)
                            db.all(
                                'SELECT DISTINCT session_id FROM player_games_hist WHERE nick LIKE ? COLLATE NOCASE ORDER BY endtime_unix DESC LIMIT ?',
                                [`%${player}%`, limit],
                                (err3, rows3) => callback(err3, rows3 ? rows3.map(r => r.session_id) : [])
                            );
                        }
                    );
                }
            );
        }
    };

    findSessions((err, sessionIds) => {
        if (err) {
            log.error('Database error searching player sessions', err);
            return res.status(500).json({ error: 'Database error' });
        }

        // If player filter returned no results
        if (sessionIds !== null && sessionIds.length === 0) {
            return res.json({ total_matches: 0, matches: [] });
        }

        let query, params;
        const selectCols = 'session_id, map_name, endtime_unix, winning_sq, losing_sq, winning_team_json, losing_team_json, game_type';

        // Build WHERE conditions and params for match_summary filters
        const conditions = [];
        const condParams = [];

        if (sessionIds !== null) {
            const placeholders = sessionIds.map(() => '?').join(',');
            conditions.push(`session_id IN (${placeholders})`);
            condParams.push(...sessionIds);
        }
        if (map) {
            conditions.push('map_name = ?');
            condParams.push(map);
        }
        // Squadron filter: text LIKE handles fuzzy/orphan matches; if the input
        // resolves to a clan_id we also include matches by winning/losing
        // clan_id so renamed squadrons stay attached.
        if (squadron) {
            const sqClanId = resolveClanIdSync(squadron);
            if (sqClanId != null) {
                conditions.push(
                    '(winning_sq LIKE ? COLLATE NOCASE OR losing_sq LIKE ? COLLATE NOCASE '
                    + 'OR winning_clan_id = ? OR losing_clan_id = ?)'
                );
                condParams.push(`%${squadron}%`, `%${squadron}%`, sqClanId, sqClanId);
            } else {
                conditions.push('(winning_sq LIKE ? COLLATE NOCASE OR losing_sq LIKE ? COLLATE NOCASE)');
                condParams.push(`%${squadron}%`, `%${squadron}%`);
            }
        }
        if (timeFrom) {
            conditions.push('endtime_unix >= ?');
            condParams.push(timeFrom);
        }
        if (timeTo) {
            conditions.push('endtime_unix <= ?');
            condParams.push(timeTo);
        }

        const where = conditions.length > 0 ? ' WHERE ' + conditions.join(' AND ') : '';
        const limitClause = sessionIds !== null ? '' : ` LIMIT ?`;
        query = `SELECT ${selectCols} FROM match_summary${where} ORDER BY endtime_unix DESC${limitClause}`;
        params = sessionIds !== null ? condParams : [...condParams, limit];

        db.all(query, params, (err, rows) => {
            if (err) {
                log.error('Database error searching matches', err);
                return res.status(500).json({ error: 'Database error' });
            }

            loadSquadronLookupCached((squadronLookup) => {
                const matches = (rows || []).map(row => {
                    let winningTeam = null, losingTeam = null;
                    try { winningTeam = row.winning_team_json ? parseJsonColumn(row.winning_team_json) : null; } catch (e) {}
                    try { losingTeam = row.losing_team_json ? parseJsonColumn(row.losing_team_json) : null; } catch (e) {}

                    return {
                        session_id: row.session_id,
                        map_name: row.map_name,
                        endtime_unix: row.endtime_unix,
                        endtime_iso: new Date(row.endtime_unix * 1000).toISOString(),
                        winning_squadron: row.winning_sq,
                        losing_squadron: row.losing_sq,
                        winning_tag: cleanText(normalizeMatchTeam(winningTeam, squadronLookup, { squadron: row.winning_sq })?.squadron_tagged) || row.winning_sq,
                        losing_tag: cleanText(normalizeMatchTeam(losingTeam, squadronLookup, { squadron: row.losing_sq })?.squadron_tagged) || row.losing_sq,
                        winning_team: normalizeMatchTeam(winningTeam, squadronLookup, { squadron: row.winning_sq }),
                        losing_team: normalizeMatchTeam(losingTeam, squadronLookup, { squadron: row.losing_sq }),
                        game_type: row.game_type || ''
                    };
                });

                log.info('Games search completed', { player, map, squadron, time_from, time_to, results: matches.length, clientIP });
                res.json({ total_matches: matches.length, matches });
            });
        });
    });
});

// ── Distinct map names ──
app.get('/api/maps', (req, res) => {
    const cacheKey = 'maps_list';
    const cached = getCachedResponse(cacheKey);
    if (cached) return res.json(cached);

    db.all('SELECT DISTINCT map_name FROM match_summary WHERE map_name IS NOT NULL ORDER BY map_name', (err, rows) => {
        if (err) {
            log.error('Database error in /api/maps', err);
            return res.status(500).json({ error: 'Database error' });
        }
        // Deduplicate: strip mode prefix like "[Conquest #1] ", normalize case, filter blanks
        const seen = new Map(); // lowercase clean name -> first original clean name
        for (const r of rows) {
            const raw = (r.map_name || '').trim();
            if (!raw) continue;
            const clean = raw.replace(/^\s*\[[^\]]+\]\s*/, '').trim();
            if (!clean) continue;
            const key = clean.toLowerCase();
            if (!seen.has(key)) seen.set(key, clean);
        }
        const maps = [...seen.values()].sort((a, b) => a.localeCompare(b));
        const response = { maps };
        setCachedResponse(cacheKey, response);
        res.json(response);
    });
});

app.get('/api/seasons', (req, res) => {
    try {
        res.json(seasonsUtil.getSeasonDetails());
    } catch (err) {
        log.error('Error in /api/seasons', err);
        res.status(500).json({ error: 'Failed to load season data' });
    }
});

app.get('/api/squadrons/resolve', (req, res) => {
    const shorts = normalizeQueryList(req.query.short ?? req.query.shorts);
    const tags = normalizeQueryList(req.query.tag ?? req.query.tags);
    const longs = normalizeQueryList(req.query.long ?? req.query.longs);
    // `name=` is alias-aware: tries the live cache first, then squadron_name_aliases
    // (covers long/short/tag renames), squadron_name_history (legacy long_name),
    // and finally player_games_hist (catches short-tag renames not in the alias
    // table). Use it when the input could be a historical name.
    const names = normalizeQueryList(req.query.name ?? req.query.names);

    if (!shorts.length && !tags.length && !longs.length && !names.length) {
        return res.status(400).json({
            error: 'At least one short, tag, long, or name value is required'
        });
    }

    loadSquadronLookupCached((squadronLookup) => {
        const results = [];
        const seen = new Set();
        const append = (resolved) => {
            const dedupeKey = resolved.resolved
                ? `clan:${String(resolved.clan_id ?? resolved.short_name ?? resolved.tag_name).toLowerCase()}`
                : `input:${resolved.input.toLowerCase()}`;
            if (seen.has(dedupeKey)) return;
            seen.add(dedupeKey);
            results.push(resolved);
        };

        const appendTyped = (value, resolvedBy) => {
            const key = String(value || '').trim();
            if (!key) return;
            const row = squadronLookup[key] || squadronLookup[key.toLowerCase()] || null;
            append(formatSquadronResolution(key, row, resolvedBy));
        };

        shorts.forEach(value => appendTyped(value, 'short_name'));
        tags.forEach(value => appendTyped(value, 'tag_name'));
        longs.forEach(value => appendTyped(value, 'long_name'));

        const respond = () => {
            res.json({
                requested: { shorts, tags, longs, names },
                total_requested: shorts.length + tags.length + longs.length + names.length,
                total_found: results.filter(row => row.resolved).length,
                results
            });
        };

        if (!names.length) return respond();

        const squadronsDbPath = path.join(STORAGE_ROOT, 'squadrons.db');
        const rowForClanId = (cid) =>
            squadronLookup[`__cid_${cid}`] || squadronLookup[String(cid)] || null;
        const longNameForClanId = (cid) => {
            const row = rowForClanId(cid);
            return row ? row.long_name : null;
        };

        const buildFromClanId = (key, cid, via) => {
            const row = rowForClanId(cid);
            if (row) {
                const r = formatSquadronResolution(key, row, 'name');
                r.via = via;
                return r;
            }
            const r = formatSquadronResolution(key, null, 'name');
            r.resolved = true;
            r.clan_id = cid;
            r.long_name = longNameForClanId(cid);
            r.via = via;
            return r;
        };

        const resolveOneName = (rawName) => new Promise((done) => {
            const key = String(rawName || '').trim();
            if (!key) return done(null);

            const hit = squadronLookup[key] || squadronLookup[key.toLowerCase()];
            if (hit && hit.clan_id != null) {
                const r = formatSquadronResolution(key, hit, 'name');
                r.via = 'squadrons_data';
                return done(r);
            }

            const tryPlayerGamesHist = () => {
                db.get(
                    `SELECT clan_id FROM player_games_hist
                     WHERE squadron_name = ? COLLATE NOCASE AND clan_id IS NOT NULL
                     ORDER BY endtime_unix DESC LIMIT 1`,
                    [key],
                    (qerr, row) => {
                        if (qerr || !row || row.clan_id == null) {
                            return done(formatSquadronResolution(key, null, 'name'));
                        }
                        done(buildFromClanId(key, row.clan_id, 'player_games_hist'));
                    }
                );
            };

            if (!fs.existsSync(squadronsDbPath)) return tryPlayerGamesHist();

            const sdb = new sqlite3.Database(squadronsDbPath, sqlite3.OPEN_READONLY, (oerr) => {
                if (oerr) return tryPlayerGamesHist();
                sdb.get(
                    `SELECT clan_id FROM squadron_name_aliases
                     WHERE LOWER(name) = LOWER(?)
                     ORDER BY last_seen DESC LIMIT 1`,
                    [key],
                    (aerr, arow) => {
                        if (!aerr && arow && arow.clan_id != null) {
                            try { sdb.close(); } catch (_) {}
                            return done(buildFromClanId(key, arow.clan_id, 'name_aliases'));
                        }
                        sdb.get(
                            `SELECT clan_id FROM squadron_name_history
                             WHERE LOWER(long_name) = LOWER(?)
                             ORDER BY last_seen DESC LIMIT 1`,
                            [key],
                            (herr, hrow) => {
                                try { sdb.close(); } catch (_) {}
                                if (!herr && hrow && hrow.clan_id != null) {
                                    return done(buildFromClanId(key, hrow.clan_id, 'name_history'));
                                }
                                tryPlayerGamesHist();
                            }
                        );
                    }
                );
            });
        });

        Promise.all(names.map(resolveOneName)).then((nameResults) => {
            for (const r of nameResults) {
                if (r) append(r);
            }
            respond();
        });
    });
});

const API_INFO = {
    name: "SREBOT Player API",
    version: "2.3.0",
    endpoints: {
        "GET /api/player/:uid": "Get player totals and per-vehicle stats by UID",
        "GET /api/player/:uid/games": "Get individual game rows for a player",
        "GET /api/player/:uid/history": "Get day-by-day player history",
        "GET /api/search/:nickname": "Search for players by nickname",
        "GET /api/live": "Get recent match summaries",
        "GET /api/match/:sessionId": "Get a single match summary by session ID",
        "GET /api/match/:sessionId/replay": "Get replay data for a match if available",
        "GET /api/match/:sessionId/scoreboard": "Get the full scoreboard context for a match",
        "GET /api/games/search": "Search matches by player, map, squadron, or time range",
        "GET /api/maps": "List distinct map names from match history",
        "GET /api/seasons": "Return season schedule data for filtering",
        "GET /api/squadrons/resolve": "Resolve squadron short/tag/long (current only) or name= (alias-aware: includes renamed squadrons) to canonical metadata",
        "GET /api/squadrons/:squadronname/comps": "Get recent comp snapshots for a squadron",
        "GET /api/leaderboard/players": "Get global player leaderboards",
        "GET /api/leaderboard/squadrons": "Get squadron leaderboards",
        "GET /api/leaderboard/vehicles": "Get vehicle-specific leaderboards",
        "GET /api/leaderboard/stats": "Get overall leaderboard statistics and top vehicles",
        "GET /api/squadrons/:squadronname": "Get squadron roster stats and summary",
        "GET /api/squadrons/:squadronname/history": "Get squadron battle and rating history",
        "GET /api/squadrons/:squadronname/games": "Get squadron match list",
        "GET /health": "Health check endpoint",
        "GET /api/info": "API information"
    },
    filtering: {
        core_query_params: {
            start_date: "ISO date string or Unix timestamp, inclusive lower bound",
            end_date: "ISO date string or Unix timestamp, inclusive upper bound"
        },
        season_filtering: "Use GET /api/seasons to map season/week windows to timestamps before querying the data endpoints.",
        response_metadata: "Filtered responses include a date_filter object describing the applied range"
    },
    databases: [
        {
            file: "sq_battles.db",
            purpose: "Primary battle history store and match summary cache",
            tables: [
                {
                    name: "player_games_hist",
                    description: "One row per player per battle / vehicle. This is the main source for player stats, games, history, search, leaderboards, squadron stats, and battle debug endpoints.",
                    used_by: [
                        "GET /api/player/:uid",
                        "GET /api/player/:uid/games",
                        "GET /api/player/:uid/history",
                        "GET /api/search/:nickname",
                        "GET /api/games/search",
                        "GET /api/leaderboard/players",
                        "GET /api/leaderboard/vehicles",
                        "GET /api/leaderboard/squadrons",
                        "GET /api/leaderboard/stats",
                        "GET /api/squadrons/:squadronname",
                        "GET /api/squadrons/:squadronname/history",
                        "GET /api/squadrons/:squadronname/games",
                        "GET /api/debug/stats",
                        "GET /api/debug/schema",
                        "GET /api/debug/player-sample",
                        "GET /api/debug/player-count/:uid",
                        "GET /api/debug/migration-status"
                    ]
                },
                {
                    name: "match_summary",
                    description: "One row per match. Stores the per-session map, winner/loser, team blobs, timestamps, and replay availability metadata.",
                    used_by: [
                        "GET /api/live",
                        "GET /api/match/:sessionId",
                        "GET /api/match/:sessionId/replay",
                        "GET /api/games/search",
                        "GET /api/maps",
                        "GET /api/squadrons/:squadronname/games",
                        "GET /api/debug/stats"
                    ]
                }
            ]
        },
        {
            file: "squadrons.db",
            purpose: "Squadron roster cache, live squadron metadata, and historical squadron point snapshots",
            tables: [
                {
                    name: "squadrons_data",
                    description: "Canonical squadron directory and latest leaderboard snapshot. Stores clan IDs, names, tags, membership counts, and current rating fields.",
                    used_by: [
                        "GET /api/leaderboard/squadrons",
                        "GET /api/leaderboard/stats",
                        "GET /api/squadrons/resolve",
                        "GET /api/squadrons/:squadronname",
                        "GET /api/squadrons/:squadronname/history",
                        "GET /api/squadrons/:squadronname/games",
                        "GET /api/debug/squadrons-db-schema",
                        "background squadron sync jobs"
                    ]
                },
                {
                    name: "squadron_members",
                    description: "Current per-squadron member roster with cached nick and points values.",
                    used_by: [
                        "GET /api/squadrons/:squadronname",
                        "GET /api/squadrons/:squadronname/games",
                        "GET /api/leaderboard/squadrons",
                        "background squadron member sync jobs"
                    ]
                },
                {
                    name: "squadrons_points",
                    description: "Historical squadron point snapshots keyed by unix time. Used for historical squadron point lookups and charts.",
                    used_by: [
                        "GET /api/squadrons/:squadronname",
                        "GET /api/squadrons/:squadronname/history",
                        "background squadron points sync jobs"
                    ]
                }
            ]
        },
        {
            file: "wl.db",
            purpose: "Read-only win/loss standings for scoreboard rendering and recent match context",
            tables: [
                {
                    name: "wl_events",
                    description: "Idempotent log of processed battle result events.",
                    used_by: [
                        "GET /api/match/:sessionId/scoreboard"
                    ]
                },
                {
                    name: "wl_standings",
                    description: "Current win/loss totals per squadron.",
                    used_by: [
                        "GET /api/match/:sessionId/scoreboard"
                    ]
                }
            ]
        },
        {
            file: "points.db",
            purpose: "Read-only cached point diffs for scoreboard rendering",
            tables: [
                {
                    name: "profile_member_points",
                    description: "Current cached points per squadron member.",
                    used_by: [
                        "internal replay processing",
                        "scoreboard point diff caching"
                    ]
                },
                {
                    name: "profile_totals",
                    description: "Current cached total points per squadron.",
                    used_by: [
                        "internal replay processing",
                        "scoreboard point diff caching"
                    ]
                },
                {
                    name: "game_cache",
                    description: "Per-session cached point diffs and updated snapshots.",
                    used_by: [
                        "GET /api/match/:sessionId/scoreboard",
                        "internal replay processing"
                    ]
                }
            ]
        }
    ]
};

app.get('/api/info', (req, res) => {
    res.json(API_INFO);
});

app.get('/api/debug/player-sample', requireAdminBearer, (req, res) => {
    const sampleQuery = `
        SELECT UID, nick, session_id, vehicle, ground_kills, air_kills, assists, captures, deaths, victor_bool
        FROM player_games_hist 
        WHERE UID IS NOT NULL
        LIMIT 10
    `;
    
    db.all(sampleQuery, [], (err, rows) => {
        if (err) {
            return res.status(500).json({ error: err.message });
        }
        res.json({ sample_data: rows });
    });
});

app.get('/api/debug/player-count/:uid', requireAdminBearer, (req, res) => {
    const { uid } = req.params;
    const countQuery = `
        SELECT 
            UID,
            COUNT(*) as total_records,
            COUNT(DISTINCT session_id) as unique_sessions,
            COUNT(DISTINCT vehicle) as unique_vehicles,
            GROUP_CONCAT(DISTINCT vehicle) as vehicles_used
        FROM player_games_hist 
        WHERE UID = ?
        GROUP BY UID
    `;
    
    db.get(countQuery, [uid], (err, row) => {
        if (err) {
            return res.status(500).json({ error: err.message });
        }
        res.json({ player_analysis: row });
    });
});

app.get('/api/debug/schema', requireAdminBearer, (req, res) => {
    const schemaQuery = `PRAGMA table_info(player_games_hist)`;
    const sampleQuery = `SELECT * FROM player_games_hist LIMIT 3`;
    
    db.all(schemaQuery, (err, schema) => {
        if (err) {
            log.error('Database error in schema query', err);
            return res.status(500).json({ error: 'Database schema error', details: err.message });
        }
        
        db.all(sampleQuery, (err, samples) => {
            if (err) {
                log.error('Database error in sample query', err);
                return res.status(500).json({ error: 'Database sample error', details: err.message });
            }
            
            const columnNames = samples.length > 0 ? Object.keys(samples[0]) : [];
            
            res.json({
                table_name: 'player_games_hist',
                schema: schema.map(col => ({
                    name: col.name,
                    type: col.type,
                    notnull: col.notnull,
                    default_value: col.dflt_value,
                    primary_key: col.pk
                })),
                column_names: columnNames,
                sample_records: samples,
                total_columns: schema.length,
                timestamp: new Date().toISOString()
            });
        });
    });
});

app.get('/api/debug/squadron-names', requireAdminBearer, (req, res) => {
    const squadronNamesQuery = `
        SELECT DISTINCT squadron_name, COUNT(*) as record_count 
        FROM player_games_hist 
        WHERE squadron_name IS NOT NULL AND squadron_name != 'UNKNOWN' 
        GROUP BY squadron_name 
        ORDER BY squadron_name 
        LIMIT 50
    `;
    
    db.all(squadronNamesQuery, [], (err, rows) => {
        if (err) {
            log.error('Database error in squadron names debug query', err);
            return res.status(500).json({
                error: 'Database error occurred',
                errorCode: 'DB_SQUADRON_NAMES_DEBUG_FAILED'
            });
        }
        
        const analysis = {
            total_unique_names: rows.length,
            squadron_names: rows,
            patterns: {
                bracketed: rows.filter(row => row.squadron_name.includes('[') && row.squadron_name.includes(']')),
                non_bracketed: rows.filter(row => !row.squadron_name.includes('[') && !row.squadron_name.includes(']')),
                mixed: rows.filter(row => (row.squadron_name.includes('[') || row.squadron_name.includes(']')) && 
                                         !(row.squadron_name.includes('[') && row.squadron_name.includes(']')))
            }
        };
        
        analysis.potential_duplicates = [];
        for (let i = 0; i < rows.length; i++) {
            for (let j = i + 1; j < rows.length; j++) {
                const name1 = rows[i].squadron_name;
                const name2 = rows[j].squadron_name;
                const normalized1 = name1.replace(/[\[\]]/g, '').trim();
                const normalized2 = name2.replace(/[\[\]]/g, '').trim();
                
                if (normalized1.toLowerCase() === normalized2.toLowerCase()) {
                    analysis.potential_duplicates.push({
                        name1: name1,
                        name2: name2,
                        normalized: normalized1,
                        records1: rows[i].record_count,
                        records2: rows[j].record_count
                    });
                }
            }
        }
        
        res.json(analysis);
    });
});

app.get('/api/debug/migration-status', requireAdminBearer, (req, res) => {
    const schemaQuery = `PRAGMA table_info(player_games_hist)`;
    
    db.all(schemaQuery, (err, schema) => {
        if (err) {
            log.error('Database error in migration status query', err);
            return res.status(500).json({ 
                error: 'Database schema error', 
                details: err.message 
            });
        }
        
        const columns = schema.map(col => col.name);
        const hasSquadronColumn = columns.includes('squadron_name');
        
        res.json({
            table_name: 'player_games_hist',
            has_squadron_column: hasSquadronColumn,
            missing_columns: hasSquadronColumn ? [] : ['squadron_name'],
            all_columns: columns,
            migration_needed: !hasSquadronColumn,
            migration_sql: hasSquadronColumn ? null : "ALTER TABLE player_games_hist ADD COLUMN squadron_name TEXT NOT NULL DEFAULT 'UNKNOWN'",
            timestamp: new Date().toISOString()
        });
    });
});

app.get('/api/debug/squadrons-db-schema', requireAdminBearer, (req, res) => {
    const squadronsDbPath = path.join(STORAGE_ROOT, 'squadrons.db');
    
    if (!fs.existsSync(squadronsDbPath)) {
        return res.status(404).json({
            error: 'Squadrons database not found',
            path: squadronsDbPath
        });
    }
    
    const squadronsDb = new sqlite3.Database(squadronsDbPath, sqlite3.OPEN_READONLY, (err) => {
        if (err) {
            return res.status(500).json({
                error: 'Failed to open squadrons database',
                details: err.message
            });
        }
        
        // Get all table names first
        const tablesQuery = `SELECT name FROM sqlite_master WHERE type='table'`;
        
        squadronsDb.all(tablesQuery, (err, tables) => {
            if (err) {
                squadronsDb.close();
                return res.status(500).json({
                    error: 'Failed to get table list',
                    details: err.message
                });
            }
            
            const tableSchemas = {};
            let completedTables = 0;
            
            if (tables.length === 0) {
                squadronsDb.close();
                return res.json({
                    database_path: squadronsDbPath,
                    tables: {},
                    message: 'No tables found in squadrons database'
                });
            }
            
            tables.forEach(table => {
                const tableName = table.name;
                const schemaQuery = `PRAGMA table_info(${tableName})`;
                const sampleQuery = `SELECT * FROM ${tableName} LIMIT 3`;
                
                squadronsDb.all(schemaQuery, (err, schema) => {
                    if (err) {
                        tableSchemas[tableName] = { error: err.message };
                        completedTables++;
                        if (completedTables === tables.length) {
                            squadronsDb.close();
                            res.json({
                                database_path: squadronsDbPath,
                                tables: tableSchemas,
                                timestamp: new Date().toISOString()
                            });
                        }
                        return;
                    }
                    
                    squadronsDb.all(sampleQuery, (err, samples) => {
                        if (err) samples = [];
                        
                        tableSchemas[tableName] = {
                            schema: schema,
                            sample_records: samples,
                            record_count: samples.length
                        };
                        
                        completedTables++;
                        if (completedTables === tables.length) {
                            squadronsDb.close();
                            res.json({
                                database_path: squadronsDbPath,
                                tables: tableSchemas,
                                timestamp: new Date().toISOString()
                            });
                        }
                    });
                });
            });
        });
    });
});

app.get('/api/leaderboard/players', (req, res) => {
    const dateFilters = parseDateFilters(req);
    const { start_date, end_date, season, week } = req.query;

    // `limit` trims the response array; the full sorted set is cached under a
    // limit-agnostic key so different callers share work. Omit `limit` to get
    // every player. Cap at 10k to keep payloads sane on accidental large values.
    const rawLimit = parseInt(req.query.limit, 10);
    const limit = Number.isFinite(rawLimit) && rawLimit > 0
        ? Math.min(rawLimit, 10000)
        : null;

    log.info('Player leaderboard request received', { limit: limit ?? 'all' });

    const applyLimit = (full) => {
        if (limit === null) return full;
        return { ...full, players: full.players.slice(0, limit), limit, returned: Math.min(limit, full.players.length) };
    };

    const cacheKey = `leaderboard_players_${start_date || 'all'}_${end_date || 'all'}`;
    const cached = getCachedResponse(cacheKey);
    if (cached) {
        log.info('Returning cached leaderboard response');
        return res.json(applyLimit(cached));
    }

    if (!dateFilters.hasFilter) {
        return res.status(400).json({
            error: 'A date filter (start_date/end_date/season/week) is required for uncached all-time player leaderboard queries.',
            errorCode: 'FILTER_REQUIRED'
        });
    }

    // Dedup: if this exact query is already running, wait for it
    dedup(cacheKey, () => new Promise((resolve, reject) => {
        // Step 1: Pure aggregation — no nick lookups, single scan
        const statsQuery = `
            SELECT
                p.UID as uid,
                SUM(ground_kills) as total_ground_kills,
                SUM(air_kills) as total_air_kills,
                SUM(ground_kills + air_kills) as total_kills,
                SUM(assists) as total_assists,
                SUM(captures) as total_captures,
                SUM(deaths) as total_deaths,
                COUNT(DISTINCT session_id) as total_battles,
                SUM(CASE WHEN UPPER(victor_bool) = 'WIN' THEN 1 ELSE 0 END) as wins,
                SUM(ground_kills + air_kills + assists * 0.5 + captures * 2) as total_score
            FROM player_games_hist p
            WHERE p.UID IS NOT NULL
                AND (? IS NULL OR endtime_unix >= ?)
                AND (? IS NULL OR endtime_unix <= ?)
            GROUP BY p.UID
            HAVING SUM(ground_kills + air_kills) > 0
            ORDER BY total_score DESC
        `;

        const queryParams = [
            dateFilters.startTimestamp, dateFilters.startTimestamp,
            dateFilters.endTimestamp, dateFilters.endTimestamp
        ];

        const queryStart = Date.now();
        heavyDb.all(statsQuery, queryParams, (err, statsRows) => {
            if (err) {
                log.error('Database error in player leaderboard aggregation', err);
                reject(err);
                return res.status(500).json({ error: 'Database error', errorCode: 'DB_PLAYER_LEADERBOARD_FAILED' });
            }

            log.info('Player leaderboard aggregation done', { rows: statsRows.length, ms: Date.now() - queryStart });

            // Step 2: Nick/squadron lookup from squadron_members cache (instant, no heavy SQL)
            loadNickLookupCached((nickCache) => {
              loadSquadronLookupCached((squadronLookup) => {
                const uncoveredUids = statsRows.filter(r => !nickCache[r.uid]).map(r => r.uid);

                const buildResponse = (fallbackMap) => {
                    const response = {
                        date_filter: createDateFilterMetadata(dateFilters, start_date, end_date, season, week),
                        timeframe: dateFilters.hasFilter ? "filtered" : "all-time",
                        total_players: statsRows.length,
                        players: statsRows.map(row => {
                            const totalKills = row.total_kills || 0;
                            const deaths = row.total_deaths || 0;
                            const wins = row.wins || 0;
                            const totalBattles = row.total_battles || 0;
                            const kdr = deaths > 0 ? (totalKills / deaths) : totalKills;
                            const winRate = totalBattles > 0 ? (wins / totalBattles) * 100 : 0;

                            const cached = nickCache[row.uid];
                            const fb = fallbackMap[row.uid];
                            const nick = cached ? cached.nick : (fb ? fb.nick : row.uid);
                            const sqId = resolveSquadronIdentity(cached, fb, squadronLookup);

                            return {
                                uid: row.uid,
                                nick,
                                squadron_name: sqId.tag_name,
                                squadron_short_name: sqId.short_name,
                                squadron_clan_id: sqId.clan_id,
                                total_kills: totalKills,
                                ground_kills: row.total_ground_kills || 0,
                                air_kills: row.total_air_kills || 0,
                                total_battles: totalBattles,
                                wins,
                                win_rate: parseFloat(winRate.toFixed(1)),
                                kdr: parseFloat(kdr.toFixed(1)),
                                deaths,
                                assists: row.total_assists || 0,
                                captures: row.total_captures || 0,
                                total_score: Math.round(row.total_score || 0)
                            };
                        })
                    };

                    log.info('Player leaderboard complete', {
                        playersReturned: statsRows.length,
                        cachedNicks: statsRows.length - uncoveredUids.length,
                        fallbackNicks: Object.keys(fallbackMap).length,
                        uncoveredNicks: uncoveredUids.length - Object.keys(fallbackMap).length,
                        totalMs: Date.now() - queryStart
                    });

                    setCachedResponse(cacheKey, response);
                    resolve(response);
                    res.json(applyLimit(response));
                };

                // Fallback for players not in any squadron — uses (UID, endtime_unix) index
                if (uncoveredUids.length > 0) {
                    const fbPlaceholders = uncoveredUids.map(() => '?').join(',');
                    db.all(`
                        SELECT UID as uid, nick, squadron_name, MAX(endtime_unix)
                        FROM player_games_hist
                        WHERE UID IN (${fbPlaceholders}) AND nick NOT LIKE 'coop/%'
                        GROUP BY UID
                    `, uncoveredUids, (err, fbRows) => {
                        const fbMap = {};
                        if (!err && fbRows) fbRows.forEach(r => { fbMap[r.uid] = r; });
                        if (err) log.warn('Fallback nick lookup failed', { error: err.message });
                        buildResponse(fbMap);
                    });
                } else {
                    buildResponse({});
                }
              });
            });
        });
    })).catch(err => {
        if (!res.headersSent) {
            res.status(500).json({ error: 'Database error', errorCode: 'DB_PLAYER_LEADERBOARD_FAILED' });
        }
    });
});

app.get('/api/leaderboard/vehicles', (req, res) => {
    const { vehicle } = req.query;

    const dateFilters = parseDateFilters(req);
    const { start_date, end_date, season, week } = req.query;

    // `limit` trims the response array; cache key omits limit so different
    // callers share the heavy aggregation. Default unlimited, cap at 10k.
    const rawLimit = parseInt(req.query.limit, 10);
    const limit = Number.isFinite(rawLimit) && rawLimit > 0
        ? Math.min(rawLimit, 10000)
        : null;

    log.info('Vehicle leaderboard request received', { vehicleFilter: vehicle, limit: limit ?? 'all' });

    const applyLimit = (full) => {
        if (limit === null) return full;
        return { ...full, vehicles: full.vehicles.slice(0, limit), limit, returned: Math.min(limit, full.vehicles.length) };
    };

    const cacheKey = `leaderboard_vehicles_${vehicle || 'all'}_${start_date || 'all'}_${end_date || 'all'}`;
    const cached = getCachedResponse(cacheKey);
    if (cached) {
        log.info('Returning cached vehicle leaderboard');
        return res.json(applyLimit(cached));
    }

    if (!vehicle && !dateFilters.hasFilter) {
        return res.status(400).json({
            error: 'A date filter (start_date/end_date/season/week) or vehicle filter is required for uncached all-time vehicle leaderboard queries.',
            errorCode: 'FILTER_REQUIRED'
        });
    }

    dedup(cacheKey, () => new Promise((resolve, reject) => {
        let statsQuery;
        let queryParams;

        if (vehicle) {
            statsQuery = `
                SELECT vehicle, MAX(vehicle_internal) as vehicle_internal, p.UID as player_uid,
                    SUM(ground_kills) as total_ground_kills, SUM(air_kills) as total_air_kills,
                    SUM(ground_kills + air_kills) as total_kills,
                    SUM(assists) as total_assists, SUM(captures) as total_captures,
                    SUM(deaths) as total_deaths, COUNT(*) as battles,
                    SUM(CASE WHEN UPPER(victor_bool) = 'WIN' THEN 1 ELSE 0 END) as wins
                FROM player_games_hist p
                WHERE vehicle = ? COLLATE NOCASE AND p.UID IS NOT NULL
                    AND (? IS NULL OR endtime_unix >= ?) AND (? IS NULL OR endtime_unix <= ?)
                GROUP BY p.UID, vehicle
                HAVING SUM(ground_kills + air_kills) > 0
                ORDER BY total_kills DESC, battles DESC
            `;
            queryParams = [vehicle, dateFilters.startTimestamp, dateFilters.startTimestamp, dateFilters.endTimestamp, dateFilters.endTimestamp];
        } else {
            statsQuery = `
                SELECT vehicle, MAX(vehicle_internal) as vehicle_internal, p.UID as player_uid,
                    SUM(ground_kills) as total_ground_kills, SUM(air_kills) as total_air_kills,
                    SUM(ground_kills + air_kills) as total_kills,
                    SUM(assists) as total_assists, SUM(captures) as total_captures,
                    SUM(deaths) as total_deaths, COUNT(*) as battles,
                    SUM(CASE WHEN UPPER(victor_bool) = 'WIN' THEN 1 ELSE 0 END) as wins
                FROM player_games_hist p
                WHERE p.UID IS NOT NULL
                    AND (? IS NULL OR endtime_unix >= ?) AND (? IS NULL OR endtime_unix <= ?)
                GROUP BY p.UID, vehicle
                HAVING SUM(ground_kills + air_kills) > 0
                ORDER BY total_kills DESC, battles DESC
            `;
            queryParams = [dateFilters.startTimestamp, dateFilters.startTimestamp, dateFilters.endTimestamp, dateFilters.endTimestamp];
        }

        const queryStart = Date.now();
        heavyDb.all(statsQuery, queryParams, (err, vehicleRows) => {
            if (err) {
                log.error('Database error in vehicle leaderboard', err);
                reject(err);
                return res.status(500).json({ error: 'Database error', errorCode: 'DB_VEHICLE_LEADERBOARD_FAILED' });
            }

            // Nick/squadron lookup from squadron_members cache (instant)
            loadNickLookupCached((nickCache) => {
              loadSquadronLookupCached((squadronLookup) => {
                const uniqueUids = [...new Set(vehicleRows.map(r => r.player_uid))];
                const uncoveredUids = uniqueUids.filter(uid => !nickCache[uid]);

                const buildResponse = (fallbackMap) => {
                    const response = {
                        date_filter: createDateFilterMetadata(dateFilters, start_date, end_date, season, week),
                        vehicles: vehicleRows.map(row => {
                            const totalKills = row.total_kills || 0;
                            const deaths = row.total_deaths || 0;
                            const wins = row.wins || 0;
                            const battles = row.battles || 0;
                            const kdr = deaths > 0 ? (totalKills / deaths) : totalKills;
                            const winRate = battles > 0 ? (wins / battles) * 100 : 0;

                            const cached = nickCache[row.player_uid];
                            const fb = fallbackMap[row.player_uid];
                            const playerNick = cached ? cached.nick : (fb ? fb.nick : row.player_uid);
                            const sqId = resolveSquadronIdentity(cached, fb, squadronLookup);

                            return {
                                vehicle: row.vehicle,
                                vehicle_internal: row.vehicle_internal || null,
                                player_uid: normalizeUid(row.player_uid),
                                player_nick: playerNick,
                                player_squadron_name: sqId.tag_name,
                                player_squadron_short_name: sqId.short_name,
                                player_squadron_clan_id: sqId.clan_id,
                                total_kills: totalKills,
                                ground_kills: row.total_ground_kills || 0,
                                air_kills: row.total_air_kills || 0,
                                battles, wins,
                                win_rate: parseFloat(winRate.toFixed(1)),
                                kdr: parseFloat(kdr.toFixed(1)),
                                deaths,
                                assists: row.total_assists || 0,
                                captures: row.total_captures || 0
                            };
                        })
                    };

                    log.info('Vehicle leaderboard complete', { vehiclesReturned: vehicleRows.length, totalMs: Date.now() - queryStart });
                    setCachedResponse(cacheKey, response);
                    resolve(response);
                    res.json(applyLimit(response));
                };

                if (uncoveredUids.length > 0) {
                    const fbPlaceholders = uncoveredUids.map(() => '?').join(',');
                    db.all(`
                        SELECT UID as uid, nick, squadron_name, MAX(endtime_unix)
                        FROM player_games_hist
                        WHERE UID IN (${fbPlaceholders}) AND nick NOT LIKE 'coop/%'
                        GROUP BY UID
                    `, uncoveredUids, (err, fbRows) => {
                        const fbMap = {};
                        if (!err && fbRows) fbRows.forEach(r => { fbMap[r.uid] = r; });
                        buildResponse(fbMap);
                    });
                } else {
                    buildResponse({});
                }
              });
            });
        });
    })).catch(err => {
        if (!res.headersSent) {
            res.status(500).json({ error: 'Database error', errorCode: 'DB_VEHICLE_LEADERBOARD_FAILED' });
        }
    });
});

app.get('/api/leaderboard/squadrons', (req, res) => {
    log.info('Squadron leaderboard request received');

    // Parse date filters
    const dateFilters = parseDateFilters(req);
    const { start_date, end_date, season, week } = req.query;
    const rawLimit = parseInt(req.query.limit, 10);
    const limit = Number.isFinite(rawLimit) && rawLimit > 0
        ? Math.min(rawLimit, 10000)
        : null;

    const applyLimit = (full) => {
        if (limit === null) return full;
        return {
            ...full,
            squadrons: full.squadrons.slice(0, limit),
            limit,
            returned: Math.min(limit, full.squadrons.length),
        };
    };

    const cacheKey = `leaderboard_squadrons_${start_date || 'all'}_${end_date || 'all'}`;
    const cached = getCachedResponse(cacheKey);
    if (cached) {
        log.info('Returning cached squadron leaderboard');
        return res.json(applyLimit(cached));
    }

    dedup(cacheKey, () => new Promise((resolve, reject) => {
        const schemaQuery = `PRAGMA table_info(player_games_hist)`;

        db.all(schemaQuery, (err, schema) => {
            if (err) {
                log.error('Database error in schema check', err);
                return reject({ status: 500, body: { error: 'Database schema error', errorCode: 'DB_SCHEMA_CHECK_FAILED' } });
            }

            const hasSquadronColumn = schema.some(col => col.name === 'squadron_name');

            if (!hasSquadronColumn) {
                log.info('Squadron column missing - returning empty squadron leaderboard');
                const emptyResponse = {
                    date_filter: createDateFilterMetadata(dateFilters, start_date, end_date, season, week),
                    timeframe: dateFilters.hasFilter ? "filtered" : "all-time",
                    total_squadrons: 0,
                    squadrons: [],
                    migration_needed: true,
                    message: 'Squadron data not available - squadron_name column missing from database'
                };
                setCachedResponse(cacheKey, emptyResponse);
                return resolve(emptyResponse);
            }

            // Group by (clan_id, squadron_name) so renamed squadrons stay
            // attached via clan_id while orphans (clan_id IS NULL) still group
            // by their text. JS-side consolidation collapses by clan_id below.
            const squadronStatsQuery = `
            SELECT
                clan_id,
                squadron_name,
                COUNT(DISTINCT UID) as player_count,
                SUM(ground_kills) as total_ground_kills,
                SUM(air_kills) as total_air_kills,
                SUM(ground_kills + air_kills) as total_kills,
                SUM(assists) as total_assists,
                SUM(captures) as total_captures,
                SUM(deaths) as total_deaths,
                COUNT(*) as total_battles,
                SUM(CASE WHEN UPPER(victor_bool) = 'WIN' THEN 1 ELSE 0 END) as wins
            FROM player_games_hist
            WHERE squadron_name IS NOT NULL
                AND squadron_name != 'UNKNOWN'
                AND (? IS NULL OR endtime_unix >= ?)
                AND (? IS NULL OR endtime_unix <= ?)
            GROUP BY clan_id, squadron_name
            HAVING SUM(ground_kills + air_kills) > 0
            ORDER BY SUM(ground_kills + air_kills) DESC
        `;

        const totalSquadronsQuery = `
            SELECT COUNT(DISTINCT COALESCE(CAST(clan_id AS TEXT), squadron_name)) as total_squadrons
            FROM player_games_hist
            WHERE squadron_name IS NOT NULL
                AND squadron_name != 'UNKNOWN'
                AND (? IS NULL OR endtime_unix >= ?)
                AND (? IS NULL OR endtime_unix <= ?)
        `;

        // Build query parameters with date filtering
        const queryParams = [
            dateFilters.startTimestamp,
            dateFilters.startTimestamp,
            dateFilters.endTimestamp,
            dateFilters.endTimestamp
        ];

        heavyDb.get(totalSquadronsQuery, queryParams, (err, totalRow) => {
            if (err) {
                log.error('Database error in total squadrons query', err);
                return reject({ status: 500, body: { error: 'Database error occurred', errorCode: 'DB_TOTAL_SQUADRONS_FAILED' } });
            }

            heavyDb.all(squadronStatsQuery, queryParams, (err, squadronRows) => {
            if (err) {
                log.error('Database error in squadron leaderboard query', err);
                return reject({ status: 500, body: { error: 'Database error occurred', errorCode: 'DB_SQUADRON_LEADERBOARD_FAILED' } });
            }

            log.info('DEBUG: Raw squadron query results', {
                rowCount: squadronRows.length,
                firstFewRows: squadronRows.slice(0, 3),
                queryUsed: squadronStatsQuery.replace(/\s+/g, ' ').trim()
            });

            const squadronsDbPath = path.join(STORAGE_ROOT, 'squadrons.db');
            let squadronLookup = {};
            let totalSquadrons = totalRow.total_squadrons;

            const loadSquadronLookup = (callback) => {
                if (!fs.existsSync(squadronsDbPath)) {
                    return callback();
                }

                const squadronsDb = new sqlite3.Database(squadronsDbPath, sqlite3.OPEN_READONLY, (squadronsErr) => {
                    if (squadronsErr) {
                        log.error('Failed to open squadrons database', squadronsErr);
                        return callback();
                    }

                const squadronLookupQuery = `SELECT clan_id, long_name, short_name, tag_name, members, clanrating FROM squadrons_data`;
                squadronsDb.all(squadronLookupQuery, [], (err, squadronLookupRows) => {
                    squadronsDb.close();

                    if (err) {
                        log.error('Error loading squadron lookup data', err);
                        return callback();
                    }

                    squadronLookupRows.forEach(lookup => {
                        if (lookup.long_name) squadronLookup[lookup.long_name] = lookup;
                        if (lookup.short_name) squadronLookup[lookup.short_name] = lookup;
                        if (lookup.tag_name) squadronLookup[lookup.tag_name] = lookup;
                        if (lookup.clan_id != null) squadronLookup[`__cid_${lookup.clan_id}`] = lookup;
                    });

                    callback();
                });
            });
            };

            loadSquadronLookup(() => {
                const consolidatedSquadrons = {};
                squadronRows.forEach(row => {
                    // Prefer clan_id-keyed lookup so renamed squadrons consolidate
                    // under their current long_name; fall back to text lookup for
                    // rows whose clan_id wasn't backfilled.
                    const lookup = (row.clan_id != null && squadronLookup[`__cid_${row.clan_id}`])
                        || squadronLookup[row.squadron_name];
                    const canonicalName = lookup ? lookup.long_name : row.squadron_name;
                    const tagName = lookup ? lookup.tag_name : row.squadron_name;
                    // Group key is clan_id when known; orphans group by name.
                    const consolidationKey = (lookup && lookup.clan_id != null)
                        ? `__cid_${lookup.clan_id}`
                        : canonicalName;

                    if (!consolidatedSquadrons[consolidationKey]) {
                        consolidatedSquadrons[consolidationKey] = {
                            clan_id: lookup ? lookup.clan_id : null,
                            tag_name: tagName,
                            short_name: lookup ? lookup.short_name : null,
                            long_name: canonicalName,
                            player_count: lookup ? (lookup.members || 0) : 0,
                            total_kills: 0,
                            ground_kills: 0,
                            air_kills: 0,
                            total_battles: 0,
                            wins: 0,
                            deaths: 0,
                            assists: 0,
                            captures: 0
                        };
                    }

                    const consolidated = consolidatedSquadrons[consolidationKey];
                    consolidated.total_kills += row.total_kills || 0;
                    consolidated.ground_kills += row.total_ground_kills || 0;
                    consolidated.air_kills += row.total_air_kills || 0;
                    consolidated.total_battles += row.total_battles || 0;
                    consolidated.wins += row.wins || 0;
                    consolidated.deaths += row.total_deaths || 0;
                    consolidated.assists += row.total_assists || 0;
                    consolidated.captures += row.total_captures || 0;
                });

                // Get points from squadron_members table
                const getSquadronPoints = () => {
                    return new Promise((resolve) => {
                        if (!fs.existsSync(squadronsDbPath)) {
                            return resolve({});
                        }

                        const pointsDb = new sqlite3.Database(squadronsDbPath, sqlite3.OPEN_READONLY, (err) => {
                            if (err) {
                                log.error('Failed to open squadrons.db for points lookup', err);
                                return resolve({});
                            }

                            pointsDb.all(`
                                SELECT long_name, clanrating as total_points
                                FROM squadrons_data
                                WHERE clanrating IS NOT NULL
                            `, [], (err, rows) => {
                                pointsDb.close();

                                if (err) {
                                    log.error('Error querying squadron points', err);
                                    return resolve({});
                                }

                                const pointsMap = {};
                                rows.forEach(row => {
                                    pointsMap[row.long_name] = row.total_points || 0;
                                });
                                resolve(pointsMap);
                            });
                        });
                    });
                };

                getSquadronPoints().then(pointsMap => {
                    const squadronsArray = Object.values(consolidatedSquadrons).map(squadron => {
                        const kdr = squadron.deaths > 0 ? (squadron.total_kills / squadron.deaths) : squadron.total_kills;
                        const winRate = squadron.total_battles > 0 ? (squadron.wins / squadron.total_battles) * 100 : 0;
                        const totalPoints = pointsMap[squadron.long_name] || 0;

                        return {
                            clan_id: squadron.clan_id,
                            tag_name: squadron.tag_name,
                            short_name: squadron.short_name,
                            long_name: squadron.long_name,
                            player_count: squadron.player_count,
                            total_kills: squadron.total_kills,
                            ground_kills: squadron.ground_kills,
                            air_kills: squadron.air_kills,
                            total_battles: squadron.total_battles,
                            wins: squadron.wins,
                            win_rate: parseFloat(winRate.toFixed(1)),
                            kdr: parseFloat(kdr.toFixed(1)),
                            deaths: squadron.deaths,
                            assists: squadron.assists,
                            captures: squadron.captures,
                            points: {
                                total_points: totalPoints,
                                has_points_data: totalPoints > 0
                            }
                        };
                    });

                    // Sort by points, fallback to total kills
                    squadronsArray.sort((a, b) => {
                        if (a.points.has_points_data && b.points.has_points_data) {
                            return b.points.total_points - a.points.total_points;
                        }
                        if (a.points.has_points_data && !b.points.has_points_data) return -1;
                        if (!a.points.has_points_data && b.points.has_points_data) return 1;
                        return b.total_kills - a.total_kills;
                    });

                    const response = {
                        date_filter: createDateFilterMetadata(dateFilters, start_date, end_date, season, week),
                        timeframe: dateFilters.hasFilter ? "filtered" : "all-time",
                        total_squadrons: squadronsArray.length,
                        squadrons: squadronsArray
                    };

                    log.info('Squadron leaderboard query completed with consolidation and points', {
                        originalSquadrons: squadronRows.length,
                        consolidatedSquadrons: squadronsArray.length,
                        totalSquadrons: squadronsArray.length,
                        squadronsWithPoints: squadronsArray.filter(s => s.points.has_points_data).length
                    });

                    setCachedResponse(cacheKey, response);
                    resolve(response);
                });
            });
            });
        });
        });
    })).then(response => {
        if (!res.headersSent) res.json(applyLimit(response));
    }).catch(err => {
        if (res.headersSent) return;
        if (err && err.status && err.body) {
            return res.status(err.status).json(err.body);
        }
        log.error('Unhandled error in squadron leaderboard', err);
        res.status(500).json({ error: 'Database error occurred', errorCode: 'DB_SQUADRON_LEADERBOARD_FAILED' });
    });
});

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

    if (!squadronname) {
        return res.status(400).json({
            error: 'Squadron name parameter is required'
        });
    }

    // Parse date filters
    const dateFilters = parseDateFilters(req);
    const { start_date, end_date, season, week } = req.query;

    log.info('Squadron details request received', { squadronName: squadronname });

    // Cache the response for 5 minutes — squadron stats change slowly and
    // a single page render fires this endpoint per tab refresh. Without
    // caching every load re-runs the 5s+ join. Dedup so concurrent first
    // hits share one execution.
    const cacheKey = `squadron_detail_${squadronname}_${start_date || ''}_${end_date || ''}_${season || ''}_${week || ''}`;
    const cached = getCachedResponse(cacheKey);
    if (cached) return res.json(cached);

    if (!hasSquadronColumn) {
            log.info('Squadron column missing - returning empty squadron details', { squadronName: squadronname });
            return res.json({
                date_filter: createDateFilterMetadata(dateFilters, start_date, end_date, season, week),
                tag_name: squadronname,
                long_name: squadronname,
                squadron_summary: {
                    player_count: 0,
                    total_kills: 0,
                    ground_kills: 0,
                    air_kills: 0,
                    total_battles: 0,
                    wins: 0,
                    win_rate: 0,
                    kdr: 0,
                    deaths: 0,
                    assists: 0,
                    captures: 0,
                    points: {
                        total_points: 0,
                        has_points_data: false
                    }
                },
                players: [],
                migration_needed: true,
                message: 'Squadron data not available - squadron_name column missing from database'
            });
        }

        let canonicalName = squadronname;
        let tagName = squadronname;

        loadSquadronLookupCached((squadronLookup) => {
            const lookup = squadronLookup[squadronname];
            let memberCount = 0;
            const allVariants = [];
            if (lookup) {
                canonicalName = lookup.long_name;
                tagName = lookup.tag_name;
                memberCount = lookup.members || 0;
            }
            allVariants.push(canonicalName);
            Object.keys(squadronLookup).forEach(key => {
                if (squadronLookup[key].long_name === canonicalName && key !== canonicalName) {
                    allVariants.push(key);
                }
            });
            

            const clanId = lookup ? lookup.clan_id : null;
            const squadronsDbPath = path.join(STORAGE_ROOT, 'squadrons.db');
            const totalPoints = lookup ? (lookup.clanrating || 0) : 0;

            // Step 1: Get the authoritative member roster from squadron_members
            const membersPromise = new Promise((resolve) => {
                if (!clanId || !fs.existsSync(squadronsDbPath)) return resolve([]);
                const squadronsDb = new sqlite3.Database(squadronsDbPath, sqlite3.OPEN_READONLY, (err) => {
                    if (err) { log.error('Failed to open squadrons.db for members lookup', err); return resolve([]); }
                    squadronsDb.all(`SELECT uid, nick, points FROM squadron_members WHERE clan_id = ?`, [clanId], (err, rows) => {
                        squadronsDb.close();
                        if (err) { log.error('Error querying squadron_members', err); return resolve([]); }
                        resolve(rows);
                    });
                });
            });

            // Step 1b: When a date filter has an end timestamp, fetch the
            // squadrons_points snapshot at or before that moment so we can
            // report each member's points AS OF that historical point.
            // clan_pts is gzip-compressed JSON of [members_dict, total_score].
            const snapshotPromise = new Promise((resolve) => {
                if (!dateFilters.endTimestamp || !fs.existsSync(squadronsDbPath)) return resolve(null);
                const sdb = new sqlite3.Database(squadronsDbPath, sqlite3.OPEN_READONLY, (err) => {
                    if (err) { resolve(null); return; }
                    sdb.get(
                        clanId
                            ? 'SELECT clan_pts, total_score FROM squadrons_points WHERE clan_id = ? AND unix_time <= ? ORDER BY unix_time DESC LIMIT 1'
                            : 'SELECT clan_pts, total_score FROM squadrons_points WHERE long_name = ? AND unix_time <= ? ORDER BY unix_time DESC LIMIT 1',
                        [clanId || canonicalName, dateFilters.endTimestamp],
                        (qerr, row) => {
                            sdb.close();
                            if (qerr || !row || !row.clan_pts) return resolve(null);
                            try {
                                const buf = Buffer.isBuffer(row.clan_pts) ? row.clan_pts : Buffer.from(row.clan_pts, 'binary');
                                const decompressed = zlib.gunzipSync(buf);
                                const parsed = JSON.parse(decompressed.toString('utf8'));
                                if (Array.isArray(parsed) && parsed[0] && typeof parsed[0] === 'object') {
                                    resolve({ members: parsed[0], total: row.total_score });
                                } else {
                                    resolve(null);
                                }
                            } catch (e) {
                                log.error('Failed to decompress clan_pts snapshot', e, { longName: canonicalName });
                                resolve(null);
                            }
                        }
                    );
                });
            });

            Promise.all([membersPromise, snapshotPromise])
                .then(([memberRows, snapshot]) => {
                    const histMembers = snapshot ? snapshot.members : null;
                    const histTotal = (snapshot && typeof snapshot.total === 'number') ? snapshot.total : null;
                    if (!memberRows.length) {
                        log.info('No members found in squadron_members', { squadronName: squadronname, clanId });
                        return res.json({
                            date_filter: createDateFilterMetadata(dateFilters, start_date, end_date, season, week),
                            tag_name: tagName,
                            long_name: canonicalName,
                            squadron_summary: {
                                player_count: 0, total_kills: 0, ground_kills: 0, air_kills: 0,
                                total_battles: 0, wins: 0, win_rate: 0, kdr: 0, deaths: 0,
                                assists: 0, captures: 0, points: { total_points: totalPoints, has_points_data: totalPoints > 0 }
                            },
                            players: [],
                            message: `Squadron '${squadronname}' not found or has no members`
                        });
                    }

                    // Build lookup maps from the roster
                    const memberNicks = {};
                    const memberPoints = {};
                    const memberUids = memberRows.map(r => {
                        const uid = String(r.uid);
                        memberNicks[uid] = r.nick || '';
                        memberPoints[uid] = r.points || 0;
                        return uid;
                    });

                    // Step 2: Query stats from player_games_hist for roster members,
                    // but only for battles explicitly attributed to this squadron.
                    const uidPlaceholders = memberUids.map(() => '?').join(',');
                    const variantPlaceholders = allVariants.map(() => '?').join(',');
                    const mainDate = buildDateClause('p', dateFilters);
                    const fallbackDate = buildDateClause('p', dateFilters);
                    const summaryDate = buildDateClause('', dateFilters);

                    // When clan_id is known (the common case post-migration),
                    // filter on it directly so SQLite can use idx_pgh_clanid_endtime.
                    // The OR-with-text-fallback we used to have here was the
                    // single biggest perf hit on filtered profile queries —
                    // it forced a full scan because the planner can't OR
                    // two index plans together cleanly. We drop the fallback;
                    // truly orphaned rows (clan_id NULL) won't appear, but
                    // those rows are unrecoverable without a manual backfill
                    // anyway.
                    const squadronAttrClause = clanId
                        ? `p.clan_id = ?`
                        : `p.squadron_name IN (${variantPlaceholders})`;
                    const summaryAttrClause = clanId
                        ? `clan_id = ?`
                        : `squadron_name IN (${variantPlaceholders})`;

                    const playerStatsQuery = `
                        SELECT
                            p.UID as uid,
                            SUM(ground_kills) as total_ground_kills,
                            SUM(air_kills) as total_air_kills,
                            SUM(ground_kills + air_kills) as total_kills,
                            SUM(assists) as total_assists,
                            SUM(captures) as total_captures,
                            SUM(deaths) as total_deaths,
                            COUNT(*) as total_battles,
                            SUM(CASE WHEN UPPER(victor_bool) = 'WIN' THEN 1 ELSE 0 END) as wins
                        FROM player_games_hist p
                        WHERE p.UID IN (${uidPlaceholders})
                            AND ${squadronAttrClause}
                            ${mainDate.clause}
                        GROUP BY p.UID
                    `;

                    const summaryQuery = `
                        SELECT
                            COUNT(DISTINCT session_id) as total_battles,
                            COUNT(DISTINCT CASE WHEN UPPER(victor_bool) = 'WIN' THEN session_id END) as wins,
                            SUM(ground_kills) as total_ground_kills,
                            SUM(air_kills) as total_air_kills,
                            SUM(ground_kills + air_kills) as total_kills,
                            SUM(assists) as total_assists,
                            SUM(captures) as total_captures,
                            SUM(deaths) as total_deaths
                        FROM player_games_hist
                        WHERE ${summaryAttrClause}
                            AND UID IS NOT NULL
                            ${summaryDate.clause}
                    `;

                    const vehicleStatsQuery = `
                        SELECT
                            vehicle_internal,
                            MAX(vehicle) as vehicle,
                            SUM(ground_kills) as total_ground_kills,
                            SUM(air_kills) as total_air_kills,
                            SUM(ground_kills + air_kills) as total_kills,
                            SUM(assists) as total_assists,
                            SUM(captures) as total_captures,
                            SUM(deaths) as total_deaths,
                            COUNT(*) as total_battles,
                            SUM(CASE WHEN UPPER(victor_bool) = 'WIN' THEN 1 ELSE 0 END) as wins
                        FROM player_games_hist
                        WHERE ${summaryAttrClause}
                            AND vehicle_internal IS NOT NULL
                            AND vehicle_internal <> ''
                            ${summaryDate.clause}
                        GROUP BY vehicle_internal
                        ORDER BY total_battles DESC, total_kills DESC
                    `;

                    // Params must match the SQL placeholder order exactly.
                    // The squadron-attribution clause is either `clan_id = ?`
                    // (1 param) OR `squadron_name IN (variants)` (V params);
                    // we only spread the side that's actually in the SQL.
                    const squadronAttrParams = clanId ? [clanId] : allVariants;

                    const statsParams = [
                        ...memberUids,
                        ...squadronAttrParams,
                        ...mainDate.params
                    ];

                    const summaryParams = [
                        ...squadronAttrParams,
                        ...summaryDate.params
                    ];
                    const vehicleStatsParams = [
                        ...squadronAttrParams,
                        ...summaryDate.params
                    ];

                    const fallbackPlayerStatsQuery = `
                        WITH roster_sessions AS (
                            SELECT
                                p.UID as uid,
                                p.session_id,
                                SUM(p.ground_kills) as total_ground_kills,
                                SUM(p.air_kills) as total_air_kills,
                                SUM(p.ground_kills + p.air_kills) as total_kills,
                                SUM(p.assists) as total_assists,
                                SUM(p.captures) as total_captures,
                                SUM(p.deaths) as total_deaths,
                                MAX(CASE WHEN UPPER(p.victor_bool) = 'WIN' THEN 1 ELSE 0 END) as won
                            FROM player_games_hist p
                            WHERE p.UID IN (${uidPlaceholders})
                                ${fallbackDate.clause}
                            GROUP BY p.UID, p.session_id
                        )
                        SELECT
                            rs.uid as uid,
                            SUM(rs.total_ground_kills) as total_ground_kills,
                            SUM(rs.total_air_kills) as total_air_kills,
                            SUM(rs.total_kills) as total_kills,
                            SUM(rs.total_assists) as total_assists,
                            SUM(rs.total_captures) as total_captures,
                            SUM(rs.total_deaths) as total_deaths,
                            COUNT(*) as total_battles,
                            SUM(rs.won) as wins
                        FROM roster_sessions rs
                        GROUP BY rs.uid
                    `;

                    const fallbackSummaryQuery = `
                        WITH roster_sessions AS (
                            SELECT
                                p.UID as uid,
                                p.session_id,
                                SUM(p.ground_kills) as total_ground_kills,
                                SUM(p.air_kills) as total_air_kills,
                                SUM(p.ground_kills + p.air_kills) as total_kills,
                                SUM(p.assists) as total_assists,
                                SUM(p.captures) as total_captures,
                                SUM(p.deaths) as total_deaths,
                                MAX(CASE WHEN UPPER(p.victor_bool) = 'WIN' THEN 1 ELSE 0 END) as won
                            FROM player_games_hist p
                            WHERE p.UID IN (${uidPlaceholders})
                                AND (? IS NULL OR p.endtime_unix >= ?)
                                AND (? IS NULL OR p.endtime_unix <= ?)
                            GROUP BY p.UID, p.session_id
                        )
                        SELECT
                            COUNT(DISTINCT session_id) as total_battles,
                            COUNT(DISTINCT CASE WHEN won = 1 THEN session_id END) as wins,
                            SUM(total_ground_kills) as total_ground_kills,
                            SUM(total_air_kills) as total_air_kills,
                            SUM(total_kills) as total_kills,
                            SUM(total_assists) as total_assists,
                            SUM(total_captures) as total_captures,
                            SUM(total_deaths) as total_deaths
                        FROM roster_sessions
                    `;

                    const fallbackStatsParams = [
                        ...memberUids,
                        ...fallbackDate.params
                    ];

                    const fallbackSummaryParams = [
                        ...memberUids,
                        ...fallbackDate.params
                    ];

                    const hasMeaningfulStats = (rows) => Array.isArray(rows) && rows.some(row =>
                        (row.total_battles || 0) > 0 ||
                        (row.total_kills || 0) > 0 ||
                        (row.total_assists || 0) > 0 ||
                        (row.total_captures || 0) > 0
                    );
                    const allowFallback = !dateFilters.hasFilter;

                    const buildAndSendResponse = (statsRows, summaryRow, vehicleRows = [], { usingFallback = false } = {}) => {
                        const safeStatsRows = statsRows || [];
                        const safeSummaryRow = summaryRow || {};
                        const safeVehicleRows = vehicleRows || [];

                        // Index stats by UID for fast lookup
                        const statsByUid = {};
                        safeStatsRows.forEach(row => { statsByUid[String(row.uid)] = row; });

                        // Build player list from roster, attaching member stats
                        const players = memberUids.map(uid => {
                            const stats = statsByUid[uid];
                            const totalKills = stats ? (stats.total_kills || 0) : 0;
                            const deaths = stats ? (stats.total_deaths || 0) : 0;
                            const wins = stats ? (stats.wins || 0) : 0;
                            const totalBattles = stats ? (stats.total_battles || 0) : 0;
                            const kdr = deaths > 0 ? (totalKills / deaths) : totalKills;
                            const winRate = totalBattles > 0 ? (wins / totalBattles) * 100 : 0;
                            const nick = (stats && stats.hist_nick) ? stats.hist_nick : memberNicks[uid];
                            let sqbPoints;
                            if (dateFilters.endTimestamp) {
                                const entry = histMembers ? histMembers[String(uid)] : null;
                                if (entry && typeof entry === 'object') {
                                    sqbPoints = Number(entry.points) || 0;
                                } else if (typeof entry === 'number') {
                                    sqbPoints = entry;
                                } else {
                                    sqbPoints = 0;
                                }
                            } else {
                                sqbPoints = memberPoints[uid] || 0;
                            }
                            return {
                                uid,
                                nick,
                                sqb_points: sqbPoints,
                                total_kills: totalKills,
                                ground_kills: stats ? (stats.total_ground_kills || 0) : 0,
                                air_kills: stats ? (stats.total_air_kills || 0) : 0,
                                total_battles: totalBattles,
                                wins,
                                win_rate: parseFloat(winRate.toFixed(1)),
                                kdr: parseFloat(kdr.toFixed(1)),
                                kps: totalBattles > 0 ? parseFloat((totalKills / totalBattles).toFixed(2)) : 0,
                                deaths,
                                assists: stats ? (stats.total_assists || 0) : 0,
                                captures: stats ? (stats.total_captures || 0) : 0
                            };
                        });

                        players.sort((a, b) => b.total_kills - a.total_kills);

                        const sqTotalKills = safeSummaryRow.total_kills || 0;
                        const sqGroundKills = safeSummaryRow.total_ground_kills || 0;
                        const sqAirKills = safeSummaryRow.total_air_kills || 0;
                        const sqDeaths = safeSummaryRow.total_deaths || 0;
                        const sqWins = safeSummaryRow.wins || 0;
                        const sqBattles = safeSummaryRow.total_battles || 0;
                        const sqAssists = safeSummaryRow.total_assists || 0;
                        const sqCaptures = safeSummaryRow.total_captures || 0;
                        const sqKdr = sqDeaths > 0 ? (sqTotalKills / sqDeaths) : sqTotalKills;
                        const sqWinRate = sqBattles > 0 ? (sqWins / sqBattles) * 100 : 0;
                        const activePlayers = players.filter(p => p.total_battles > 0);
                        const sqKps = activePlayers.length > 0
                            ? parseFloat((activePlayers.reduce((sum, p) => sum + p.kps, 0) / activePlayers.length).toFixed(2))
                            : 0;
                        loadPerformanceBenchmarksCached(dateFilters, (benchmarks) => {
                            const playerBenchmark = benchmarks.players;
                            const squadronBenchmark = benchmarks.squadrons;

                            Promise.all([
                                queryPlayerRatingStatsForUids(memberUids, dateFilters),
                                querySquadronRatingStats(allVariants, dateFilters, clanId)
                            ]).then(([playerRatingStats, squadronRatingStats]) => {
                                players.forEach(player => {
                                    player.performance = computePerformanceScore(
                                        playerRatingStats.get(String(player.uid)) || { games: 0 },
                                        playerBenchmark
                                    );
                                });
                                players.sort((a, b) => (b.performance || 0) - (a.performance || 0) || (b.total_kills || 0) - (a.total_kills || 0));

                                const squadronPerformanceSource = squadronRatingStats && Number(squadronRatingStats.games || 0) > 0
                                    ? squadronRatingStats
                                    : {
                                        games: sqBattles,
                                        total_kills: sqTotalKills,
                                        total_assists: sqAssists,
                                        total_captures: sqCaptures,
                                        total_deaths: sqDeaths,
                                        wins: sqWins,
                                        heavy_score: 0
                                    };
                                const sqPerformance = computePerformanceScore(squadronPerformanceSource, squadronBenchmark);

                                const response = {
                                    date_filter: createDateFilterMetadata(dateFilters, start_date, end_date, season, week),
                                    clan_id: lookup ? lookup.clan_id : null,
                                    tag_name: tagName,
                                    short_name: lookup ? (lookup.short_name || tagName) : tagName,
                                    long_name: canonicalName,
                                    squadron_summary: {
                                        player_count: memberCount,
                                        total_kills: sqTotalKills,
                                        ground_kills: sqGroundKills,
                                        air_kills: sqAirKills,
                                        total_battles: sqBattles,
                                        wins: sqWins,
                                        win_rate: parseFloat(sqWinRate.toFixed(1)),
                                        kdr: parseFloat(sqKdr.toFixed(1)),
                                        kps: sqKps,
                                        deaths: sqDeaths,
                                        assists: sqAssists,
                                        captures: sqCaptures,
                                        performance: sqPerformance,
                                        points: {
                                            total_points: dateFilters.endTimestamp
                                                ? (histTotal != null ? histTotal : 0)
                                                : totalPoints,
                                            has_points_data: dateFilters.endTimestamp
                                                ? (histTotal != null && histTotal > 0)
                                                : (totalPoints > 0)
                                        }
                                    },
                                    players,
                                    vehicles: safeVehicleRows.map(row => {
                                        const totalKills = row.total_kills || 0;
                                        const deaths = row.total_deaths || 0;
                                        const wins = row.wins || 0;
                                        const totalBattles = row.total_battles || 0;
                                        return {
                                            vehicle_internal: row.vehicle_internal,
                                            vehicle: normalizeVehicleName(row.vehicle) || row.vehicle_internal,
                                            total_kills: totalKills,
                                            ground_kills: row.total_ground_kills || 0,
                                            air_kills: row.total_air_kills || 0,
                                            total_battles: totalBattles,
                                            wins,
                                            win_rate: totalBattles > 0 ? parseFloat(((wins / totalBattles) * 100).toFixed(1)) : 0,
                                            kdr: deaths > 0 ? parseFloat((totalKills / deaths).toFixed(1)) : totalKills,
                                            deaths,
                                            assists: row.total_assists || 0,
                                            captures: row.total_captures || 0,
                                        };
                                    })
                                };

                                log.info('Squadron details query completed', {
                                    squadronName: squadronname,
                                    canonicalName,
                                    variants: allVariants,
                                    rosterSize: memberUids.length,
                                    playersWithStats: Object.keys(statsByUid).length,
                                    totalPoints,
                                    histTotal,
                                    usingSnapshot: !!histMembers,
                                    usingFallback,
                                    squadronBattles: sqBattles
                                });

                                setCachedResponse(cacheKey, response);
                                res.json(response);
                            });
                        });
                    };

                    const dbAll = (query, params) => new Promise((resolve, reject) =>
                        db.all(query, params, (err, rows) => err ? reject(err) : resolve(rows))
                    );
                    const dbGet = (query, params) => new Promise((resolve, reject) =>
                        db.get(query, params, (err, row) => err ? reject(err) : resolve(row))
                    );

                    Promise.all([
                        dbAll(playerStatsQuery, statsParams),
                        dbGet(summaryQuery, summaryParams),
                        dbAll(vehicleStatsQuery, vehicleStatsParams)
                    ]).then(([statsRows, summaryRow, vehicleRows]) => {
                        if (hasMeaningfulStats(statsRows) || !allowFallback) {
                            return buildAndSendResponse(statsRows, summaryRow, vehicleRows, { usingFallback: false });
                        }

                        return Promise.all([
                            dbAll(fallbackPlayerStatsQuery, fallbackStatsParams),
                            dbGet(fallbackSummaryQuery, fallbackSummaryParams)
                        ]).then(([fallbackStatsRows, fallbackSummaryRow]) => {
                            buildAndSendResponse(fallbackStatsRows, fallbackSummaryRow, vehicleRows, { usingFallback: true });
                        }).catch((fallbackErr) => {
                            log.error('Database error querying fallback squadron stats', fallbackErr, { squadronName: squadronname });
                            buildAndSendResponse(statsRows, summaryRow, vehicleRows, { usingFallback: false });
                        });
                    }).catch((err) => {
                        log.error('Database error in squadron queries', err, { squadronName: squadronname });
                        res.status(500).json({ error: 'Database error occurred', errorCode: 'DB_SQUADRON_QUERY_FAILED' });
                    });
                })
                .catch(err => {
                    log.error('Database error in squadron queries', err, { squadronName: squadronname });
                    res.status(500).json({ error: 'Database error occurred', errorCode: 'DB_SQUADRON_QUERY_FAILED' });
                });
        });
});

app.get('/api/squadrons/:squadronname/history', (req, res) => {
    const { squadronname } = req.params;

    if (!squadronname) {
        return res.status(400).json({ error: 'Squadron name parameter is required' });
    }

    const cacheKey = `squadron_history_${squadronname}`;
    const cached = getCachedResponse(cacheKey);
    if (cached) return res.json(cached);

    loadSquadronLookupCached((squadronLookup) => {
        const lookup = squadronLookup[squadronname];
        let canonicalName = squadronname;
        const clanId = lookup ? lookup.clan_id : null;

        if (lookup) {
            canonicalName = lookup.long_name;
        }

        const allVariants = [canonicalName];
        Object.keys(squadronLookup).forEach(key => {
            if (squadronLookup[key].long_name === canonicalName && key !== canonicalName) {
                allVariants.push(key);
            }
        });

        const placeholders = allVariants.map(() => '?').join(',');

        // When clan_id is known, filter on it directly so SQLite uses
        // idx_pgh_clanid_endtime. The OR-with-text-fallback we used to have
        // here forced a full scan because the planner can't OR two index
        // plans together cleanly. Same change as /api/squadrons/:name —
        // truly orphaned rows (clan_id NULL) are unrecoverable without a
        // backfill anyway.
        const historyQuery = clanId
            ? `
            SELECT
                date(endtime_unix, 'unixepoch') as period,
                COUNT(DISTINCT session_id) as battles,
                ROUND(SUM(CASE WHEN UPPER(victor_bool) = 'WIN' THEN 1.0 ELSE 0 END) * 100.0 / NULLIF(COUNT(*), 0), 1) as win_rate,
                ROUND(CAST(SUM(ground_kills + air_kills) AS REAL) / MAX(SUM(deaths), 1), 2) as kdr
            FROM player_games_hist
            WHERE clan_id = ?
                AND UID IS NOT NULL
                AND endtime_unix IS NOT NULL
            GROUP BY period
            ORDER BY period ASC`
            : `
            SELECT
                date(endtime_unix, 'unixepoch') as period,
                COUNT(DISTINCT session_id) as battles,
                ROUND(SUM(CASE WHEN UPPER(victor_bool) = 'WIN' THEN 1.0 ELSE 0 END) * 100.0 / NULLIF(COUNT(*), 0), 1) as win_rate,
                ROUND(CAST(SUM(ground_kills + air_kills) AS REAL) / MAX(SUM(deaths), 1), 2) as kdr
            FROM player_games_hist
            WHERE squadron_name IN (${placeholders})
                AND UID IS NOT NULL
                AND endtime_unix IS NOT NULL
            GROUP BY period
            ORDER BY period ASC
        `;

        const historyParams = clanId ? [clanId] : allVariants;
        db.all(historyQuery, historyParams, (err, battleRows) => {
            if (err) {
                log.error('Database error in squadron history query', err, { squadronName: squadronname });
                return res.status(500).json({ error: 'Database error', errorCode: 'DB_SQUADRON_HISTORY_FAILED' });
            }

            // Fetch rating history from squadrons.db and return separately.
            // Rating is returned at the raw hourly-snapshot granularity so the
            // rating chart can show full detail (season resets, intraday
            // spikes); daily aggregation hid transient troughs.
            const squadronsDbPath = path.join(STORAGE_ROOT, 'squadrons.db');
            if (!fs.existsSync(squadronsDbPath)) {
                return res.json({ history: battleRows, rating_hourly: [] });
            }

            const squadronsDb = new sqlite3.Database(squadronsDbPath, sqlite3.OPEN_READONLY, (err) => {
                if (err) {
                    return res.json({ history: battleRows, rating_hourly: [] });
                }

                // Prefer clan_id so renames don't truncate the rating chart.
                const ratingQuery = clanId
                    ? `
                    SELECT unix_time, total_score
                    FROM squadrons_points
                    WHERE clan_id = ?
                    ORDER BY unix_time ASC
                `
                    : `
                    SELECT unix_time, total_score
                    FROM squadrons_points
                    WHERE long_name = ?
                    ORDER BY unix_time ASC
                `;

                squadronsDb.all(ratingQuery, [clanId || canonicalName], (err, ratingRows) => {
                    squadronsDb.close();

                    const buildAndCache = (body) => {
                        setCachedResponse(cacheKey, body);
                        res.json(body);
                    };

                    if (err || !ratingRows) {
                        return buildAndCache({ history: battleRows, rating_hourly: [] });
                    }

                    buildAndCache({
                        history: battleRows,
                        rating_hourly: ratingRows.map(r => ({
                            t: r.unix_time,
                            rating: r.total_score,
                        })),
                    });
                });
            });
        });
    });
});

app.get('/api/squadrons/:squadronname/games', (req, res) => {
    const { squadronname } = req.params;

    if (!squadronname) {
        return res.status(400).json({ error: 'Squadron name parameter is required' });
    }

    const dateFilters = parseDateFilters(req);
    const cacheKey = `squadron_games_${squadronname}_${dateFilters.startTimestamp || 'null'}_${dateFilters.endTimestamp || 'null'}`;
    const cached = getCachedResponse(cacheKey);
    if (cached) return res.json(cached);

    if (!hasSquadronColumn) {
        return res.json({
            tag_name: squadronname,
            long_name: squadronname,
            games: [],
            total_games_returned: 0,
            message: 'Squadron data not available - squadron_name column missing from database'
        });
    }

    loadSquadronLookupCached((squadronLookup) => {
        const lookup = squadronLookup[squadronname];
        let canonicalName = squadronname;
        let tagName = squadronname;
        const clanId = lookup ? lookup.clan_id : null;

        if (lookup) {
            canonicalName = lookup.long_name;
            tagName = lookup.tag_name;
        }

        const allVariants = [canonicalName];
        Object.keys(squadronLookup).forEach(key => {
            if (squadronLookup[key].long_name === canonicalName && key !== canonicalName) {
                allVariants.push(key);
            }
        });

        const variantPlaceholders = allVariants.map(() => '?').join(',');
        // Same OR-fallback removal as /api/squadrons/:name and /history —
        // forces the planner onto idx_pgh_clanid_endtime instead of a scan.
        const attrClause = clanId
            ? `p.clan_id = ?`
            : `p.squadron_name IN (${variantPlaceholders})`;

        const gamesQuery = `
            SELECT
                p.session_id,
                MAX(p.endtime_unix) as endtime_unix,
                GROUP_CONCAT(DISTINCT p.nick) as players,
                COUNT(DISTINCT p.UID) as player_count,
                SUM(p.ground_kills) as ground_kills,
                SUM(p.air_kills) as air_kills,
                SUM(p.assists) as assists,
                SUM(p.captures) as captures,
                SUM(p.deaths) as deaths,
                CASE WHEN SUM(CASE WHEN UPPER(victor_bool) = 'WIN' THEN 1 ELSE 0 END) * 2 > COUNT(*) THEN 'Win' ELSE 'Loss' END as result,
                ms.map_name
            FROM player_games_hist p
            LEFT JOIN match_summary ms ON ms.session_id = p.session_id
            WHERE ${attrClause}
                AND p.UID IS NOT NULL
                AND p.session_id IS NOT NULL
                AND (? IS NULL OR p.endtime_unix >= ?)
                AND (? IS NULL OR p.endtime_unix <= ?)
            GROUP BY p.session_id
            ORDER BY endtime_unix DESC
            LIMIT 2000
        `;

        const gamesParams = [
            ...(clanId ? [clanId] : allVariants),
            dateFilters.startTimestamp,
            dateFilters.startTimestamp,
            dateFilters.endTimestamp,
            dateFilters.endTimestamp
        ];

        db.all(gamesQuery, gamesParams, (err, rows) => {
            if (err) {
                log.error('Database error in squadron games query', err, { squadronName: squadronname });
                return res.status(500).json({ error: 'Database error', errorCode: 'DB_SQUADRON_GAMES_FAILED' });
            }

            const response = {
                tag_name: tagName,
                long_name: canonicalName,
                games: (rows || []).map(row => ({
                    session_id: row.session_id,
                    timestamp: row.endtime_unix || 0,
                    map_name: row.map_name || null,
                    players: row.players || '',
                    player_count: row.player_count || 0,
                    stats: {
                        ground_kills: row.ground_kills || 0,
                        air_kills: row.air_kills || 0,
                        assists: row.assists || 0,
                        captures: row.captures || 0,
                        deaths: row.deaths || 0
                    },
                    result: row.result || 'Unknown'
                })),
                total_games_returned: (rows || []).length
            };

            setCachedResponse(cacheKey, response);
            res.json(response);
        });
    });
});

function sendSqliteSchema(res, dbPath, label) {
    if (!fs.existsSync(dbPath)) {
        return res.status(404).json({
            error: `${label} database not found`,
            path: dbPath
        });
    }

    const schemaDb = new sqlite3.Database(dbPath, sqlite3.OPEN_READONLY, (err) => {
        if (err) {
            return res.status(500).json({
                error: `Failed to open ${label} database`,
                details: err.message
            });
        }

        schemaDb.all(`SELECT name FROM sqlite_master WHERE type='table'`, (err, tables) => {
            if (err) {
                schemaDb.close();
                return res.status(500).json({ error: 'Failed to get table list', details: err.message });
            }

            const tableSchemas = {};
            let completedTables = 0;
            if (!tables.length) {
                schemaDb.close();
                return res.json({ database_path: dbPath, tables: {}, message: `No tables found in ${label} database` });
            }

            tables.forEach(table => {
                const tableName = table.name;
                schemaDb.all(`PRAGMA table_info(${tableName})`, (schemaErr, schema) => {
                    if (schemaErr) {
                        tableSchemas[tableName] = { error: schemaErr.message };
                        completedTables++;
                        if (completedTables === tables.length) {
                            schemaDb.close();
                            res.json({ database_path: dbPath, tables: tableSchemas, timestamp: new Date().toISOString() });
                        }
                        return;
                    }

                    schemaDb.all(`SELECT * FROM ${tableName} LIMIT 3`, (sampleErr, samples) => {
                        tableSchemas[tableName] = {
                            schema,
                            sample_records: sampleErr ? [] : (samples || []),
                            record_count: sampleErr ? 0 : (samples || []).length
                        };
                        completedTables++;
                        if (completedTables === tables.length) {
                            schemaDb.close();
                            res.json({ database_path: dbPath, tables: tableSchemas, timestamp: new Date().toISOString() });
                        }
                    });
                });
            });
        });
    });
}

app.get('/api/leaderboard/stats', (req, res) => {
    log.info('Leaderboard stats request received');
    const dateFilters = parseDateFilters(req);
    const { start_date, end_date, season, week } = req.query;

    const cacheKey = `leaderboard_stats_${start_date || 'all'}_${end_date || 'all'}_${season || 'all'}_${week || 'all'}`;
    const cached = getCachedResponse(cacheKey, STATS_CACHE_TTL);
    if (cached) {
        log.info('Returning cached leaderboard stats');
        return res.json(cached);
    }

    // Dedup so the 3 web cluster workers + any concurrent traffic share one
    // DB call. Without this we saw 200s+ wall-clock durations on cold start as
    // requests serialized on the single read connection.
    dedup(cacheKey, () => new Promise((resolve, reject) => {
        const overallStatsQuery = `
            SELECT
                COUNT(DISTINCT UID) as total_players,
                COUNT(DISTINCT vehicle) as total_vehicles_used,
                COUNT(*) as total_battles
            FROM player_games_hist
            WHERE (? IS NULL OR endtime_unix >= ?)
                AND (? IS NULL OR endtime_unix <= ?)
        `;

        const topVehiclesQuery = `
            SELECT
                vehicle as vehicle,
                MAX(vehicle_internal) as vehicle_internal,
                COUNT(*) as usage_count
            FROM player_games_hist
            WHERE (? IS NULL OR endtime_unix >= ?)
                AND (? IS NULL OR endtime_unix <= ?)
            GROUP BY vehicle
            ORDER BY usage_count DESC
            LIMIT 12
        `;

        const queryParams = [
            dateFilters.startTimestamp,
            dateFilters.startTimestamp,
            dateFilters.endTimestamp,
            dateFilters.endTimestamp,
        ];
        const queryStart = Date.now();
        heavyDb.get(overallStatsQuery, queryParams, (err, statsRow) => {
            if (err) {
                log.error('Database error in overall stats query', err);
                return reject({ status: 500, body: { error: 'Database error occurred', errorCode: 'DB_OVERALL_STATS_FAILED' } });
            }

            heavyDb.all(topVehiclesQuery, queryParams, (err, vehicleRows) => {
                if (err) {
                    log.error('Database error in top vehicles query', err);
                    return reject({ status: 500, body: { error: 'Database error occurred', errorCode: 'DB_TOP_VEHICLES_FAILED' } });
                }

                const response = {
                    date_filter: createDateFilterMetadata(dateFilters, start_date, end_date, season, week),
                    total_players: statsRow.total_players,
                    total_vehicles_used: statsRow.total_vehicles_used,
                    total_battles: statsRow.total_battles,
                    last_updated: new Date().toISOString(),
                    top_vehicles: vehicleRows.map(row => ({
                        vehicle: row.vehicle,
                        vehicle_internal: row.vehicle_internal || null,
                        usage_count: row.usage_count
                    }))
                };

                log.info('Leaderboard stats query completed', {
                    totalPlayers: statsRow.total_players,
                    totalVehicles: statsRow.total_vehicles_used,
                    totalBattles: statsRow.total_battles,
                    topVehiclesReturned: vehicleRows.length,
                    ms: Date.now() - queryStart,
                });

                setCachedResponse(cacheKey, response);
                resolve(response);
            });
        });
    })).then(response => res.json(response))
       .catch(err => {
           if (res.headersSent) return;
           if (err && err.status) return res.status(err.status).json(err.body);
           res.status(500).json({ error: 'Database error', errorCode: 'DB_LEADERBOARD_STATS_FAILED' });
       });
});

// ============================================================================
// ANALYTICS ENDPOINTS
// ============================================================================

// Normalize a raw map_name: strip "[mode]" prefixes, leading "Gamemode " token,
// collapse whitespace, and use lowercase as the merge key while preserving a
// human-friendly display form (Title Case).
function normalizeMapName(raw) {
    if (!raw) return null;
    let s = String(raw).trim();
    if (!s) return null;
    s = s.replace(/^\s*\[[^\]]+\]\s*/, '');           // "[Conquest #1] Foo" -> "Foo"
    s = s.replace(/^\s*gamemode\s+/i, '');             // "Gamemode Foo"     -> "Foo"
    s = s.replace(/\s+/g, ' ').trim();
    if (!s) return null;
    const key = s.toLowerCase();
    // Title-case display form: capitalize first letter of each word but keep
    // already-cased words like "to", "the", and parens intact.
    const display = s.replace(/\b([a-z])/g, (m) => m.toUpperCase());
    return { key, display };
}

app.get('/api/analytics/maps/:squadron', (req, res) => {
    const sq = req.params.squadron;
    const startDate = parseInt(req.query.start_date) || 0;
    const endDate = parseInt(req.query.end_date) || 0;
    loadSquadronLookupCached(() => {
        const filter = resolveSquadronFilter(sq);
        const variantSet = new Set(filter.variants);
        const where = matchSummarySquadronWhere(filter);
        const params = [...where.params, startDate];
        let endClause = '';
        if (endDate) { endClause = ' AND endtime_unix <= ?'; params.push(endDate); }

        db.all(
            `SELECT map_name, winning_sq, losing_sq, winning_clan_id, losing_clan_id
             FROM match_summary
             WHERE ${where.clause} AND endtime_unix >= ?${endClause}`,
            params,
            (err, rows) => {
                if (err) return res.status(500).json({ error: 'Database error' });

                const stats = {};
                for (const row of rows) {
                    const norm = normalizeMapName(row.map_name) || { key: 'unknown', display: 'Unknown' };
                    if (!stats[norm.key]) stats[norm.key] = { map_name: norm.display, wins: 0, losses: 0 };
                    if (rowIsWinFor(row, filter, variantSet)) stats[norm.key].wins++;
                    else if (rowIsLossFor(row, filter, variantSet)) stats[norm.key].losses++;
                }

                const result = Object.values(stats).map(r => ({
                    ...r,
                    total: r.wins + r.losses,
                    win_rate: r.wins + r.losses > 0 ? Math.round(r.wins / (r.wins + r.losses) * 1000) / 10 : 0,
                }));
                result.sort((a, b) => b.total - a.total);
                res.json(result);
            }
        );
    });
});

app.get('/api/analytics/time/:squadron', (req, res) => {
    const sq = req.params.squadron;
    const startDate = parseInt(req.query.start_date) || 0;
    const endDate = parseInt(req.query.end_date) || 0;
    loadSquadronLookupCached(() => {
        const filter = resolveSquadronFilter(sq);
        const variantSet = new Set(filter.variants);
        const where = matchSummarySquadronWhere(filter);
        const params = [...where.params, Math.max(startDate, 1)];
        let endClause = '';
        if (endDate) { endClause = ' AND endtime_unix <= ?'; params.push(endDate); }

        db.all(
            `SELECT endtime_unix, winning_sq, losing_sq, winning_clan_id, losing_clan_id
             FROM match_summary
             WHERE ${where.clause} AND endtime_unix >= ?${endClause}`,
            params,
            (err, rows) => {
                if (err) return res.status(500).json({ error: 'Database error' });

                const hourly = {};
                const daily = {};
                for (const row of rows) {
                    const d = new Date(row.endtime_unix * 1000);
                    const hour = d.getUTCHours();
                    if (!hourly[hour]) hourly[hour] = { wins: 0, losses: 0 };
                    if (rowIsWinFor(row, filter, variantSet)) hourly[hour].wins++;
                    else if (rowIsLossFor(row, filter, variantSet)) hourly[hour].losses++;
                    const dayMs = Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate());
                    daily[dayMs] = (daily[dayMs] || 0) + 1;
                }

                const hourlyOut = {};
                for (const hour of Object.keys(hourly).sort((a, b) => a - b)) {
                    const s = hourly[hour];
                    const total = s.wins + s.losses;
                    hourlyOut[hour] = { ...s, total, win_rate: total > 0 ? Math.round(s.wins / total * 1000) / 10 : 0 };
                }
                const dailyOut = Object.entries(daily)
                    .map(([day, count]) => ({ day: parseInt(day), count }))
                    .sort((a, b) => a.day - b.day);
                res.json({ hourly: hourlyOut, daily: dailyOut });
            }
        );
    });
});

app.get('/api/analytics/consistency/:squadron', (req, res) => {
    const sq = req.params.squadron;
    const minGames = parseInt(req.query.min_games) || 10;
    const startDate = parseInt(req.query.start_date) || 0;
    const endDate = parseInt(req.query.end_date) || 0;

    loadSquadronLookupCached(() => {
        const filter = resolveSquadronFilter(sq);
        const variantSet = new Set(filter.variants);
        const pghWhere = playerGamesHistSquadronWhere(filter);

        const params = [...pghWhere.params];
        let dateClause = '';
        if (startDate) { dateClause += ' AND p.endtime_unix >= ?'; params.push(startDate); }
        if (endDate)   { dateClause += ' AND p.endtime_unix <= ?'; params.push(endDate); }

        db.all(
            `SELECT p.UID,
                    p.nick,
                    p.session_id,
                    SUM(p.ground_kills + p.air_kills) as total_kills,
                    SUM(p.deaths) as total_deaths,
                    m.winning_sq,
                    m.losing_sq,
                    m.winning_clan_id,
                    m.losing_clan_id
             FROM player_games_hist p
             LEFT JOIN match_summary m ON m.session_id = p.session_id
             WHERE ${pghWhere.clause}${dateClause}
             GROUP BY p.UID, p.session_id`,
            params,
            (err, rows) => {
                if (err) return res.status(500).json({ error: 'Database error' });

                const players = {};
                for (const row of rows) {
                    const UID = row.UID;
                    if (!players[UID]) players[UID] = { uid: UID, nick: row.nick, kills: [], deaths: [], wins: 0, losses: 0 };
                    players[UID].nick = row.nick;
                    players[UID].kills.push(row.total_kills || 0);
                    players[UID].deaths.push(row.total_deaths || 0);
                    if (rowIsWinFor(row, filter, variantSet)) players[UID].wins++;
                    else if (rowIsLossFor(row, filter, variantSet)) players[UID].losses++;
                }

                const result = Object.values(players)
                    .filter(p => p.kills.length >= minGames)
                    .map(p => {
                        const n = p.kills.length;
                        const sumK = p.kills.reduce((a, b) => a + b, 0);
                        const sumD = p.deaths.reduce((a, b) => a + b, 0);
                        const avgK = sumK / n;
                        const avgD = sumD / n;
                        const kd = sumD > 0 ? sumK / sumD : sumK;
                        const decided = p.wins + p.losses;
                        const winRate = decided > 0 ? (p.wins / decided) * 100 : 0;
                        return {
                            uid: p.uid,
                            nick: p.nick,
                            games: n,
                            wins: p.wins,
                            losses: p.losses,
                            avg_kills: Math.round(avgK * 100) / 100,
                            avg_deaths: Math.round(avgD * 100) / 100,
                            kd: Math.round(kd * 100) / 100,
                            win_rate: Math.round(winRate * 10) / 10,
                        };
                    })
                    .sort((a, b) => b.games - a.games);

                res.json(result);
            }
        );
    });
});

app.get('/api/analytics/matchup/:squadron', (req, res) => {
    const sq = req.params.squadron;
    const startDate = parseInt(req.query.start_date) || 0;
    const endDate = parseInt(req.query.end_date) || 0;
    loadSquadronLookupCached(() => {
        const filter = resolveSquadronFilter(sq);
        const variantSet = new Set(filter.variants);
        const where = matchSummarySquadronWhere(filter);
        const params = [...where.params, startDate];
        let endClause = '';
        if (endDate) { endClause = ' AND endtime_unix <= ?'; params.push(endDate); }

        db.all(
            `SELECT winning_sq, losing_sq, winning_clan_id, losing_clan_id
             FROM match_summary
             WHERE ${where.clause} AND endtime_unix >= ?${endClause}`,
            params,
            (err, rows) => {
                if (err) return res.status(500).json({ error: 'Database error' });

                const stats = {};
                for (const row of rows) {
                    const { winning_sq, losing_sq } = row;
                    if (!winning_sq || !losing_sq || winning_sq === losing_sq) continue;
                    const isWin = rowIsWinFor(row, filter, variantSet);
                    const isLoss = rowIsLossFor(row, filter, variantSet);
                    if (!isWin && !isLoss) continue;
                    const opponent = isWin ? losing_sq : winning_sq;
                    if (!opponent) continue;
                    if (!stats[opponent]) stats[opponent] = { opponent, wins: 0, losses: 0 };
                    if (isWin) stats[opponent].wins++;
                    else stats[opponent].losses++;
                }

                const enriched = Object.values(stats).map(r => {
                    const total = r.wins + r.losses;
                    return {
                        ...r,
                        total,
                        win_rate: total > 0 ? Math.round(r.wins / total * 1000) / 10 : 0,
                    };
                });

                const wonAgainst = [...enriched]
                    .sort((a, b) => (b.wins - a.wins) || (b.total - a.total))
                    .slice(0, 10);
                const lostAgainst = [...enriched]
                    .sort((a, b) => (b.losses - a.losses) || (b.total - a.total))
                    .slice(0, 10);

                res.json({
                    won_against: wonAgainst,
                    lost_against: lostAgainst,
                    total_opponents: enriched.length,
                });
            }
        );
    });
});

app.get('/api/analytics/comps/:squadron', (req, res) => {
    const sq = req.params.squadron;
    const startDate = parseInt(req.query.start_date) || 0;
    const endDate = parseInt(req.query.end_date) || 0;
    const minSize = Math.max(1, parseInt(req.query.min_size) || 8);

    loadSquadronLookupCached(() => {
    const filter = resolveSquadronFilter(sq);
    const variantSet = new Set(filter.variants);
    const pghWhere = playerGamesHistSquadronWhere(filter);
    const params = [...pghWhere.params];
    let dateClause = '';
    if (startDate) { dateClause += ' AND p.endtime_unix >= ?'; params.push(startDate); }
    if (endDate)   { dateClause += ' AND p.endtime_unix <= ?'; params.push(endDate); }

    db.all(
        `SELECT p.session_id, p.UID, p.vehicle, p.vehicle_internal,
                p.ground_kills, p.air_kills, p.deaths,
                m.winning_sq, m.losing_sq, m.winning_clan_id, m.losing_clan_id
         FROM player_games_hist p
         LEFT JOIN match_summary m ON m.session_id = p.session_id
         WHERE ${pghWhere.clause}${dateClause}`,
        params,
        (err, rows) => {
            if (err) return res.status(500).json({ error: 'Database error' });

            // Outcome per session and the multiset of vehicle internals brought.
            const sessionOutcome = new Map();   // sid -> 'win' | 'loss' | 'unknown'
            const sessionVehicles = new Map();  // sid -> Map<internal, count>

            // Per-vehicle aggregates keyed by internal id (so type lookup works
            // and dedup of localized aliases is correct). wins/losses count
            // sessions; spawns/kills/deaths count individual rows.
            const vehStats = new Map();  // internal -> { display, type, spawns, kills, deaths, sessions: Map<sid, outcome> }

            for (const r of rows) {
                if (!r.session_id) continue;
                // Lowercase the internal so case-only duplicates (e.g.
                // germ_leopard_I vs germ_leopard_i) collapse into one row.
                const rawInternal = (r.vehicle_internal && r.vehicle_internal !== 'DISCONNECTED') ? r.vehicle_internal : null;
                if (!rawInternal) continue;
                const internal = rawInternal.toLowerCase();
                const display = normalizeVehicleName(r.vehicle) || internal;

                if (!sessionOutcome.has(r.session_id)) {
                    let outcome = 'unknown';
                    if (rowIsWinFor(r, filter, variantSet)) outcome = 'win';
                    else if (rowIsLossFor(r, filter, variantSet)) outcome = 'loss';
                    sessionOutcome.set(r.session_id, outcome);
                }
                const outcome = sessionOutcome.get(r.session_id);

                let vMap = sessionVehicles.get(r.session_id);
                if (!vMap) { vMap = new Map(); sessionVehicles.set(r.session_id, vMap); }
                vMap.set(internal, (vMap.get(internal) || 0) + 1);

                let s = vehStats.get(internal);
                if (!s) {
                    s = {
                        display,
                        type: getVehicleType(internal),
                        spawns: 0, kills: 0, deaths: 0,
                        sessions: new Map(),
                    };
                    vehStats.set(internal, s);
                }
                s.spawns++;
                s.kills += (r.ground_kills || 0) + (r.air_kills || 0);
                s.deaths += r.deaths || 0;
                if (!s.sessions.has(r.session_id)) s.sessions.set(r.session_id, outcome);
            }

            const totalSessions = sessionOutcome.size;

            const topVehicles = [...vehStats.entries()]
                .map(([internal, s]) => {
                    let wins = 0, losses = 0;
                    for (const o of s.sessions.values()) {
                        if (o === 'win') wins++;
                        else if (o === 'loss') losses++;
                    }
                    const decided = wins + losses;
                    const sessionsCount = s.sessions.size;
                    return {
                        vehicle_internal: internal,
                        vehicle: s.display,
                        type: s.type,
                        spawns: s.spawns,
                        sessions: sessionsCount,
                        share_pct: totalSessions > 0 ? Math.round((sessionsCount / totalSessions) * 1000) / 10 : 0,
                        kills: s.kills,
                        deaths: s.deaths,
                        kd: s.deaths > 0 ? Math.round((s.kills / s.deaths) * 100) / 100 : s.kills,
                        wins,
                        losses,
                        win_rate: decided > 0 ? Math.round((wins / decided) * 1000) / 10 : 0,
                    };
                })
                .sort((a, b) => b.spawns - a.spawns)
                .slice(0, 50);

            // Match-level compositions: keyed by the multiset of vehicles fielded
            // per session. We aggregate across matches that fielded the *exact*
            // same multiset, plus a parallel aggregation by type-notation
            // (e.g. "3F 4T 1AA") for the search-bar presets.
            const compStats = new Map();   // sig -> { vehicles, types, notation, size, games, wins, losses }
            const notationStats = new Map(); // notation -> { types, size, games, wins, losses }

            for (const [sid, vMap] of sessionVehicles) {
                const flat = [];
                const typeCounts = {};
                for (const [internal, n] of vMap) {
                    const t = vehStats.get(internal).type;
                    for (let i = 0; i < n; i++) flat.push(internal);
                    typeCounts[t] = (typeCounts[t] || 0) + n;
                }
                if (flat.length < minSize) continue;
                flat.sort();
                const sig = flat.join('||');
                const notation = buildCompNotation(typeCounts);
                const outcome = sessionOutcome.get(sid);

                let c = compStats.get(sig);
                if (!c) {
                    // De-aggregate to (internal, count) pairs for the response.
                    const counts = new Map();
                    for (const id of flat) counts.set(id, (counts.get(id) || 0) + 1);
                    const vehiclesArr = [...counts.entries()].map(([id, count]) => ({
                        vehicle_internal: id,
                        vehicle: vehStats.get(id).display,
                        type: vehStats.get(id).type,
                        count,
                    })).sort((a, b) => a.vehicle.localeCompare(b.vehicle));
                    c = {
                        vehicles: vehiclesArr,
                        types: typeCounts,
                        notation,
                        size: flat.length,
                        games: 0, wins: 0, losses: 0,
                    };
                    compStats.set(sig, c);
                }
                c.games++;
                if (outcome === 'win') c.wins++;
                else if (outcome === 'loss') c.losses++;

                let nc = notationStats.get(notation);
                if (!nc) {
                    nc = { notation, types: { ...typeCounts }, size: flat.length, games: 0, wins: 0, losses: 0 };
                    notationStats.set(notation, nc);
                }
                nc.games++;
                if (outcome === 'win') nc.wins++;
                else if (outcome === 'loss') nc.losses++;
            }

            // Keep all distinct comps (so preset notations always have something
            // to match against). Cap at 500 as a sanity ceiling — even a year of
            // active play rarely produces more distinct vehicle multisets than
            // that with the size>=8 filter.
            const compositions = [...compStats.values()]
                .map(c => {
                    const decided = c.wins + c.losses;
                    return {
                        vehicles: c.vehicles,
                        types: c.types,
                        notation: c.notation,
                        size: c.size,
                        games: c.games,
                        wins: c.wins,
                        losses: c.losses,
                        win_rate: decided > 0 ? Math.round((c.wins / decided) * 1000) / 10 : 0,
                    };
                })
                .sort((a, b) => (b.games - a.games) || (b.win_rate - a.win_rate))
                .slice(0, 500);

            const notations = [...notationStats.values()]
                .map(n => {
                    const decided = n.wins + n.losses;
                    return {
                        notation: n.notation,
                        types: n.types,
                        size: n.size,
                        games: n.games,
                        wins: n.wins,
                        losses: n.losses,
                        win_rate: decided > 0 ? Math.round((n.wins / decided) * 1000) / 10 : 0,
                    };
                })
                .sort((a, b) => b.games - a.games);

            res.json({
                min_size: minSize,
                notations,
                total_sessions: totalSessions,
                top_vehicles: topVehicles,
                compositions,
            });
        }
    );
    });
});

// ─── PLAYER-SCOPED ANALYTICS ───────────────────────────────────────────────
// All endpoints take :uid plus optional ?start_date / ?end_date (unix seconds).
// We group by session because a player can have multiple rows per session
// (one per vehicle); for the per-session squadron we use MAX(squadron_name)
// — players don't switch squadrons mid-match.

function playerSessionDateClause(req) {
    const startDate = parseInt(req.query.start_date) || 0;
    const endDate = parseInt(req.query.end_date) || 0;
    let clause = '';
    const params = [];
    if (startDate) { clause += ' AND p.endtime_unix >= ?'; params.push(startDate); }
    if (endDate)   { clause += ' AND p.endtime_unix <= ?'; params.push(endDate); }
    return { clause, params };
}

app.get('/api/analytics/player/maps/:uid', (req, res) => {
    const uid = req.params.uid;
    const { clause, params } = playerSessionDateClause(req);

    db.all(
        `SELECT p.session_id,
                MAX(p.squadron_name) AS sq,
                m.map_name,
                m.winning_sq,
                m.losing_sq
         FROM player_games_hist p
         JOIN match_summary m ON m.session_id = p.session_id
         WHERE p.UID = ?${clause}
         GROUP BY p.session_id`,
        [uid, ...params],
        (err, rows) => {
            if (err) return res.status(500).json({ error: 'Database error' });
            const stats = {};
            for (const { sq, map_name, winning_sq, losing_sq } of rows) {
                if (!sq) continue;
                const norm = normalizeMapName(map_name) || { key: 'unknown', display: 'Unknown' };
                if (!stats[norm.key]) stats[norm.key] = { map_name: norm.display, wins: 0, losses: 0 };
                if (winning_sq === sq) stats[norm.key].wins++;
                else if (losing_sq === sq) stats[norm.key].losses++;
            }
            const result = Object.values(stats).map(r => ({
                ...r,
                total: r.wins + r.losses,
                win_rate: (r.wins + r.losses) > 0 ? Math.round(r.wins / (r.wins + r.losses) * 1000) / 10 : 0,
            })).sort((a, b) => b.total - a.total);
            res.json(result);
        }
    );
});

app.get('/api/analytics/player/time/:uid', (req, res) => {
    const uid = req.params.uid;
    const { clause, params } = playerSessionDateClause(req);

    db.all(
        `SELECT p.session_id,
                MAX(p.squadron_name) AS sq,
                MAX(p.endtime_unix) AS endtime_unix,
                m.winning_sq,
                m.losing_sq
         FROM player_games_hist p
         JOIN match_summary m ON m.session_id = p.session_id
         WHERE p.UID = ?${clause}
         GROUP BY p.session_id`,
        [uid, ...params],
        (err, rows) => {
            if (err) return res.status(500).json({ error: 'Database error' });
            const hourly = {};
            const daily = {};
            for (const { sq, endtime_unix, winning_sq, losing_sq } of rows) {
                if (!sq || !endtime_unix) continue;
                const d = new Date(endtime_unix * 1000);
                const hour = d.getUTCHours();
                if (!hourly[hour]) hourly[hour] = { wins: 0, losses: 0 };
                if (winning_sq === sq) hourly[hour].wins++;
                else if (losing_sq === sq) hourly[hour].losses++;

                const dayMs = Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate());
                daily[dayMs] = (daily[dayMs] || 0) + 1;
            }
            const hourlyOut = {};
            for (const hour of Object.keys(hourly).sort((a, b) => a - b)) {
                const s = hourly[hour];
                const total = s.wins + s.losses;
                hourlyOut[hour] = { ...s, total, win_rate: total > 0 ? Math.round(s.wins / total * 1000) / 10 : 0 };
            }
            const dailyOut = Object.entries(daily)
                .map(([day, count]) => ({ day: parseInt(day), count }))
                .sort((a, b) => a.day - b.day);
            res.json({ hourly: hourlyOut, daily: dailyOut });
        }
    );
});

app.get('/api/analytics/player/timeline/:uid', (req, res) => {
    // Returns one row per (session, vehicle) the player used. Frontend groups
    // by session for the K/D & WR lines and by week × vehicle for the stacked bar.
    const uid = req.params.uid;
    const { clause, params } = playerSessionDateClause(req);

    db.all(
        `SELECT p.session_id,
                p.endtime_unix,
                p.vehicle,
                p.ground_kills,
                p.air_kills,
                p.deaths,
                p.squadron_name AS sq,
                m.winning_sq,
                m.losing_sq
         FROM player_games_hist p
         LEFT JOIN match_summary m ON m.session_id = p.session_id
         WHERE p.UID = ?${clause}
         ORDER BY p.endtime_unix ASC`,
        [uid, ...params],
        (err, rows) => {
            if (err) return res.status(500).json({ error: 'Database error' });
            const result = rows.filter(r => r.endtime_unix).map(r => ({
                session_id: r.session_id,
                endtime_unix: r.endtime_unix,
                vehicle: r.vehicle || null,
                kills: (r.ground_kills || 0) + (r.air_kills || 0),
                deaths: r.deaths || 0,
                won: r.winning_sq === r.sq ? 1 : (r.losing_sq === r.sq ? 0 : null),
            }));
            res.json(result);
        }
    );
});

app.get('/api/analytics/player/squadmates/:uid', (req, res) => {
    const uid = req.params.uid;
    const { clause, params } = playerSessionDateClause(req);
    const limit = Math.max(1, Math.min(parseInt(req.query.limit) || 20, 50));

    db.all(
        `WITH shared_sessions AS (
            SELECT p.UID AS teammate_uid,
                   p.session_id,
                   me.squadron_name AS me_sq,
                   m.winning_sq,
                   m.losing_sq
            FROM player_games_hist p
            JOIN player_games_hist me
              ON me.session_id = p.session_id
             AND me.squadron_name = p.squadron_name
            LEFT JOIN match_summary m ON m.session_id = p.session_id
            WHERE me.UID = ?${clause}
              AND p.UID != ?
              AND p.UID IS NOT NULL
              AND p.UID != ''
              AND p.nick NOT LIKE 'coop/%'
              AND me.nick NOT LIKE 'coop/%'
        )
        SELECT teammate_uid AS uid,
               COUNT(DISTINCT session_id) AS shared,
               SUM(CASE WHEN winning_sq = me_sq THEN 1 ELSE 0 END) AS wins,
               SUM(CASE WHEN losing_sq = me_sq THEN 1 ELSE 0 END) AS losses
        FROM shared_sessions
        GROUP BY teammate_uid
        HAVING shared > 0
        ORDER BY shared DESC, wins DESC, uid ASC
        LIMIT ?`,
        [uid, ...params, uid, limit],
        (err, rows) => {
            if (err) return res.status(500).json({ error: 'Database error' });
            const teammateUids = rows.map(r => r.uid).filter(Boolean);
            if (!teammateUids.length) return res.json([]);

            const placeholders = teammateUids.map(() => '?').join(',');
            db.all(
                `SELECT UID AS uid, nick, COUNT(*) AS cnt, MAX(endtime_unix) AS last_seen
                 FROM player_games_hist
                 WHERE UID IN (${placeholders})
                   AND nick NOT LIKE 'coop/%'
                 GROUP BY UID, nick`,
                teammateUids,
                (nickErr, nickRows) => {
                    if (nickErr) return res.status(500).json({ error: 'Database error' });
                    const bestNicks = new Map();
                    for (const row of nickRows) {
                        const current = bestNicks.get(row.uid);
                        const candidate = {
                            nick: row.nick || row.uid,
                            cnt: row.cnt || 0,
                            last_seen: row.last_seen || 0,
                        };
                        if (!current || candidate.cnt > current.cnt || (candidate.cnt === current.cnt && candidate.last_seen > current.last_seen)) {
                            bestNicks.set(row.uid, candidate);
                        }
                    }

                    const result = rows.map(row => {
                        const wins = Number(row.wins) || 0;
                        const losses = Number(row.losses) || 0;
                        const shared = Number(row.shared) || 0;
                        const resolved = wins + losses;
                        return {
                            uid: row.uid,
                            nick: (bestNicks.get(row.uid) || {}).nick || row.uid,
                            shared,
                            wins,
                            losses,
                            win_rate: resolved > 0 ? Math.round((wins / resolved) * 1000) / 10 : 0,
                        };
                    });

                    res.json(result);
                }
            );
        }
    );
});

app.get('/api/analytics/player/matchup/:uid', (req, res) => {
    const uid = req.params.uid;
    const { clause, params } = playerSessionDateClause(req);

    db.all(
        `SELECT p.session_id,
                MAX(p.squadron_name) AS sq,
                m.winning_sq,
                m.losing_sq
         FROM player_games_hist p
         JOIN match_summary m ON m.session_id = p.session_id
         WHERE p.UID = ?${clause}
         GROUP BY p.session_id`,
        [uid, ...params],
        (err, rows) => {
            if (err) return res.status(500).json({ error: 'Database error' });
            const stats = {};
            for (const { sq, winning_sq, losing_sq } of rows) {
                if (!sq || !winning_sq || !losing_sq || winning_sq === losing_sq) continue;
                const opponent = winning_sq === sq ? losing_sq : (losing_sq === sq ? winning_sq : null);
                if (!opponent) continue;
                if (!stats[opponent]) stats[opponent] = { opponent, wins: 0, losses: 0 };
                if (winning_sq === sq) stats[opponent].wins++;
                else stats[opponent].losses++;
            }
            const enriched = Object.values(stats).map(r => {
                const total = r.wins + r.losses;
                return { ...r, total, win_rate: total > 0 ? Math.round(r.wins / total * 1000) / 10 : 0 };
            });
            const wonAgainst = [...enriched]
                .sort((a, b) => (b.wins - a.wins) || (b.total - a.total))
                .slice(0, 10);
            const lostAgainst = [...enriched]
                .sort((a, b) => (b.losses - a.losses) || (b.total - a.total))
                .slice(0, 10);
            res.json({ won_against: wonAgainst, lost_against: lostAgainst, total_opponents: enriched.length });
        }
    );
});

// ─── VEHICLE-SCOPED ANALYTICS ──────────────────────────────────────────────

function vehicleDateClause(req) {
    const startDate = parseInt(req.query.start_date) || 0;
    const endDate = parseInt(req.query.end_date) || 0;
    let clause = '';
    const params = [];
    if (startDate) { clause += ' AND p.endtime_unix >= ?'; params.push(startDate); }
    if (endDate)   { clause += ' AND p.endtime_unix <= ?'; params.push(endDate); }
    return { clause, params };
}

// Set of vehicle_internal ids that have data, with their total spawn counts.
// The display name is *not* served here — the client looks it up in the
// translation map (/api/i18n/vehicles), which is keyed by internal and stores
// per-language names with country/event glyphs (▄ ◢ ◊ ␗) baked in (see
// BOT/utils.init_vehicle_translation_cache, strip_decorations=False). That map
// is the single source of truth for display.
let vehicleListCache = null;
function ensureVehicleList(cb) {
    if (vehicleListCache) return cb(null, vehicleListCache);
    heavyDb.all(
        `SELECT LOWER(vehicle_internal) AS vehicle_internal, COUNT(*) AS total
         FROM player_games_hist
         WHERE vehicle_internal IS NOT NULL AND vehicle_internal != ''
         GROUP BY vehicle_internal COLLATE NOCASE`,
        [],
        (err, rows) => {
            if (err) return cb(err);
            vehicleListCache = rows
                .filter(r => r.vehicle_internal && r.vehicle_internal !== 'disconnected')
                .map(r => ({ vehicle_internal: r.vehicle_internal, total: r.total || 0 }))
                .sort((a, b) => b.total - a.total);
            cb(null, vehicleListCache);
        }
    );
}

app.get('/api/analytics/vehicle-list', (req, res) => {
    const cacheKey = 'analytics_vehicle_list';
    const cached = getCachedResponse(cacheKey);
    if (cached) return res.json(cached);

    ensureVehicleList((err, list) => {
        if (err) return res.status(500).json({ error: 'Database error' });
        const out = { vehicles: list };
        setCachedResponse(cacheKey, out);
        res.json(out);
    });
});

// Per-vehicle endpoints key on vehicle_internal (case-insensitive) so casing
// duplicates in player_games_hist collapse, and country variants stay distinct.
app.get('/api/analytics/vehicle/stats/:internal', (req, res) => {
    const internal = req.params.internal;
    const { clause, params } = vehicleDateClause(req);
    db.all(
        `SELECT p.UID, p.session_id, p.squadron_name AS sq,
                p.ground_kills, p.air_kills, p.deaths,
                m.winning_sq, m.losing_sq
         FROM player_games_hist p
         LEFT JOIN match_summary m ON m.session_id = p.session_id
         WHERE p.vehicle_internal = ? COLLATE NOCASE${clause}`,
        [internal, ...params],
        (err, rows) => {
            if (err) return res.status(500).json({ error: 'Database error' });
            let kills = 0, deaths = 0, wins = 0, losses = 0;
            const players = new Set(), sessions = new Set();
            for (const r of rows) {
                kills  += (r.ground_kills || 0) + (r.air_kills || 0);
                deaths += r.deaths || 0;
                if (r.UID) players.add(r.UID);
                if (r.session_id) sessions.add(r.session_id);
                if (r.sq && r.winning_sq === r.sq) wins++;
                else if (r.sq && r.losing_sq === r.sq) losses++;
            }
            const decided = wins + losses;
            res.json({
                spawns: rows.length,
                unique_players: players.size,
                sessions: sessions.size,
                kills, deaths,
                kd: deaths > 0 ? Math.round((kills / deaths) * 100) / 100 : kills,
                ks: rows.length > 0 ? Math.round((kills / rows.length) * 100) / 100 : 0,
                win_rate: decided > 0 ? Math.round((wins / decided) * 1000) / 10 : 0,
                wins, losses,
            });
        }
    );
});

app.get('/api/analytics/vehicle/players/:internal', (req, res) => {
    const internal = req.params.internal;
    const minGames = parseInt(req.query.min_games) || 3;
    const { clause, params } = vehicleDateClause(req);
    db.all(
        `SELECT p.UID, p.nick, p.squadron_name AS sq,
                p.session_id,
                p.ground_kills, p.air_kills, p.deaths,
                m.winning_sq, m.losing_sq
         FROM player_games_hist p
         LEFT JOIN match_summary m ON m.session_id = p.session_id
         WHERE p.vehicle_internal = ? COLLATE NOCASE${clause}`,
        [internal, ...params],
        (err, rows) => {
            if (err) return res.status(500).json({ error: 'Database error' });
            const stats = {};
            for (const r of rows) {
                if (!r.UID) continue;
                let s = stats[r.UID];
                if (!s) { s = { uid: r.UID, nick: r.nick, sq: r.sq, kills: 0, deaths: 0, wins: 0, losses: 0, sessions: new Set() }; stats[r.UID] = s; }
                s.nick = r.nick;
                s.sq = r.sq;
                s.kills += (r.ground_kills || 0) + (r.air_kills || 0);
                s.deaths += r.deaths || 0;
                if (r.session_id) s.sessions.add(r.session_id);
                if (r.sq && r.winning_sq === r.sq) s.wins++;
                else if (r.sq && r.losing_sq === r.sq) s.losses++;
            }
            const out = Object.values(stats)
                .map(s => {
                    const decided = s.wins + s.losses;
                    return {
                        uid: s.uid,
                        nick: s.nick,
                        squadron: s.sq || null,
                        games: s.sessions.size,
                        spawns: s.kills + s.deaths > 0 ? undefined : undefined,  // placeholder
                        kills: s.kills, deaths: s.deaths,
                        kd: s.deaths > 0 ? Math.round((s.kills / s.deaths) * 100) / 100 : s.kills,
                        win_rate: decided > 0 ? Math.round((s.wins / decided) * 1000) / 10 : 0,
                        wins: s.wins, losses: s.losses,
                    };
                })
                .filter(s => s.games >= minGames)
                .sort((a, b) => b.games - a.games)
                .slice(0, 500);
            res.json(out);
        }
    );
});

app.get('/api/analytics/vehicle/squadrons/:internal', (req, res) => {
    const internal = req.params.internal;
    const minGames = parseInt(req.query.min_games) || 3;
    const { clause, params } = vehicleDateClause(req);
    db.all(
        `SELECT p.squadron_name AS sq,
                p.session_id,
                p.ground_kills, p.air_kills, p.deaths,
                m.winning_sq, m.losing_sq
         FROM player_games_hist p
         LEFT JOIN match_summary m ON m.session_id = p.session_id
         WHERE p.vehicle_internal = ? COLLATE NOCASE${clause}`,
        [internal, ...params],
        (err, rows) => {
            if (err) return res.status(500).json({ error: 'Database error' });
            const stats = {};
            for (const r of rows) {
                if (!r.sq) continue;
                let s = stats[r.sq];
                if (!s) { s = { squadron: r.sq, kills: 0, deaths: 0, wins: 0, losses: 0, sessions: new Set() }; stats[r.sq] = s; }
                s.kills += (r.ground_kills || 0) + (r.air_kills || 0);
                s.deaths += r.deaths || 0;
                if (r.session_id) s.sessions.add(r.session_id);
                if (r.winning_sq === r.sq) s.wins++;
                else if (r.losing_sq === r.sq) s.losses++;
            }
            const out = Object.values(stats)
                .map(s => {
                    const decided = s.wins + s.losses;
                    return {
                        squadron: s.squadron,
                        games: s.sessions.size,
                        kills: s.kills, deaths: s.deaths,
                        kd: s.deaths > 0 ? Math.round((s.kills / s.deaths) * 100) / 100 : s.kills,
                        win_rate: decided > 0 ? Math.round((s.wins / decided) * 1000) / 10 : 0,
                        wins: s.wins, losses: s.losses,
                    };
                })
                .filter(s => s.games >= minGames)
                .sort((a, b) => b.games - a.games)
                .slice(0, 500);
            res.json(out);
        }
    );
});

app.get('/api/analytics/vehicle/maps/:internal', (req, res) => {
    const internal = req.params.internal;
    const { clause, params } = vehicleDateClause(req);
    db.all(
        `SELECT p.session_id, p.squadron_name AS sq,
                m.map_name, m.winning_sq, m.losing_sq
         FROM player_games_hist p
         JOIN match_summary m ON m.session_id = p.session_id
         WHERE p.vehicle_internal = ? COLLATE NOCASE${clause}
         GROUP BY p.session_id, p.squadron_name`,
        [internal, ...params],
        (err, rows) => {
            if (err) return res.status(500).json({ error: 'Database error' });
            const stats = {};
            for (const { sq, map_name, winning_sq, losing_sq } of rows) {
                if (!sq) continue;
                const norm = normalizeMapName(map_name) || { key: 'unknown', display: 'Unknown' };
                if (!stats[norm.key]) stats[norm.key] = { map_name: norm.display, wins: 0, losses: 0 };
                if (winning_sq === sq) stats[norm.key].wins++;
                else if (losing_sq === sq) stats[norm.key].losses++;
            }
            const result = Object.values(stats).map(r => ({
                ...r,
                total: r.wins + r.losses,
                win_rate: (r.wins + r.losses) > 0 ? Math.round(r.wins / (r.wins + r.losses) * 1000) / 10 : 0,
            })).sort((a, b) => b.total - a.total);
            res.json(result);
        }
    );
});

// ─── i18n: vehicle translation map ────────────────────────────────────
// Serves a flat object: { internal_id: { en: "...", ru: "...", ... } }
// produced by BOT/utils.init_vehicle_translation_cache() (Python). Falls back
// to the English-only `vehicle_data_cache.json` so the page works even before
// the multi-lang cache has been generated.

let _vehicleTranslationsResponse = null;
let _vehicleTranslationsMtime = 0;

function buildTranslationsResponse() {
    const fullPath = path.join(STORAGE_ROOT, 'CACHE', 'vehicle_translations.json');
    const englishPath = path.join(STORAGE_ROOT, 'CACHE', 'vehicle_data_cache_all.json');
    const englishFallback = path.join(STORAGE_ROOT, 'CACHE', 'vehicle_data_cache.json');

    if (fs.existsSync(fullPath)) {
        const stat = fs.statSync(fullPath);
        if (_vehicleTranslationsResponse && stat.mtimeMs === _vehicleTranslationsMtime) {
            return _vehicleTranslationsResponse;
        }
        const data = JSON.parse(fs.readFileSync(fullPath, 'utf-8'));
        _vehicleTranslationsResponse = { source: 'multilang', vehicles: data };
        _vehicleTranslationsMtime = stat.mtimeMs;
        return _vehicleTranslationsResponse;
    }

    const target = fs.existsSync(englishPath) ? englishPath : (fs.existsSync(englishFallback) ? englishFallback : null);
    if (!target) return { source: 'none', vehicles: {} };
    const stat = fs.statSync(target);
    if (_vehicleTranslationsResponse && stat.mtimeMs === _vehicleTranslationsMtime) {
        return _vehicleTranslationsResponse;
    }
    const raw = JSON.parse(fs.readFileSync(target, 'utf-8'));
    const vehicles = {};
    for (const entry of raw) {
        if (!Array.isArray(entry) || entry.length < 2) continue;
        const cdk = entry[0];
        const englishName = entry[1];
        vehicles[cdk] = { en: englishName };
    }
    _vehicleTranslationsResponse = { source: 'english_only', vehicles };
    _vehicleTranslationsMtime = stat.mtimeMs;
    return _vehicleTranslationsResponse;
}

app.get('/api/i18n/vehicles', (req, res) => {
    res.set('Cache-Control', 'public, max-age=3600');
    res.json(buildTranslationsResponse());
});

app.get('/api/i18n/vehicle-types', (req, res) => {
    const map = loadVehicleMetaCache();
    const types = {};
    for (const [internal, meta] of map.entries()) {
        types[internal] = meta && meta.type ? meta.type : '?';
    }
    res.set('Cache-Control', 'public, max-age=3600');
    res.json({ source: 'vehicle_meta_cache', types });
});

app.use((err, req, res, next) => {
    console.error('Unhandled error:', err);
    res.status(500).json({
        error: 'Internal server error'
    });
});

app.use((req, res) => {
    res.status(404).json({
        error: 'Endpoint not found',
        availableEndpoints: [
            'GET /api/player/:uid',
            'GET /api/player/:uid/games',
            'GET /api/search/:nickname',
            'GET /api/live',
            'GET /api/leaderboard/players',
            'GET /api/leaderboard/squadrons',
            'GET /api/leaderboard/vehicles',
            'GET /api/leaderboard/stats',
            'GET /api/squadrons/:squadronname',
            'GET /api/analytics/maps/:squadron',
            'GET /api/analytics/player/squadmates/:uid',
            'GET /api/analytics/time/:squadron',
            'GET /api/analytics/comps/:squadron',
            'GET /api/analytics/consistency/:squadron',
            'GET /api/i18n/vehicles',
            'GET /api/analytics/matchup/:squadron',
            'GET /api/debug/schema',
            'GET /health',
            'GET /api/info',
            'GET /api/i18n/vehicle-types'
        ]
    });
});


// Periodic database liveness check (lightweight query, every 5 minutes)
setInterval(() => {
    db.get("SELECT 1", (err) => {
        if (err) log.error('Database liveness check failed', err);
    });
}, 300000);

// Periodic WAL checkpoint every 10 min. PASSIVE mode is intentional: TRUNCATE
// blocks db's worker thread while waiting for heavyDb readers (vehicle/player
// leaderboards run 47–133s), which serializes all db queries behind it.
// PASSIVE never blocks — it checkpoints what it can and skips if readers are active.
setTimeout(() => {
    setInterval(() => {
        runWalCheckpoint('PASSIVE', 'Periodic WAL checkpoint completed', 'Periodic WAL checkpoint failed:', 'debug');
    }, 600000); // Every 10 minutes
}, 150000); // Start after 2.5 min offset

app.listen(PORT, () => {
    console.log(`SREBOT Player API server running on port ${PORT}`);
    console.log(`Health check: http://localhost:${PORT}/health`);
    console.log(`API info: http://localhost:${PORT}/api/info`);
    log.info('Database refresh interval set to 60 seconds');
});

// All-time benchmark warmup used to run here at +5s. Removed: the dual
// CTE-heavy benchmark queries (player + squadron, each ~minutes on the full
// 4M-row table) were the dominant startup cost and blocked the read connection
// against incoming user traffic. The cache fills lazily on first request now
// (TTL is 1 hour, see PERFORMANCE_BENCHMARK_CACHE_TTL).

// Last-resort guards. SQLite BUSY/IOERR errors that escape callback handling
// would otherwise tear down the API and put PM2 into a restart loop (we hit
// 127 restarts in one afternoon from a BUSY on squadrons.db). Log and keep
// running — every code path that can produce these has its own handler now,
// and a real bug should surface in the logs rather than via a crash.
process.on('uncaughtException', (err) => {
    log.error('Uncaught exception (continuing)', err, { message: err && err.message, code: err && err.code });
});
process.on('unhandledRejection', (reason) => {
    const r = reason instanceof Error ? reason : new Error(String(reason));
    log.error('Unhandled rejection (continuing)', r, { message: r.message, code: r.code });
});

process.on('SIGINT', () => {
    console.log('\nShutting down server...');
    db.close((err) => {
        if (err) {
            console.error('Error closing database:', err.message);
        } else {
            console.log('Database connection closed.');
        }
        process.exit(0);
    });
});
