/**
 * Database Management MCP Tools (Phase 2)
 *
 * Advanced database lifecycle tools: search, recent, tag, archive,
 * unarchive, rename, and summary.
 *
 * CRITICAL: NEVER use console.log() - stdout is reserved for JSON-RPC protocol.
 * Use console.error() for all logging.
 *
 * @module tools/database-management
 */
import { z } from 'zod';
import { RegistryService } from '../services/storage/registry/index.js';
import { requireDatabase, selectDatabase, clearDatabase, getDefaultStoragePath, getCurrentDatabaseName } from '../server/state.js';
import { successResult } from '../server/types.js';
import { validateInput } from '../utils/validation.js';
import { formatResponse, handleError } from './shared.js';
import { getDatabasePath, validateName } from '../services/storage/database/helpers.js';
import { DatabaseError, DatabaseErrorCode } from '../services/storage/database/types.js';
import { renameSync, existsSync } from 'fs';
import Database from 'better-sqlite3';
// ═══════════════════════════════════════════════════════════════════════════════
// VALIDATION SCHEMAS (used inside handlers with validateInput)
// ═══════════════════════════════════════════════════════════════════════════════
const DbSearchInput = z.object({
    query: z.string().max(200).default(''),
    tags: z.array(z.string()).optional(),
    status: z.enum(['active', 'archived', 'all']).default('active'),
    min_documents: z.number().int().min(0).optional(),
    created_after: z.string().optional(),
    created_before: z.string().optional(),
    size_min_mb: z.number().min(0).optional(),
    size_max_mb: z.number().min(0).optional(),
    metadata: z.record(z.string(), z.string()).optional(),
    sort_by: z.enum(['name', 'last_accessed', 'created', 'size', 'documents']).default('last_accessed'),
    limit: z.number().int().min(1).max(100).default(20),
});
const DbRecentInput = z.object({
    limit: z.number().int().min(1).max(50).default(10),
});
const DbTagInput = z.object({
    database_name: z.string().min(1),
    action: z.enum(['add', 'remove', 'set', 'list']),
    tags: z.array(z.string().min(1).max(50)).max(20).optional(),
    metadata: z.record(z.string(), z.string().max(200)).optional(),
});
const DbArchiveInput = z.object({
    database_name: z.string().min(1),
    reason: z.string().max(500).optional(),
});
const DbUnarchiveInput = z.object({
    database_name: z.string().min(1),
});
const DbRenameInput = z.object({
    old_name: z.string().min(1),
    new_name: z.string().min(1).max(64).regex(/^[a-zA-Z0-9_-]+$/),
    update_description: z.string().max(500).optional(),
});
const DbSummaryInput = z.object({
    database_name: z.string().optional(),
});
const DbWorkspaceInput = z.object({
    action: z.enum(['create', 'list', 'get', 'delete', 'add_database', 'remove_database']).describe('Workspace action'),
    name: z.string().min(1).max(64).optional().describe('Workspace name (required for all except list)'),
    description: z.string().max(500).optional().describe('Workspace description (for create)'),
    database_name: z.string().min(1).optional().describe('Database name (for add_database/remove_database)'),
});
// ═══════════════════════════════════════════════════════════════════════════════
// HANDLER: ocr_db_search
// ═══════════════════════════════════════════════════════════════════════════════
/**
 * Handle ocr_db_search - Find databases by name, description, or tags
 */
