/**
 * Database Maintenance & Garbage Collection MCP Tools
 *
 * Provides storage-level maintenance operations that complement the
 * data-integrity checks in health.ts. Focuses on reclaiming disk space,
 * optimizing SQLite, and cleaning up stale artifacts.
 *
 * Tools: ocr_db_maintenance
 *
 * CRITICAL: NEVER use console.log() - stdout is reserved for JSON-RPC protocol.
 * Use console.error() for all logging.
 *
 * @module tools/maintenance
 */
import { z } from 'zod';
import { statSync, readdirSync, unlinkSync, existsSync } from 'fs';
import { join } from 'path';
import Database from 'better-sqlite3';
import { requireDatabase, getDefaultStoragePath, state } from '../server/state.js';
import { successResult } from '../server/types.js';
import { validateInput } from '../utils/validation.js';
import { formatResponse, handleError } from './shared.js';
import { getDatabasePath } from '../services/storage/database/helpers.js';
import { RegistryService } from '../services/storage/registry/index.js';
// ═══════════════════════════════════════════════════════════════════════════════
// INPUT SCHEMA
// ═══════════════════════════════════════════════════════════════════════════════
const MaintenanceInput = z.object({
    action: z
        .enum(['analyze', 'vacuum', 'wal_checkpoint', 'cleanup_backups', 'cleanup_temp', 'full'])
        .describe('Maintenance action: analyze (report sizes, WAL state, stale files), ' +
        'vacuum (defragment and reclaim space), wal_checkpoint (flush WAL to main db), ' +
        'cleanup_backups (remove old pre-migration backups beyond retention), ' +
        'cleanup_temp (remove stale WAL/SHM files for deleted databases), ' +
        'full (run all maintenance actions in sequence)'),
    database_name: z
        .string()
        .optional()
        .describe('Database name (uses current if not specified). Ignored for cleanup_temp.'),
    backup_retention: z
        .number()
        .int()
        .min(0)
        .max(10)
        .default(3)
        .describe('Number of pre-migration backups to retain per database (default 3)'),
    dry_run: z
        .boolean()
        .default(false)
        .describe('If true, report what would be cleaned without actually deleting anything'),
});
function getDbSizeReport(dbPath, conn) {
    const dbStat = existsSync(dbPath) ? statSync(dbPath) : { size: 0 };
    const walPath = `${dbPath}-wal`;
    const shmPath = `${dbPath}-shm`;
    const walStat = existsSync(walPath) ? statSync(walPath) : { size: 0 };
    const shmStat = existsSync(shmPath) ? statSync(shmPath) : { size: 0 };
    const pageCount = conn.pragma('page_count')[0]?.page_count ?? 0;
    const pageSize = conn.pragma('page_size')[0]?.page_size ?? 4096;
    const freelistCount = conn.pragma('freelist_count')[0]?.freelist_count ?? 0;
    // WAL page count from wal_checkpoint provides pages in the WAL
    let walPageCount = 0;
    try {
        const walInfo = conn.pragma('wal_checkpoint(PASSIVE)');
        walPageCount = walInfo[0]?.log ?? 0;
    }
    catch {
        // WAL checkpoint info not available
    }
    return {
        db_size_bytes: dbStat.size,
        wal_size_bytes: walStat.size,
        shm_size_bytes: shmStat.size,
        total_size_bytes: dbStat.size + walStat.size + shmStat.size,
        wal_page_count: walPageCount,
        freelist_count: freelistCount,
        page_count: pageCount,
        page_size: pageSize,
    };
}
function formatBytes(bytes) {
    if (bytes === 0)
        return '0 B';
    const units = ['B', 'KB', 'MB', 'GB'];
    const i = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1);
    return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${units[i]}`;
}
function findStaleFiles(storagePath) {
    const stale = [];
    if (!existsSync(storagePath))
        return stale;
    const files = readdirSync(storagePath);
    const dbFiles = new Set(files.filter(f => f.endsWith('.db')).map(f => f.slice(0, -3)));
    for (const file of files) {
        const fullPath = join(storagePath, file);
        // Stale WAL/SHM files for databases that no longer exist
        if (file.endsWith('-wal') || file.endsWith('-shm')) {
            const dbBaseName = file.endsWith('.db-wal') || file.endsWith('.db-shm')
                ? file.replace(/(\.db-wal|\.db-shm)$/, '')
                : file.replace(/(-wal|-shm)$/, '');
            if (!dbFiles.has(dbBaseName) && !existsSync(join(storagePath, `${dbBaseName}.db`))) {
                try {
                    const stat = statSync(fullPath);
                    stale.push({ path: fullPath, size: stat.size, type: 'orphaned_wal_shm' });
                }
                catch {
                    // File may have been removed
                }
            }
        }
        // Pre-migration backup files
        if (file.includes('.pre-migrate-v')) {
            try {
                const stat = statSync(fullPath);
                stale.push({ path: fullPath, size: stat.size, type: 'pre_migration_backup' });
            }
            catch {
                // File may have been removed
            }
        }
    }
    return stale;
}
// ═══════════════════════════════════════════════════════════════════════════════
// HANDLER: ocr_db_maintenance
// ═══════════════════════════════════════════════════════════════════════════════
async function handleMaintenance(params) {
    try {
        const input = validateInput(MaintenanceInput, params);
        const storagePath = getDefaultStoragePath();
        const results = {
            action: input.action,
            dry_run: input.dry_run,
        };
        const actions_performed = [];
        const runAnalyze = input.action === 'analyze' || input.action === 'full';
        const runVacuum = input.action === 'vacuum' || input.action === 'full';
        const runCheckpoint = input.action === 'wal_checkpoint' || input.action === 'full';
        const runCleanupBackups = input.action === 'cleanup_backups' || input.action === 'full';
        const runCleanupTemp = input.action === 'cleanup_temp' || input.action === 'full';
        // ──────────────────────────────────────────────────────────────
        // Get database connection (for db-specific actions)
        // ──────────────────────────────────────────────────────────────
        let conn = null;
        let dbPath = null;
        let shouldClose = false;
        const targetName = input.database_name ?? state.currentDatabaseName;
        if (targetName && (runAnalyze || runVacuum || runCheckpoint)) {
            if (state.currentDatabaseName === targetName) {
                conn = requireDatabase().db.getConnection();
                dbPath = getDatabasePath(targetName, storagePath);
            }
            else {
                dbPath = getDatabasePath(targetName, storagePath);
                if (existsSync(dbPath)) {
                    conn = new Database(dbPath);
                    shouldClose = true;
                }
            }
        }
        try {
            // ────────────────────────────────────────────────────────────
            // ANALYZE: Report sizes, WAL state, fragmentation
            // ────────────────────────────────────────────────────────────
            if (runAnalyze && conn && dbPath) {
                const sizeReport = getDbSizeReport(dbPath, conn);
                const fragmentationPct = sizeReport.page_count > 0
                    ? Math.round((sizeReport.freelist_count / sizeReport.page_count) * 100)
                    : 0;
                results.size_report = {
                    ...sizeReport,
                    db_size_human: formatBytes(sizeReport.db_size_bytes),
                    wal_size_human: formatBytes(sizeReport.wal_size_bytes),
                    total_size_human: formatBytes(sizeReport.total_size_bytes),
                    fragmentation_pct: fragmentationPct,
                };
                // Auto-maintenance recommendations
                const recommendations = [];
                if (sizeReport.wal_size_bytes > 50 * 1024 * 1024) {
                    recommendations.push(`WAL file is ${formatBytes(sizeReport.wal_size_bytes)} — run wal_checkpoint to flush`);
                }
                if (fragmentationPct > 10) {
                    recommendations.push(`Database is ${fragmentationPct}% fragmented — run vacuum to reclaim space`);
                }
                if (sizeReport.freelist_count > 1000) {
                    recommendations.push(`${sizeReport.freelist_count} free pages — vacuum will reclaim ~${formatBytes(sizeReport.freelist_count * sizeReport.page_size)}`);
                }
                results.recommendations = recommendations;
                actions_performed.push('analyze');
            }
            // ────────────────────────────────────────────────────────────
            // WAL CHECKPOINT: Flush WAL to main db file
            // ────────────────────────────────────────────────────────────
            if (runCheckpoint && conn && dbPath) {
                const beforeWal = existsSync(`${dbPath}-wal`) ? statSync(`${dbPath}-wal`).size : 0;
                if (!input.dry_run) {
                    const checkpointResult = conn.pragma('wal_checkpoint(TRUNCATE)');
                    const afterWal = existsSync(`${dbPath}-wal`) ? statSync(`${dbPath}-wal`).size : 0;
                    results.wal_checkpoint = {
                        before_wal_bytes: beforeWal,
                        after_wal_bytes: afterWal,
                        reclaimed_bytes: beforeWal - afterWal,
                        reclaimed_human: formatBytes(Math.max(0, beforeWal - afterWal)),
                        checkpoint_info: checkpointResult[0] ?? null,
                    };
                    actions_performed.push(`wal_checkpoint (reclaimed ${formatBytes(Math.max(0, beforeWal - afterWal))})`);
                }
                else {
                    results.wal_checkpoint = {
                        would_flush_bytes: beforeWal,
                        would_flush_human: formatBytes(beforeWal),
                    };
                    actions_performed.push('wal_checkpoint (dry run)');
                }
            }
            // ────────────────────────────────────────────────────────────
            // VACUUM: Defragment and reclaim space
            // ────────────────────────────────────────────────────────────
            if (runVacuum && conn && dbPath) {
                const beforeSize = existsSync(dbPath) ? statSync(dbPath).size : 0;
                if (!input.dry_run) {
                    // Run PRAGMA optimize before vacuum for up-to-date statistics
                    conn.pragma('optimize');
                    conn.exec('VACUUM');
                    const afterSize = existsSync(dbPath) ? statSync(dbPath).size : 0;
                    results.vacuum = {
                        before_bytes: beforeSize,
                        after_bytes: afterSize,
                        reclaimed_bytes: beforeSize - afterSize,
                        reclaimed_human: formatBytes(Math.max(0, beforeSize - afterSize)),
                    };
                    actions_performed.push(`vacuum (reclaimed ${formatBytes(Math.max(0, beforeSize - afterSize))})`);
                    // Sync updated file size to registry via updateDatabase
                    try {
                        const registry = RegistryService.getInstance();
                        const entry = registry.getDatabase(targetName);
                        if (entry) {
                            registry.syncStats(targetName, {
                                document_count: entry.document_count,
                                chunk_count: entry.chunk_count,
                                embedding_count: entry.embedding_count,
                                size_bytes: afterSize,
                            });
                        }
                    }
                    catch {
                        // Registry sync failure is non-fatal
                    }
                }
                else {
                    const sizeReport = getDbSizeReport(dbPath, conn);
                    const estimatedReclaim = sizeReport.freelist_count * sizeReport.page_size;
                    results.vacuum = {
                        current_bytes: beforeSize,
                        estimated_reclaim_bytes: estimatedReclaim,
                        estimated_reclaim_human: formatBytes(estimatedReclaim),
                    };
                    actions_performed.push('vacuum (dry run)');
                }
            }
            // ────────────────────────────────────────────────────────────
            // CLEANUP BACKUPS: Remove old pre-migration backups
            // ────────────────────────────────────────────────────────────
            if (runCleanupBackups) {
                const staleFiles = findStaleFiles(storagePath);
                const backupFiles = staleFiles.filter(f => f.type === 'pre_migration_backup');
                // Group backups by their source database
                const backupsByDb = new Map();
                for (const backup of backupFiles) {
                    const fileName = backup.path.split('/').pop() ?? '';
                    const sourceDbName = fileName.split('.db.pre-migrate-v')[0];
                    const existing = backupsByDb.get(sourceDbName) ?? [];
                    existing.push(backup);
                    backupsByDb.set(sourceDbName, existing);
                }
                let totalCleanedBytes = 0;
                let totalCleanedCount = 0;
                const cleanedDetails = [];
                const retention = input.backup_retention ?? 3;
                for (const [, backups] of backupsByDb.entries()) {
                    // Sort by version number (extract from filename), keep newest
                    const sorted = backups.sort((a, b) => {
                        const vA = parseInt(a.path.match(/pre-migrate-v(\d+)/)?.[1] ?? '0', 10);
                        const vB = parseInt(b.path.match(/pre-migrate-v(\d+)/)?.[1] ?? '0', 10);
                        return vA - vB;
                    });
                    const toDelete = sorted.length > retention
                        ? sorted.slice(0, sorted.length - retention)
                        : [];
                    for (const file of toDelete) {
                        if (!input.dry_run) {
                            try {
                                unlinkSync(file.path);
                                // Also clean WAL/SHM for backup
                                const walPath = `${file.path}-wal`;
                                const shmPath = `${file.path}-shm`;
                                if (existsSync(walPath))
                                    unlinkSync(walPath);
                                if (existsSync(shmPath))
                                    unlinkSync(shmPath);
                            }
                            catch (err) {
                                console.error(`[maintenance] Failed to delete backup ${file.path}: ${err}`);
                                continue;
                            }
                        }
                        totalCleanedBytes += file.size;
                        totalCleanedCount++;
                        cleanedDetails.push({ file: file.path, size: file.size });
                    }
                }
                results.cleanup_backups = {
                    backups_found: backupFiles.length,
                    backups_removed: totalCleanedCount,
                    retention: input.backup_retention,
                    reclaimed_bytes: totalCleanedBytes,
                    reclaimed_human: formatBytes(totalCleanedBytes),
                    details: cleanedDetails.length <= 20 ? cleanedDetails : cleanedDetails.slice(0, 20),
                };
                actions_performed.push(input.dry_run
                    ? `cleanup_backups (dry run: would remove ${totalCleanedCount} backups, ${formatBytes(totalCleanedBytes)})`
                    : `cleanup_backups (removed ${totalCleanedCount} backups, reclaimed ${formatBytes(totalCleanedBytes)})`);
            }
            // ────────────────────────────────────────────────────────────
            // CLEANUP TEMP: Remove orphaned WAL/SHM files
            // ────────────────────────────────────────────────────────────
            if (runCleanupTemp) {
                const staleFiles = findStaleFiles(storagePath);
                const orphanedFiles = staleFiles.filter(f => f.type === 'orphaned_wal_shm');
                let cleanedBytes = 0;
                let cleanedCount = 0;
                for (const file of orphanedFiles) {
                    if (!input.dry_run) {
                        try {
                            unlinkSync(file.path);
                        }
                        catch (err) {
                            console.error(`[maintenance] Failed to delete orphaned file ${file.path}: ${err}`);
                            continue;
                        }
                    }
                    cleanedBytes += file.size;
                    cleanedCount++;
                }
                results.cleanup_temp = {
                    orphaned_files_found: orphanedFiles.length,
                    orphaned_files_removed: cleanedCount,
                    reclaimed_bytes: cleanedBytes,
                    reclaimed_human: formatBytes(cleanedBytes),
                };
                actions_performed.push(input.dry_run
                    ? `cleanup_temp (dry run: would remove ${cleanedCount} orphaned files)`
                    : `cleanup_temp (removed ${cleanedCount} orphaned files, reclaimed ${formatBytes(cleanedBytes)})`);
            }
        }
        finally {
            if (shouldClose && conn) {
                conn.close();
            }
        }
        results.actions_performed = actions_performed;
        // Build next_steps
        const nextSteps = [];
        if (input.action === 'analyze') {
            nextSteps.push({ tool: 'ocr_db_maintenance', description: 'Run full maintenance with action=full' }, { tool: 'ocr_db_maintenance', description: 'Run vacuum to reclaim space' });
        }
        if (input.dry_run) {
            nextSteps.push({ tool: 'ocr_db_maintenance', description: 'Run again with dry_run=false to apply changes' });
        }
        nextSteps.push({ tool: 'ocr_health_check', description: 'Check data integrity after maintenance' });
        return formatResponse(successResult({ ...results, next_steps: nextSteps }));
    }
    catch (error) {
        return handleError(error);
    }
}
// ═══════════════════════════════════════════════════════════════════════════════
// TOOL DEFINITIONS EXPORT
// ═══════════════════════════════════════════════════════════════════════════════
export const maintenanceTools = {
    ocr_db_maintenance: {
        description: '[MAINTENANCE] Database storage maintenance and garbage collection. ' +
            'Actions: analyze (report sizes/fragmentation), vacuum (defragment & reclaim space), ' +
            'wal_checkpoint (flush WAL), cleanup_backups (prune old backups), ' +
            'cleanup_temp (remove orphaned files), full (all actions). ' +
            'Use dry_run=true to preview changes before applying.',
        inputSchema: MaintenanceInput.shape,
        handler: handleMaintenance,
    },
};
//# sourceMappingURL=maintenance.js.map