// Persists Discord Gateway session data (session_id, resume_url, sequence)
// to disk so discord.js can RESUME instead of fresh IDENTIFY on restarts.
//
// RESUME skips the full Gateway handshake and guild hydration, saving
// 500-2000ms on cold starts. Critical for scale-to-zero where the bot
// process is killed and restarted frequently.
//
// Session data is stored as a JSON file in the kimaki data directory.
// It's ephemeral — if the file is missing or stale, discord.js falls
// back to IDENTIFY automatically.
//
// Implementation: uses the public `buildStrategy` hook in discord.js
// ClientOptions.ws. Inside buildStrategy, the @discordjs/ws manager's
// options.retrieveSessionInfo and options.updateSessionInfo callbacks
// are replaced with disk-backed versions BEFORE any shard connects.
// This is the approved extension point — no monkey-patching of private
// internals or method overrides needed.
import fs from 'node:fs';
import path from 'node:path';
import { SimpleShardingStrategy, } from '@discordjs/ws';
import { getDataDir } from './config.js';
import { createLogger, LogPrefix } from './logger.js';
const logger = createLogger(LogPrefix.DISCORD);
// Gateway sessions expire after 60 seconds of disconnection per Discord docs.
// Use a conservative 30s window — if the file is older than this, RESUME
// will likely fail anyway and discord.js falls back to IDENTIFY.
const MAX_SESSION_AGE_MS = 30_000;
function getSessionFilePath() {
    return path.join(getDataDir(), 'gateway-session.json');
}
function loadPersistedSessions() {
    const filePath = getSessionFilePath();
    try {
        const raw = fs.readFileSync(filePath, 'utf-8');
        const parsed = JSON.parse(raw);
        if (!parsed.shards || typeof parsed.savedAt !== 'number') {
            return null;
        }
        if (Date.now() - parsed.savedAt > MAX_SESSION_AGE_MS) {
            logger.log('Gateway session file too old, will IDENTIFY fresh');
            return null;
        }
        return parsed;
    }
    catch {
        return null;
    }
}
function savePersistedSessions(sessions) {
    const filePath = getSessionFilePath();
    try {
        fs.writeFileSync(filePath, JSON.stringify(sessions), 'utf-8');
    }
    catch (error) {
        logger.warn(`Failed to persist gateway session: ${error instanceof Error ? error.message : String(error)}`);
    }
}
/**
 * Creates a `buildStrategy` function for discord.js ClientOptions.ws
 * that enables Gateway session persistence for RESUME on restarts.
 *
 * Usage:
 * ```ts
 * const client = new Client({
 *   ws: { buildStrategy: buildSessionResumeStrategy() },
 * })
 * ```
 *
 * How it works:
 * - discord.js calls buildStrategy(manager) during WebSocketManager construction
 * - We replace manager.options.retrieveSessionInfo/updateSessionInfo with
 *   disk-backed versions BEFORE any shard connects
 * - On startup, persisted session data enables RESUME instead of IDENTIFY
 * - On shutdown, flushPersist() writes the latest session data to disk
 *
 * Returns the buildStrategy function AND a flushPersist handle so the
 * caller can ensure data is written on graceful shutdown.
 */
export function buildSessionResumeStrategy() {
    // In-memory cache of current session info per shard
    const currentSessions = {};
    // Load any persisted sessions from last run
    const persisted = loadPersistedSessions();
    if (persisted) {
        Object.assign(currentSessions, persisted.shards);
        const shardCount = Object.keys(persisted.shards).length;
        logger.log(`Loaded ${shardCount} persisted gateway session(s) for RESUME (age: ${Math.round((Date.now() - persisted.savedAt) / 1000)}s)`);
    }
    // Debounced disk writer. updateSessionInfo fires on every Gateway dispatch
    // (to update the sequence number), so writing on every call would thrash
    // disk. Debounce to at most one write per second.
    let pendingWrite = null;
    const schedulePersist = () => {
        if (frozen || pendingWrite) {
            return;
        }
        pendingWrite = setTimeout(() => {
            pendingWrite = null;
            if (frozen) {
                return;
            }
            savePersistedSessions({
                shards: currentSessions,
                savedAt: Date.now(),
            });
        }, 1_000);
    };
    // After flush, freeze persistence so destroy()'s updateSessionInfo(null)
    // doesn't overwrite the saved session data via schedulePersist.
    let frozen = false;
    const flushPersist = () => {
        if (pendingWrite) {
            clearTimeout(pendingWrite);
            pendingWrite = null;
        }
        const shardCount = Object.keys(currentSessions).length;
        if (shardCount > 0) {
            savePersistedSessions({
                shards: currentSessions,
                savedAt: Date.now(),
            });
            logger.log(`Persisted ${shardCount} gateway session(s) for next RESUME`);
        }
        frozen = true;
    };
    const buildStrategy = (manager) => {
        // Wrap the original callbacks from discord.js (which store in-memory
        // on the discord.js WebSocketShard.sessionInfo property).
        const originalRetrieve = manager.options.retrieveSessionInfo;
        const originalUpdate = manager.options.updateSessionInfo;
        // Replace retrieveSessionInfo: try persisted data first, then in-memory.
        // This runs BEFORE shard.connect() sends IDENTIFY or RESUME, so the
        // persisted session is available for the first connection attempt.
        manager.options.retrieveSessionInfo = (shardId) => {
            // Check in-memory cache (populated from disk on startup, or from
            // previous updateSessionInfo calls during this process lifetime)
            const cached = currentSessions[String(shardId)];
            if (cached) {
                logger.log(`[shard ${shardId}] Attempting RESUME (session: ${cached.sessionId.slice(0, 8)}..., seq: ${cached.sequence})`);
                return cached;
            }
            // Fall back to discord.js default (reads shard.sessionInfo)
            return originalRetrieve(shardId);
        };
        // Replace updateSessionInfo: persist to disk alongside in-memory update.
        manager.options.updateSessionInfo = (shardId, info) => {
            // Call original so discord.js shard.sessionInfo stays in sync
            originalUpdate(shardId, info);
            if (info) {
                currentSessions[String(shardId)] = info;
                schedulePersist();
            }
            else {
                // null = session invalidated (logout or invalid session).
                // Only delete this shard's data, not other shards'.
                delete currentSessions[String(shardId)];
                schedulePersist();
            }
        };
        return new SimpleShardingStrategy(manager);
    };
    return { buildStrategy, flushPersist };
}