async function handleDbSearch(params) {
    try {
        const input = validateInput(DbSearchInput, params);
        const registry = RegistryService.getInstance();
        const filters = {
            status: input.status,
            tags: input.tags,
            min_documents: input.min_documents,
            created_after: input.created_after,
            created_before: input.created_before,
            size_min_mb: input.size_min_mb,
            size_max_mb: input.size_max_mb,
            metadata: input.metadata,
        };
        const results = registry.search(input.query ?? '', filters);
        // Sort results by input.sort_by (DESC for last_accessed/size/documents, ASC for name/created)
        results.sort((a, b) => {
            switch (input.sort_by) {
                case 'name':
                    return a.name.localeCompare(b.name);
                case 'last_accessed':
                    return (b.last_accessed_at ?? '').localeCompare(a.last_accessed_at ?? '');
                case 'created':
                    return a.created_at.localeCompare(b.created_at);
                case 'size':
                    return b.size_bytes - a.size_bytes;
                case 'documents':
                    return b.document_count - a.document_count;
                default:
                    return 0;
            }
        });
        // Slice to limit
        const sliced = results.slice(0, input.limit);
        return formatResponse(successResult({
            databases: sliced.map(r => ({
                name: r.name,
                description: r.description,
                status: r.status,
                tags: r.tags,
                metadata: r.metadata,
                size_bytes: r.size_bytes,
                document_count: r.document_count,
                last_accessed_at: r.last_accessed_at,
                created_at: r.created_at,
                match_score: r.match_score,
                match_reason: r.match_reason,
            })),
            total_matches: results.length,
            returned: sliced.length,
            next_steps: [
                { tool: 'ocr_db_select', description: 'Select a database to work with' },
                { tool: 'ocr_db_tag', description: 'Add tags to organize databases' },
            ],
        }));
    }
    catch (error) {
        return handleError(error);
    }
}
// ═══════════════════════════════════════════════════════════════════════════════
// HANDLER: ocr_db_recent
// ═══════════════════════════════════════════════════════════════════════════════
/**
 * Handle ocr_db_recent - Show recently accessed databases
 */
async function handleDbRecent(params) {
    try {
        const input = validateInput(DbRecentInput, params);
        const registry = RegistryService.getInstance();
        const recent = registry.getRecent(input.limit ?? 10);
        return formatResponse(successResult({
            databases: recent.map(r => ({
                name: r.name,
                description: r.description,
                last_accessed_at: r.last_accessed_at,
                access_count: r.access_count,
                last_action: r.last_action,
                tags: r.tags,
                document_count: r.document_count,
                size_bytes: r.size_bytes,
            })),
            total: recent.length,
            next_steps: [
                { tool: 'ocr_db_select', description: 'Select your most recent database' },
            ],
        }));
    }
    catch (error) {
        return handleError(error);
    }
}
// ═══════════════════════════════════════════════════════════════════════════════
// HANDLER: ocr_db_tag
// ═══════════════════════════════════════════════════════════════════════════════
/**
 * Handle ocr_db_tag - Add, remove, set, or list tags and metadata on a database
 */
async function handleDbTag(params) {
    try {
        const input = validateInput(DbTagInput, params);
        const registry = RegistryService.getInstance();
        // Validate tags provided for mutation actions
        if ((input.action === 'add' || input.action === 'remove' || input.action === 'set') && (!input.tags || input.tags.length === 0)) {
            throw new DatabaseError(`Tags are required for action '${input.action}'`, DatabaseErrorCode.REGISTRY_ERROR);
        }
        switch (input.action) {
            case 'add':
                registry.addTags(input.database_name, input.tags);
                break;
            case 'remove':
                registry.removeTags(input.database_name, input.tags);
                break;
            case 'set':
                registry.setTags(input.database_name, input.tags);
                if (input.metadata) {
                    registry.setMetadata(input.database_name, input.metadata);
                }
                break;
            case 'list': {
                // Verify database exists before listing
                const exists = registry.getDatabase(input.database_name);
                if (!exists) {
                    throw new DatabaseError(`Database '${input.database_name}' not found in registry`, DatabaseErrorCode.DATABASE_NOT_FOUND);
                }
                break;
            }
        }
        // Fetch current state after mutation
        const entry = registry.getDatabase(input.database_name);
        const metadata = registry.getMetadata(input.database_name);
        return formatResponse(successResult({
            database_name: input.database_name,
            action: input.action,
            tags: entry?.tags ?? [],
            metadata,
            next_steps: [
                { tool: 'ocr_db_search', description: 'Search databases by tags' },
                { tool: 'ocr_db_list', description: 'List all databases' },
            ],
        }));
    }
    catch (error) {
        return handleError(error);
    }
}
// ═══════════════════════════════════════════════════════════════════════════════
// HANDLER: ocr_db_archive
// ═══════════════════════════════════════════════════════════════════════════════
/**
 * Handle ocr_db_archive - Archive a database to hide from default list/search
 */
