"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.archiveOldTraces = archiveOldTraces;
exports.checkIncompleteArchive = checkIncompleteArchive;
const fs = __importStar(require("node:fs"));
const path = __importStar(require("node:path"));
const client_1 = require("@libsql/client");
const drizzle_orm_1 = require("drizzle-orm");
const libsql_1 = require("drizzle-orm/libsql");
const logger_js_1 = __importDefault(require("../logger.js"));
const archive_js_1 = require("../server/models/archive.js");
const trace_js_1 = require("../server/models/trace.js");
const archive_fs_js_1 = require("./archive-fs.js");
function containsPathTraversal(filePath) {
    return filePath.includes("..");
}
/**
 * Archive old traces to separate database file
 * Implements the 7-stage archive process defined in Intent document
 */
async function archiveOldTraces(dbPath, config) {
    const startTime = Date.now();
    let releaseLock = null;
    let mainClient = null;
    let archiveClient = null;
    let archiveFilePath = null;
    try {
        // ========================================
        // Phase 1: Prepare & Lock
        // ========================================
        // 1.1 Acquire file lock to prevent concurrent archive operations
        const lockPath = `${dbPath}.archive.lock`;
        releaseLock = await (0, archive_fs_js_1.acquireFileLock)(lockPath);
        if (!releaseLock) {
            logger_js_1.default.warn("[Archive] another archive process is running, skipping");
            return {
                success: false,
                archivedTraces: 0,
                archivedSpans: 0,
                duration: Date.now() - startTime,
                error: "Another archive process is running",
            };
        }
        // 1.2 Calculate cutoff time
        const cutoffTime = Date.now() - config.activeDays * 24 * 60 * 60 * 1000;
        // 1.3 Open main database
        mainClient = (0, client_1.createClient)({ url: `file:${dbPath}` });
        const drizzleDb = (0, libsql_1.drizzle)(mainClient);
        // 1.4 Query old traces (endTime < cutoffTime, not already archived)
        const oldTraces = await drizzleDb.select().from(trace_js_1.Trace).where((0, drizzle_orm_1.lt)(trace_js_1.Trace.endTime, cutoffTime));
        logger_js_1.default.info("[Archive] found traces older than cutoff", {
            count: oldTraces.length,
            cutoffDate: new Date(cutoffTime).toISOString(),
        });
        // 1.5 If no data to archive, skip
        if (oldTraces.length === 0) {
            mainClient.close();
            mainClient = null;
            await releaseLock();
            return {
                success: true,
                archivedTraces: 0,
                archivedSpans: 0,
                duration: Date.now() - startTime,
            };
        }
        // ========================================
        // Phase 2: Create Archive File
        // ========================================
        // 2.1 Determine archive directory
        const archiveDir = config.archiveDir || path.join(path.dirname(dbPath), "archives");
        // Security: Check for path traversal
        if (containsPathTraversal(archiveDir)) {
            throw new Error(`Path traversal detected in archiveDir: ${archiveDir}`);
        }
        await (0, archive_fs_js_1.ensureArchiveDir)(archiveDir);
        // 2.2 Generate archive filename with timestamp (use data start date + current timestamp)
        const dateRangeStart = Math.min(...oldTraces.map((t) => t.startTime));
        const archiveDate = new Date(dateRangeStart);
        const archiveFileName = (0, archive_fs_js_1.generateArchiveFilename)(archiveDate);
        archiveFilePath = path.join(archiveDir, archiveFileName);
        // 2.4 Check disk space (need at least 2x estimated size)
        const estimatedSize = oldTraces.length * 10 * 1024; // Estimate 10KB per trace
        const availableSpace = await (0, archive_fs_js_1.checkDiskSpace)(archiveDir);
        if (availableSpace < estimatedSize * 2) {
            // Try to free space by cleaning up old archives
            logger_js_1.default.warn("[Archive] disk space low, cleaning up old archives", {
                available: availableSpace,
                required: estimatedSize * 2,
            });
            await (0, archive_fs_js_1.cleanupOldArchives)(archiveDir, Math.max(1, config.archiveMaxFiles - 1));
            // Check again
            const newAvailableSpace = await (0, archive_fs_js_1.checkDiskSpace)(archiveDir);
            if (newAvailableSpace < estimatedSize * 2) {
                throw new Error(`Insufficient disk space: need ${estimatedSize * 2} bytes, available ${newAvailableSpace} bytes`);
            }
        }
        // 2.5 Create empty archive SQLite database
        archiveClient = (0, client_1.createClient)({ url: `file:${archiveFilePath}` });
        // Create schema in archive (same as main database)
        await archiveClient.execute(`
      CREATE TABLE IF NOT EXISTS Trace (
        id TEXT PRIMARY KEY NOT NULL,
        rootId TEXT,
        parentId TEXT,
        name TEXT NOT NULL,
        startTime INTEGER NOT NULL,
        endTime INTEGER NOT NULL,
        status TEXT NOT NULL,
        attributes TEXT NOT NULL,
        links TEXT,
        events TEXT,
        userId TEXT,
        sessionId TEXT,
        componentId TEXT,
        action INTEGER,
        token INTEGER DEFAULT 0,
        cost REAL DEFAULT 0,
        remark TEXT,
        isImport INTEGER DEFAULT 0,
        archived INTEGER DEFAULT 0,
        archivedAt INTEGER
      )
    `);
        // ========================================
        // Phase 3: Migrate Data (Batch Processing)
        // ========================================
        logger_js_1.default.info("[Archive] migrating data to archive file", { archiveFile: archiveFileName });
        const drizzleArchiveDb = (0, libsql_1.drizzle)(archiveClient);
        const BATCH_SIZE = 1000;
        // 3.1 Batch migrate traces
        for (let i = 0; i < oldTraces.length; i += BATCH_SIZE) {
            const batch = oldTraces.slice(i, i + BATCH_SIZE);
            // Insert batch into archive database using transaction
            await archiveClient.execute("BEGIN TRANSACTION");
            try {
                for (const trace of batch) {
                    await drizzleArchiveDb.insert(trace_js_1.Trace).values({
                        ...trace,
                        archived: 0, // Reset archived flag in archive
                        archivedAt: null,
                    });
                }
                await archiveClient.execute("COMMIT");
            }
            catch (error) {
                await archiveClient.execute("ROLLBACK");
                throw error;
            }
        }
        // ========================================
        // Phase 4: Verify Integrity
        // ========================================
        logger_js_1.default.info("[Archive] verifying archive integrity");
        // 4.1 Close archive DB before checksum calculation
        archiveClient.close();
        archiveClient = null;
        // 4.2 Calculate SHA256 checksum
        const checksum = await (0, archive_fs_js_1.calculateFileChecksum)(archiveFilePath);
        // 4.3 Verify archive file is readable and has correct count
        const verifyClient = (0, client_1.createClient)({ url: `file:${archiveFilePath}` });
        const countResult = await verifyClient.execute("SELECT COUNT(*) as cnt FROM Trace");
        const cnt = Number(countResult.rows[0]?.cnt ?? 0);
        verifyClient.close();
        if (cnt !== oldTraces.length) {
            // Verification failed, delete archive file
            logger_js_1.default.error("[Archive] verification failed", {
                expected: oldTraces.length,
                actual: cnt,
            });
            fs.unlinkSync(archiveFilePath);
            throw new Error(`Archive verification failed: expected ${oldTraces.length} traces, got ${cnt}`);
        }
        // ========================================
        // Phase 5: Mark Data as Archived
        // ========================================
        // 5.1 Mark traces as archived (two-phase delete)
        const traceIds = oldTraces.map((t) => t.id);
        const archivedAt = Date.now();
        await drizzleDb
            .update(trace_js_1.Trace)
            .set({ archived: 1, archivedAt })
            .where((0, drizzle_orm_1.inArray)(trace_js_1.Trace.id, traceIds));
        // ========================================
        // Phase 6: Record Metadata
        // ========================================
        // 6.1 Calculate date range
        const dateRangeEnd = Math.max(...oldTraces.map((t) => t.endTime));
        const fileSize = fs.statSync(archiveFilePath).size;
        // 6.2 Check if archive_metadata table exists, create if not
        const tableExistsResult = await mainClient.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='ArchiveMetadata'");
        if (tableExistsResult.rows.length === 0) {
            await mainClient.execute(`
        CREATE TABLE IF NOT EXISTS ArchiveMetadata (
          id TEXT PRIMARY KEY NOT NULL,
          archiveFile TEXT NOT NULL UNIQUE,
          createdAt INTEGER NOT NULL,
          dateRangeStart INTEGER NOT NULL,
          dateRangeEnd INTEGER NOT NULL,
          traceCount INTEGER NOT NULL,
          spanCount INTEGER NOT NULL,
          fileSize INTEGER,
          checksum TEXT
        )
      `);
            await mainClient.execute("CREATE INDEX IF NOT EXISTS archive_metadata_created_at_idx ON ArchiveMetadata(createdAt)");
            await mainClient.execute("CREATE INDEX IF NOT EXISTS archive_metadata_archive_file_idx ON ArchiveMetadata(archiveFile)");
        }
        // 6.3 Insert or update metadata record (update if archive file already exists)
        await drizzleDb
            .insert(archive_js_1.ArchiveMetadata)
            .values({
            archiveFile: archiveFileName,
            createdAt: Date.now(),
            dateRangeStart,
            dateRangeEnd,
            traceCount: oldTraces.length,
            spanCount: 0, // Simplified - no separate spans table
            fileSize,
            checksum,
        })
            .onConflictDoUpdate({
            target: archive_js_1.ArchiveMetadata.archiveFile,
            set: {
                createdAt: (0, drizzle_orm_1.sql) `excluded.createdAt`,
                dateRangeStart: (0, drizzle_orm_1.sql) `excluded.dateRangeStart`,
                dateRangeEnd: (0, drizzle_orm_1.sql) `excluded.dateRangeEnd`,
                traceCount: (0, drizzle_orm_1.sql) `excluded.traceCount`,
                spanCount: (0, drizzle_orm_1.sql) `excluded.spanCount`,
                fileSize: (0, drizzle_orm_1.sql) `excluded.fileSize`,
                checksum: (0, drizzle_orm_1.sql) `excluded.checksum`,
            },
        });
        // ========================================
        // Phase 7: Physical Delete & Cleanup
        // ========================================
        // 7.1 Delete marked traces from main database
        logger_js_1.default.info("[Archive] deleting archived traces from main database");
        await drizzleDb.delete(trace_js_1.Trace).where((0, drizzle_orm_1.eq)(trace_js_1.Trace.archived, 1));
        // 7.2 Cleanup old archives
        const deletedArchives = await (0, archive_fs_js_1.cleanupOldArchives)(archiveDir, config.archiveMaxFiles);
        if (deletedArchives.length > 0) {
            logger_js_1.default.info("[Archive] cleaned up old archive files", {
                deleted: deletedArchives,
            });
        }
        // 7.3 Close main database
        mainClient.close();
        mainClient = null;
        // 7.4 Release file lock
        await releaseLock();
        releaseLock = null;
        const duration = Date.now() - startTime;
        logger_js_1.default.info("[Archive] archive operation completed", {
            archivedTraces: oldTraces.length,
            archiveFile: archiveFileName,
            fileSize: fs.statSync(archiveFilePath).size,
            duration: `${duration}ms`,
        });
        return {
            success: true,
            archiveFile: archiveFileName,
            archivedTraces: oldTraces.length,
            archivedSpans: 0,
            duration,
            checksum,
        };
    }
    catch (error) {
        // Cleanup on error
        if (archiveClient) {
            try {
                archiveClient.close();
            }
            catch { }
        }
        if (mainClient) {
            try {
                // Rollback archived flag if set
                const drizzleDb = (0, libsql_1.drizzle)(mainClient);
                await drizzleDb
                    .update(trace_js_1.Trace)
                    .set({ archived: 0, archivedAt: null })
                    .where((0, drizzle_orm_1.eq)(trace_js_1.Trace.archived, 1));
                mainClient.close();
            }
            catch { }
        }
        // Delete incomplete archive file if created
        if (archiveFilePath && fs.existsSync(archiveFilePath)) {
            try {
                fs.unlinkSync(archiveFilePath);
            }
            catch { }
        }
        // Release lock if held
        if (releaseLock) {
            try {
                await releaseLock();
            }
            catch { }
        }
        return {
            success: false,
            archivedTraces: 0,
            archivedSpans: 0,
            duration: Date.now() - startTime,
            error: error.message,
        };
    }
}
/**
 * Check for incomplete archive from previous crash
 * Returns true if incomplete archive was found and reset
 */
async function checkIncompleteArchive(dbPath) {
    let mainClient = null;
    try {
        mainClient = (0, client_1.createClient)({ url: `file:${dbPath}` });
        const drizzleDb = (0, libsql_1.drizzle)(mainClient);
        // Check if any traces are marked as archived but not deleted
        const markedTraces = await drizzleDb.select().from(trace_js_1.Trace).where((0, drizzle_orm_1.eq)(trace_js_1.Trace.archived, 1)).limit(1);
        if (markedTraces.length > 0) {
            logger_js_1.default.warn("[Archive] found incomplete archive from previous crash, resetting...");
            // Conservative strategy: reset archived flag
            await drizzleDb
                .update(trace_js_1.Trace)
                .set({ archived: 0, archivedAt: null })
                .where((0, drizzle_orm_1.eq)(trace_js_1.Trace.archived, 1));
            mainClient.close();
            logger_js_1.default.info("[Archive] incomplete archive state has been reset");
            return true;
        }
        mainClient.close();
        return false;
    }
    catch (_error) {
        if (mainClient) {
            try {
                mainClient.close();
            }
            catch { }
        }
        return false;
    }
}