async function handleDbArchive(params) {
    try {
        const input = validateInput(DbArchiveInput, params);
        const registry = RegistryService.getInstance();
        registry.archive(input.database_name, input.reason);
        try {
            registry.recordAccess(input.database_name, 'archive', `Archived database ${input.database_name}`);
        }
        catch { /* activity logging must never break the main operation */ }
        return formatResponse(successResult({
            database_name: input.database_name,
            archived: true,
            reason: input.reason ?? null,
            next_steps: [
                { tool: 'ocr_db_unarchive', description: 'Restore this database if needed' },
                { tool: 'ocr_db_list', description: 'List active databases' },
            ],
        }));
    }
    catch (error) {
        return handleError(error);
    }
}
// ═══════════════════════════════════════════════════════════════════════════════
// HANDLER: ocr_db_unarchive
// ═══════════════════════════════════════════════════════════════════════════════
/**
 * Handle ocr_db_unarchive - Restore an archived database to active status
 */
async function handleDbUnarchive(params) {
    try {
        const input = validateInput(DbUnarchiveInput, params);
        const registry = RegistryService.getInstance();
        registry.unarchive(input.database_name);
        return formatResponse(successResult({
            database_name: input.database_name,
            unarchived: true,
            next_steps: [
                { tool: 'ocr_db_select', description: 'Select the restored database' },
                { tool: 'ocr_db_list', description: 'List active databases' },
            ],
        }));
    }
    catch (error) {
        return handleError(error);
    }
}
// ═══════════════════════════════════════════════════════════════════════════════
// HANDLER: ocr_db_rename
// ═══════════════════════════════════════════════════════════════════════════════
/**
 * Handle ocr_db_rename - Rename a database (filesystem + registry + internal metadata)
 */
async function handleDbRename(params) {
    try {
        const input = validateInput(DbRenameInput, params);
        const registry = RegistryService.getInstance();
        // 1. Validate new name format
        validateName(input.new_name);
        // 2. Check new_name doesn't already exist
        if (registry.getDatabase(input.new_name)) {
            throw new DatabaseError(`Database '${input.new_name}' already exists`, DatabaseErrorCode.DATABASE_ALREADY_EXISTS);
        }
        // 3. Check old entry exists
        const entry = registry.getDatabase(input.old_name);
        if (!entry) {
            throw new DatabaseError(`Database '${input.old_name}' not found`, DatabaseErrorCode.DATABASE_NOT_FOUND);
        }
        // 4. Compute paths
        const storagePath = getDefaultStoragePath();
        const oldPath = getDatabasePath(input.old_name, storagePath);
        const newPath = getDatabasePath(input.new_name, storagePath);
        // 5. Close if currently selected (must close before filesystem rename)
        const wasSelected = getCurrentDatabaseName() === input.old_name;
        if (wasSelected) {
            clearDatabase(/* forceClose */ true);
        }
        // 6. Rename filesystem files (before clearing state so we can recover on failure)
        try {
            renameSync(oldPath, newPath);
            for (const suffix of ['-wal', '-shm']) {
                const oldSuffix = oldPath + suffix;
                const newSuffix = newPath + suffix;
                if (existsSync(oldSuffix)) {
                    renameSync(oldSuffix, newSuffix);
                }
            }
        }
        catch (fsError) {
            // Filesystem rename failed -- re-select the original database to restore state
            if (wasSelected) {
                selectDatabase(input.old_name);
            }
            throw fsError;
        }
        // 8. Update registry name (CASCADE propagates to tags, metadata, workspace members)
        registry.rename(input.old_name, input.new_name);
        // 9. Update file_path (and description if provided) in a single call
        const registryUpdates = { file_path: newPath };
        if (input.update_description) {
            registryUpdates.description = input.update_description;
        }
        registry.updateDatabase(input.new_name, registryUpdates);
        // 10. Update internal database_metadata table
        const tempDb = new Database(newPath);
        try {
            tempDb.prepare('UPDATE database_metadata SET database_name = ? WHERE id = 1').run(input.new_name);
        }
        finally {
            tempDb.close();
        }
        // 11. If was selected, re-select
        if (wasSelected) {
            selectDatabase(input.new_name);
        }
        try {
            registry.recordAccess(input.new_name, 'rename', `Renamed ${input.old_name} → ${input.new_name}`);
        }
        catch { /* activity logging must never break the main operation */ }
        // Fetch updated tags
        const updated = registry.getDatabase(input.new_name);
        return formatResponse(successResult({
            old_name: input.old_name,
            new_name: input.new_name,
            new_path: newPath,
            was_selected: wasSelected,
            tags: updated?.tags ?? [],
            next_steps: [
                { tool: 'ocr_db_select', description: 'Select the renamed database' },
            ],
        }));
    }
    catch (error) {
        return handleError(error);
    }
}
// ═══════════════════════════════════════════════════════════════════════════════
// HANDLER: ocr_db_summary
// ═══════════════════════════════════════════════════════════════════════════════
/**
 * Handle ocr_db_summary - AI-readable profile of a database
 */
async function handleDbSummary(params) {
    try {
        const input = validateInput(DbSummaryInput, params);
        const registry = RegistryService.getInstance();
        const targetName = input.database_name ?? getCurrentDatabaseName();
        if (!targetName) {
            throw new DatabaseError('No database specified and no database selected', DatabaseErrorCode.DATABASE_NOT_FOUND);
        }
        const entry = registry.getDatabase(targetName);
        if (!entry) {
            throw new DatabaseError(`Database '${targetName}' not found in registry`, DatabaseErrorCode.DATABASE_NOT_FOUND);
        }
        // Open connection: use current if selected, otherwise open readonly
        let conn;
        let shouldClose = false;
        if (getCurrentDatabaseName() === targetName) {
            conn = requireDatabase().db.getConnection();
        }
        else {
            const dbPath = getDatabasePath(targetName, getDefaultStoragePath());
            conn = new Database(dbPath, { readonly: true });
            shouldClose = true;
        }
        try {
            // Query stats
            const fileTypes = conn
                .prepare('SELECT file_type, COUNT(*) as count FROM documents GROUP BY file_type')
                .all();
            const pageCount = conn
                .prepare('SELECT SUM(page_count) as total FROM documents')
                .get();
            const dateRange = conn
                .prepare('SELECT MIN(created_at) as earliest, MAX(created_at) as latest FROM documents')
                .get();
            const qualityStats = conn
                .prepare('SELECT AVG(parse_quality_score) as avg_quality FROM ocr_results WHERE parse_quality_score IS NOT NULL')
                .get();
            const chunkCount = conn
                .prepare('SELECT COUNT(*) as c FROM chunks')
                .get();
            const embeddingCount = conn
                .prepare('SELECT COUNT(*) as c FROM embeddings')
                .get();
            const imageCount = conn
                .prepare('SELECT COUNT(*) as c FROM images')
                .get();
            const vlmCompleteCount = conn
                .prepare("SELECT COUNT(*) as c FROM images WHERE vlm_status = 'complete'")
                .get();
            const profile = {
                name: targetName,
                description: entry.description,
                status: entry.status,
                file_types: fileTypes,
                total_pages: pageCount.total ?? 0,
                document_date_range: dateRange,
                avg_ocr_quality: qualityStats.avg_quality,
                chunk_count: chunkCount.c,
                embedding_count: embeddingCount.c,
                image_count: imageCount.c,
                vlm_complete_count: vlmCompleteCount.c,
                embedding_coverage: chunkCount.c > 0
                    ? Math.round((embeddingCount.c / chunkCount.c) * 100)
                    : 0,
                vlm_coverage: imageCount.c > 0
                    ? Math.round((vlmCompleteCount.c / imageCount.c) * 100)
                    : 0,
                size_bytes: entry.size_bytes,
                document_count: entry.document_count,
                tags: entry.tags,
                metadata: entry.metadata,
                workspaces: registry.getWorkspacesForDatabase(targetName),
                created_at: entry.created_at,
                last_accessed_at: entry.last_accessed_at,
                access_count: entry.access_count,
            };
            // Cache profile in registry
            registry.updateDatabase(targetName, { profile_json: JSON.stringify(profile) });
            return formatResponse(successResult({
                ...profile,
                next_steps: [
                    { tool: 'ocr_db_select', description: 'Select this database' },
                    { tool: 'ocr_search', description: 'Search within this database' },
                ],
            }));
        }
        finally {
            if (shouldClose) {
                conn.close();
            }
        }
    }
    catch (error) {
        return handleError(error);
    }
}
// ═══════════════════════════════════════════════════════════════════════════════
// HANDLER: ocr_db_workspace
// ═══════════════════════════════════════════════════════════════════════════════
async function handleDbWorkspace(params) {
    try {
        const input = validateInput(DbWorkspaceInput, params);
        const registry = RegistryService.getInstance();
        // Validate required params based on action
        if (input.action !== 'list' && !input.name) {
            throw new DatabaseError(`'name' is required for action '${input.action}'`, DatabaseErrorCode.REGISTRY_ERROR);
        }
        if ((input.action === 'add_database' || input.action === 'remove_database') && !input.database_name) {
            throw new DatabaseError(`'database_name' is required for action '${input.action}'`, DatabaseErrorCode.REGISTRY_ERROR);
        }
        switch (input.action) {
            case 'create': {
                registry.createWorkspace(input.name, input.description);
                return formatResponse(successResult({
                    workspace: input.name,
                    created: true,
                    description: input.description ?? null,
                    next_steps: [
                        { tool: 'ocr_db_workspace', description: 'Add databases with add_database action' },
                    ],
                }));
            }
            case 'list': {
                const workspaces = registry.listWorkspaces();
                const result = workspaces.map(ws => {
                    const members = registry.getWorkspaceMembers(ws.name);
                    return { ...ws, databases: members ?? [] };
                });
                return formatResponse(successResult({
                    workspaces: result,
                    total: result.length,
                    next_steps: [
                        { tool: 'ocr_db_workspace', description: 'Get workspace details with get action' },
                        { tool: 'ocr_search_cross_db', description: 'Search within a workspace' },
                    ],
                }));
            }
            case 'get': {
                const ws = registry.getWorkspace(input.name);
                if (!ws) {
                    throw new DatabaseError(`Workspace "${input.name}" not found`, DatabaseErrorCode.WORKSPACE_NOT_FOUND);
                }
                const members = registry.getWorkspaceMembers(input.name);
                return formatResponse(successResult({
                    ...ws,
                    databases: members ?? [],
                    database_count: (members ?? []).length,
                    next_steps: [
                        { tool: 'ocr_db_workspace', description: 'Add/remove databases from this workspace' },
                        { tool: 'ocr_search_cross_db', description: `Search within workspace "${input.name}"` },
                    ],
                }));
            }
            case 'delete': {
                registry.deleteWorkspace(input.name);
                return formatResponse(successResult({
                    workspace: input.name,
                    deleted: true,
                    next_steps: [
                        { tool: 'ocr_db_workspace', description: 'List remaining workspaces' },
                    ],
                }));
            }
            case 'add_database': {
                registry.addToWorkspace(input.name, input.database_name);
                const members = registry.getWorkspaceMembers(input.name);
                return formatResponse(successResult({
                    workspace: input.name,
                    database_added: input.database_name,
                    databases: members ?? [],
                    next_steps: [
                        { tool: 'ocr_db_workspace', description: 'Add more databases or get workspace details' },
                        { tool: 'ocr_search_cross_db', description: `Search within workspace "${input.name}"` },
                    ],
                }));
            }
            case 'remove_database': {
                registry.removeFromWorkspace(input.name, input.database_name);
                const members = registry.getWorkspaceMembers(input.name);
                return formatResponse(successResult({
                    workspace: input.name,
                    database_removed: input.database_name,
                    databases: members ?? [],
                    next_steps: [
                        { tool: 'ocr_db_workspace', description: 'Manage workspace members' },
                    ],
                }));
            }
            default: {
                const _exhaustive = input.action;
                throw new DatabaseError(`Unknown workspace action: ${_exhaustive}`, DatabaseErrorCode.REGISTRY_ERROR);
            }
        }
    }
    catch (error) {
        return handleError(error);
    }
}
// ═══════════════════════════════════════════════════════════════════════════════
// TOOL DEFINITIONS FOR MCP REGISTRATION
// ═══════════════════════════════════════════════════════════════════════════════
/**
 * Database management tools collection for MCP server registration
 */
export const databaseManagementTools = {
    ocr_db_search: {
        description: '[SEARCH] Find databases by name, description, or tags. Supports FTS5 full-text search, tag/status/date/size filters. Default: active databases only.',
        inputSchema: {
            query: z.string().max(200).default('').describe('Search query for FTS5 full-text search'),
            tags: z.array(z.string()).optional().describe('Filter by tags (match any)'),
            status: z.enum(['active', 'archived', 'all']).default('active').describe('Filter by status'),
            min_documents: z.number().int().min(0).optional().describe('Minimum document count'),
            created_after: z.string().optional().describe('Filter databases created after this ISO date'),
            created_before: z.string().optional().describe('Filter databases created before this ISO date'),
            size_min_mb: z.number().min(0).optional().describe('Minimum database size in MB'),
            size_max_mb: z.number().min(0).optional().describe('Maximum database size in MB'),
            metadata: z.record(z.string(), z.string()).optional().describe('Filter by metadata key-value pairs'),
            sort_by: z.enum(['name', 'last_accessed', 'created', 'size', 'documents']).default('last_accessed').describe('Sort field'),
            limit: z.number().int().min(1).max(100).default(20).describe('Maximum results to return'),
        },
        handler: handleDbSearch,
    },
    ocr_db_recent: {
        description: '[ESSENTIAL] Show recently accessed databases. Returns last N databases selected, sorted by most recent first.',
        inputSchema: {
            limit: z.number().int().min(1).max(50).default(10).describe('Number of recent databases to return'),
        },
        handler: handleDbRecent,
    },
    ocr_db_tag: {
        description: '[MANAGE] Add, remove, set, or list tags and metadata on a database.',
        inputSchema: {
            database_name: z.string().min(1).describe('Name of the database'),
            action: z.enum(['add', 'remove', 'set', 'list']).describe('Tag operation to perform'),
            tags: z.array(z.string().min(1).max(50)).max(20).optional().describe('Tags to add/remove/set'),
            metadata: z.record(z.string(), z.string().max(200)).optional().describe('Key-value metadata to set (only with set action)'),
        },
        handler: handleDbTag,
    },
    ocr_db_archive: {
        description: '[MANAGE] Archive a database to hide from default list/search. Does NOT delete data.',
        inputSchema: {
            database_name: z.string().min(1).describe('Name of the database to archive'),
            reason: z.string().max(500).optional().describe('Optional reason for archiving'),
        },
        handler: handleDbArchive,
    },
    ocr_db_unarchive: {
        description: '[MANAGE] Restore an archived database to active status.',
        inputSchema: {
            database_name: z.string().min(1).describe('Name of the database to unarchive'),
        },
        handler: handleDbUnarchive,
    },
    ocr_db_rename: {
        description: '[MANAGE] Rename a database. Updates filename, registry, and internal metadata.',
        inputSchema: {
            old_name: z.string().min(1).describe('Current database name'),
            new_name: z.string().min(1).max(64).regex(/^[a-zA-Z0-9_-]+$/).describe('New database name (alphanumeric, underscore, hyphen only)'),
            update_description: z.string().max(500).optional().describe('Optional new description'),
        },
        handler: handleDbRename,
    },
    ocr_db_summary: {
        description: '[STATUS] AI-readable profile of a database. Returns document types, page counts, quality scores, embedding coverage, tags, and metadata.',
        inputSchema: {
            database_name: z.string().optional().describe('Database name (uses current if not specified)'),
        },
        handler: handleDbSummary,
    },
    ocr_db_workspace: {
        description: '[MANAGE] Create, list, and manage database workspaces (groups). Workspaces let you organize related databases and search within them using ocr_search_cross_db.',
        inputSchema: {
            action: z.enum(['create', 'list', 'get', 'delete', 'add_database', 'remove_database']).describe('Workspace action'),
            name: z.string().min(1).max(64).optional().describe('Workspace name (required for all except list)'),
            description: z.string().max(500).optional().describe('Workspace description (for create)'),
            database_name: z.string().min(1).optional().describe('Database name (for add_database/remove_database)'),
        },
        handler: handleDbWorkspace,
    },
};
//# sourceMappingURL=database-management.js.map