/**
 * ProvenanceVerifier - Hash integrity and chain verification
 *
 * Constitution Compliance:
 * - CP-003: Immutable Hash Verification
 * - CP-001: Complete Provenance Chain
 *
 * FAIL FAST: All errors throw immediately
 * NO MOCKS: Tests use real DatabaseService
 */
import fs from 'fs';
import { ProvenanceErrorCode } from './tracker.js';
import { ProvenanceType } from '../../models/provenance.js';
import { computeHash, hashFile, isValidHashFormat } from '../../utils/hash.js';
import { rowToDocument, rowToOCRResult, rowToChunk, rowToEmbedding, rowToProvenance, rowToImage, } from '../storage/database/converters.js';
/** Error codes for verifier operations */
export const VerifierErrorCode = {
    ...ProvenanceErrorCode,
    INTEGRITY_FAILED: 'INTEGRITY_VERIFICATION_FAILED',
    CONTENT_NOT_FOUND: 'CONTENT_NOT_FOUND',
    FILE_NOT_FOUND: 'SOURCE_FILE_NOT_FOUND',
    HASH_FORMAT_INVALID: 'HASH_FORMAT_INVALID',
};
/**
 * VerifierError - Typed error for verification operations
 * FAIL FAST: Always throw with detailed error information
 */
export class VerifierError extends Error {
    code;
    details;
    constructor(message, code, details) {
        super(message);
        this.code = code;
        this.details = details;
        this.name = 'VerifierError';
        Object.setPrototypeOf(this, VerifierError.prototype);
    }
}
/**
 * ProvenanceVerifier - Hash integrity and chain verification
 *
 * Provides verification for:
 * 1. Single provenance record content hash
 * 2. Complete provenance chain integrity
 * 3. Database-wide verification
 * 4. Source file integrity
 */
export class ProvenanceVerifier {
    db;
    tracker;
    rawDb;
    constructor(db, tracker) {
        this.db = db;
        this.tracker = tracker;
        this.rawDb = db.getConnection();
    }
    /**
     * Verify content hash for a single provenance record
     *
     * @param provenanceId - Provenance record ID to verify
     * @returns Verification result with computed and expected hashes
     * @throws VerifierError if provenance not found, content not found, or file not accessible
     */
    async verifyContentHash(provenanceId) {
        // Get provenance record - throws if not found
        const record = this.tracker.getProvenanceById(provenanceId);
        // Get content and expected hash based on type
        const { content, expectedHash, isFile } = this.getContentForVerification(record);
        // Validate expected hash format
        const formatValid = isValidHashFormat(expectedHash);
        // Compute hash of content
        let computedHash;
        if (isFile && typeof content === 'string') {
            // For files, content is the file path - hash the file
            computedHash = await hashFile(content);
        }
        else {
            // For text content, compute hash directly
            computedHash = computeHash(content);
        }
        const valid = formatValid && computedHash === expectedHash;
        return {
            valid,
            item_id: provenanceId,
            item_type: record.type,
            expected_hash: expectedHash,
            computed_hash: computedHash,
            format_valid: formatValid,
            verified_at: new Date().toISOString(),
        };
    }
    /**
     * Verify complete provenance chain from item to root
     *
     * @param provenanceId - Starting provenance record ID
     * @returns Chain verification result with all failing items
     * @throws VerifierError if chain broken or item not found
     */
    async verifyChain(provenanceId) {
        // Get chain using tracker - throws if not found
        const chain = this.tracker.getProvenanceChain(provenanceId);
        const failedItems = [];
        let hashesVerified = 0;
        let hashesFailed = 0;
        // Verify each record in the chain: current + ancestors
        const allRecords = [chain.current, ...chain.ancestors];
        for (const record of allRecords) {
            try {
                const result = await this.verifyContentHash(record.id);
                if (result.valid) {
                    hashesVerified++;
                }
                else {
                    hashesFailed++;
                    failedItems.push({
                        id: record.id,
                        expected_hash: result.expected_hash,
                        computed_hash: result.computed_hash,
                        type: record.type,
                    });
                }
            }
            catch (error) {
                // Content not found or file not accessible - count as failure
                hashesFailed++;
                failedItems.push({
                    id: record.id,
                    expected_hash: record.content_hash,
                    computed_hash: 'ERROR: ' + (error instanceof Error ? error.message : 'Unknown error'),
                    type: record.type,
                });
            }
        }
        const valid = hashesFailed === 0 && chain.isComplete;
        return {
            valid,
            chain_intact: chain.isComplete,
            hashes_verified: hashesVerified,
            hashes_failed: hashesFailed,
            failed_items: failedItems,
            verified_at: new Date().toISOString(),
            start_id: provenanceId,
            chain_depth: chain.depth,
            root_document_id: chain.root.root_document_id,
            chain_length: allRecords.length,
        };
    }
    /**
     * Verify all provenance records in database
     *
     * @returns Database verification result with counts by type
     */
    async verifyDatabase() {
        const startTime = Date.now();
        const allProvenance = this.getAllProvenance();
        // H-6: Cap failedItems to prevent unbounded memory growth
        const MAX_FAILED_ITEMS = 1000;
        const failedItems = [];
        let failedOverflow = 0;
        let hashesVerified = 0;
        let hashesFailed = 0;
        let documentsVerified = 0;
        let ocrResultsVerified = 0;
        let chunksVerified = 0;
        let embeddingsVerified = 0;
        let imagesVerified = 0;
        let vlmDescriptionsVerified = 0;
        for (const record of allProvenance) {
            try {
                const result = await this.verifyContentHash(record.id);
                // Count by type
                switch (record.type) {
                    case ProvenanceType.DOCUMENT:
                        documentsVerified++;
                        break;
                    case ProvenanceType.OCR_RESULT:
                        ocrResultsVerified++;
                        break;
                    case ProvenanceType.CHUNK:
                        chunksVerified++;
                        break;
                    case ProvenanceType.EMBEDDING:
                        embeddingsVerified++;
                        break;
                    case ProvenanceType.IMAGE:
                        imagesVerified++;
                        break;
                    case ProvenanceType.VLM_DESCRIPTION:
                        vlmDescriptionsVerified++;
                        break;
                }
                if (result.valid) {
                    hashesVerified++;
                }
                else {
                    hashesFailed++;
                    if (failedItems.length < MAX_FAILED_ITEMS) {
                        failedItems.push({
                            id: record.id,
                            expected_hash: result.expected_hash,
                            computed_hash: result.computed_hash,
                            type: record.type,
                        });
                    }
                    else {
                        failedOverflow++;
                    }
                }
            }
            catch (error) {
                hashesFailed++;
                if (failedItems.length < MAX_FAILED_ITEMS) {
                    failedItems.push({
                        id: record.id,
                        expected_hash: record.content_hash,
                        computed_hash: 'ERROR: ' + (error instanceof Error ? error.message : 'Unknown error'),
                        type: record.type,
                    });
                }
                else {
                    failedOverflow++;
                }
            }
        }
        const durationMs = Date.now() - startTime;
        // Chain integrity check: verify all parent_id references exist and depths are correct
        let chainIntact = true;
        const chainErrors = [];
        const parentedRecords = this.rawDb
            .prepare(`SELECT id, parent_id, type, chain_depth FROM provenance WHERE parent_id IS NOT NULL`)
            .all();
        for (const record of parentedRecords) {
            const parent = this.rawDb
                .prepare('SELECT id, chain_depth FROM provenance WHERE id = ?')
                .get(record.parent_id);
            if (!parent) {
                chainIntact = false;
                if (chainErrors.length < 10) {
                    chainErrors.push(`${record.id} (${record.type}): parent ${record.parent_id} not found`);
                }
            }
            else if (parent.chain_depth !== record.chain_depth - 1) {
                chainIntact = false;
                if (chainErrors.length < 10) {
                    chainErrors.push(`${record.id} (${record.type}): depth ${record.chain_depth} but parent depth ${parent.chain_depth} (expected ${record.chain_depth - 1})`);
                }
            }
        }
        return {
            valid: hashesFailed === 0 && chainIntact,
            chain_intact: chainIntact,
            chain_errors: chainErrors.length > 0 ? chainErrors : undefined,
            hashes_verified: hashesVerified,
            hashes_failed: hashesFailed,
            failed_items: failedItems,
            verified_at: new Date().toISOString(),
            database_name: this.getDatabaseName(),
            documents_verified: documentsVerified,
            ocr_results_verified: ocrResultsVerified,
            chunks_verified: chunksVerified,
            embeddings_verified: embeddingsVerified,
            images_verified: imagesVerified,
            vlm_descriptions_verified: vlmDescriptionsVerified,
            duration_ms: durationMs,
            failed_overflow: failedOverflow,
        };
    }
    /**
     * Verify source file still matches stored hash
     *
     * @param documentId - Document ID to verify
     * @returns Verification result for the file
     * @throws VerifierError if document not found or file not accessible
     */
    async verifyFileIntegrity(documentId) {
        // Get document
        const doc = this.db.getDocument(documentId);
        if (!doc) {
            throw new VerifierError(`Document not found: ${documentId}`, VerifierErrorCode.NOT_FOUND, {
                documentId,
            });
        }
        // Check file exists
        if (!fs.existsSync(doc.file_path)) {
            throw new VerifierError(`Source file not found: ${doc.file_path}`, VerifierErrorCode.FILE_NOT_FOUND, { documentId, filePath: doc.file_path });
        }
        // Hash the file
        const computedHash = await hashFile(doc.file_path);
        const expectedHash = doc.file_hash;
        const formatValid = isValidHashFormat(expectedHash);
        const valid = formatValid && computedHash === expectedHash;
        return {
            valid,
            item_id: documentId,
            item_type: ProvenanceType.DOCUMENT,
            expected_hash: expectedHash,
            computed_hash: computedHash,
            format_valid: formatValid,
            verified_at: new Date().toISOString(),
        };
    }
    // ════════════════════════════════════════════════════════════════
    // PRIVATE HELPER METHODS
    // ════════════════════════════════════════════════════════════════
    /**
     * Get content and expected hash for a provenance record
     * Returns { content, expectedHash, isFile } or throws if content not found
     *
     * CRITICAL MAPPING:
     * - DOCUMENT: file_path → file_hash (via hashFile)
     * - OCR_RESULT: extracted_text → content_hash
     * - CHUNK: text → text_hash
     * - EMBEDDING: original_text → content_hash
     */
    getContentForVerification(record) {
        switch (record.type) {
            case ProvenanceType.DOCUMENT: {
                // For DOCUMENT, we verify the file on disk using file_hash
                // Query document by provenance_id since record.id IS the document's provenance_id
                const doc = this.getDocumentByProvenanceId(record.id);
                if (!doc) {
                    throw new VerifierError(`Document not found for provenance ${record.id}`, VerifierErrorCode.CONTENT_NOT_FOUND, { provenanceId: record.id, type: record.type });
                }
                // Check file exists
                if (!fs.existsSync(doc.file_path)) {
                    throw new VerifierError(`Source file not found: ${doc.file_path}`, VerifierErrorCode.FILE_NOT_FOUND, { provenanceId: record.id, filePath: doc.file_path });
                }
                // Return file path - caller will hash the file
                return { content: doc.file_path, expectedHash: doc.file_hash, isFile: true };
            }
            case ProvenanceType.OCR_RESULT: {
                const ocr = this.getOCRResultByProvenanceId(record.id);
                if (!ocr) {
                    throw new VerifierError(`OCR result not found for provenance ${record.id}`, VerifierErrorCode.CONTENT_NOT_FOUND, { provenanceId: record.id, type: record.type });
                }
                return { content: ocr.extracted_text, expectedHash: ocr.content_hash, isFile: false };
            }
            case ProvenanceType.CHUNK: {
                const chunk = this.getChunkByProvenanceId(record.id);
                if (!chunk) {
                    throw new VerifierError(`Chunk not found for provenance ${record.id}`, VerifierErrorCode.CONTENT_NOT_FOUND, { provenanceId: record.id, type: record.type });
                }
                // CRITICAL: CHUNK uses text_hash, not content_hash
                return { content: chunk.text, expectedHash: chunk.text_hash, isFile: false };
            }
            case ProvenanceType.EMBEDDING: {
                const emb = this.getEmbeddingByProvenanceId(record.id);
                if (!emb) {
                    throw new VerifierError(`Embedding not found for provenance ${record.id}`, VerifierErrorCode.CONTENT_NOT_FOUND, { provenanceId: record.id, type: record.type });
                }
                return { content: emb.original_text, expectedHash: emb.content_hash, isFile: false };
            }
            case ProvenanceType.IMAGE: {
                // IMAGE verification: hash the extracted image file
                const image = this.getImageByProvenanceId(record.id);
                if (!image) {
                    throw new VerifierError(`Image not found for provenance ${record.id}`, VerifierErrorCode.CONTENT_NOT_FOUND, { provenanceId: record.id, type: record.type });
                }
                // Check extracted file exists
                if (!image.extracted_path || !fs.existsSync(image.extracted_path)) {
                    throw new VerifierError(`Extracted image file not found: ${image.extracted_path}`, VerifierErrorCode.FILE_NOT_FOUND, { provenanceId: record.id, imagePath: image.extracted_path });
                }
                // Return file path - caller will hash the file
                return { content: image.extracted_path, expectedHash: record.content_hash, isFile: true };
            }
            case ProvenanceType.VLM_DESCRIPTION: {
                // VLM_DESCRIPTION verification: hash the VLM description text
                // The description is stored in images.vlm_description, but we need to find it
                // via the provenance chain - the parent should be an IMAGE
                // Get the parent IMAGE to find the VLM description
                if (!record.parent_id) {
                    throw new VerifierError(`VLM_DESCRIPTION has no parent_id: ${record.id}`, VerifierErrorCode.CHAIN_BROKEN, { provenanceId: record.id });
                }
                const image = this.getImageByProvenanceId(record.parent_id);
                if (!image) {
                    throw new VerifierError(`Parent image not found for VLM_DESCRIPTION ${record.id}`, VerifierErrorCode.CONTENT_NOT_FOUND, { provenanceId: record.id, parentId: record.parent_id });
                }
                if (!image.vlm_description) {
                    throw new VerifierError(`VLM description is empty for image ${image.id}`, VerifierErrorCode.CONTENT_NOT_FOUND, { provenanceId: record.id, imageId: image.id });
                }
                return { content: image.vlm_description, expectedHash: record.content_hash, isFile: false };
            }
            case ProvenanceType.COMPARISON: {
                const comparison = this.rawDb
                    .prepare('SELECT text_diff_json, structural_diff_json FROM comparisons WHERE provenance_id = ?')
                    .get(record.id);
                if (!comparison) {
                    throw new VerifierError(`Comparison not found for provenance ${record.id}`, VerifierErrorCode.CONTENT_NOT_FOUND, { provenanceId: record.id });
                }
                const parseDiffField = (json, field) => {
                    try {
                        return JSON.parse(json);
                    }
                    catch (error) {
                        console.error(`[ProvenanceVerifier] Failed to parse ${field} JSON for provenance ${record.id}:`, error instanceof Error ? error.message : String(error));
                        throw new VerifierError(`Corrupt ${field} in comparison for provenance ${record.id}`, VerifierErrorCode.CONTENT_NOT_FOUND, { provenanceId: record.id, field });
                    }
                };
                const diffContent = JSON.stringify({
                    text_diff: parseDiffField(comparison.text_diff_json, 'text_diff_json'),
                    structural_diff: parseDiffField(comparison.structural_diff_json, 'structural_diff_json'),
                });
                return { content: diffContent, expectedHash: record.content_hash, isFile: false };
            }
            case ProvenanceType.EXTRACTION: {
                // EXTRACTION: content_hash = computeHash(JSON.stringify(extractionJson))
                // Re-derive: Load extraction record and hash its extraction_json field
                const extraction = this.rawDb
                    .prepare('SELECT extraction_json FROM extractions WHERE provenance_id = ?')
                    .get(record.id);
                if (!extraction) {
                    throw new VerifierError(`Extraction not found for provenance ${record.id}`, VerifierErrorCode.CONTENT_NOT_FOUND, { provenanceId: record.id, type: record.type });
                }
                // The hash was computed over JSON.stringify(response.extractionJson) which was
                // stored as extraction_json. Re-hash the stored string directly.
                return {
                    content: extraction.extraction_json,
                    expectedHash: record.content_hash,
                    isFile: false,
                };
            }
            case ProvenanceType.FORM_FILL: {
                // FORM_FILL: content_hash = computeHash(JSON.stringify({ fields_filled, fields_not_found }))
                // Re-derive: Load from form_fills table, reconstruct the same object
                const formFill = this.rawDb
                    .prepare('SELECT fields_filled, fields_not_found FROM form_fills WHERE provenance_id = ?')
                    .get(record.id);
                if (!formFill) {
                    throw new VerifierError(`Form fill not found for provenance ${record.id}`, VerifierErrorCode.CONTENT_NOT_FOUND, { provenanceId: record.id, type: record.type });
                }
                // Reconstruct the exact object that was hashed: { fields_filled, fields_not_found }
                // The original code hashes: computeHash(JSON.stringify({ fields_filled: result.fieldsFilled, fields_not_found: result.fieldsNotFound }))
                // form_fills stores these as JSON strings, so parse them back to arrays
                let parsedFieldsFilled;
                let parsedFieldsNotFound;
                try {
                    parsedFieldsFilled = JSON.parse(formFill.fields_filled);
                }
                catch (parseError) {
                    throw new VerifierError(`Corrupt JSON in fields_filled for provenance ${record.id}: ${parseError instanceof Error ? parseError.message : String(parseError)}. Data preview: ${formFill.fields_filled.slice(0, 200)}`, VerifierErrorCode.INTEGRITY_FAILED, { provenanceId: record.id, field: 'fields_filled' });
                }
                try {
                    parsedFieldsNotFound = JSON.parse(formFill.fields_not_found);
                }
                catch (parseError) {
                    throw new VerifierError(`Corrupt JSON in fields_not_found for provenance ${record.id}: ${parseError instanceof Error ? parseError.message : String(parseError)}. Data preview: ${formFill.fields_not_found.slice(0, 200)}`, VerifierErrorCode.INTEGRITY_FAILED, { provenanceId: record.id, field: 'fields_not_found' });
                }
                const formFillContent = JSON.stringify({
                    fields_filled: parsedFieldsFilled,
                    fields_not_found: parsedFieldsNotFound,
                });
                return { content: formFillContent, expectedHash: record.content_hash, isFile: false };
            }
            case ProvenanceType.CLUSTERING: {
                // CLUSTERING: content_hash = computeHash(JSON.stringify(centroid) + ':' + runId)
                // Re-derive: Load cluster record and reconstruct the same input
                const cluster = this.rawDb
                    .prepare('SELECT centroid_json, run_id FROM clusters WHERE provenance_id = ?')
                    .get(record.id);
                if (!cluster) {
                    throw new VerifierError(`Cluster not found for provenance ${record.id}`, VerifierErrorCode.CONTENT_NOT_FOUND, { provenanceId: record.id, type: record.type });
                }
                // The original hash: computeHash(JSON.stringify(centroid) + ':' + runId)
                // centroid_json is already JSON.stringify(centroid), so use it directly
                const clusterContent = cluster.centroid_json + ':' + cluster.run_id;
                return { content: clusterContent, expectedHash: record.content_hash, isFile: false };
            }
            default: {
                const unknownType = record.type;
                throw new VerifierError(`Unknown provenance type: ${unknownType}`, VerifierErrorCode.INVALID_TYPE, { type: unknownType });
            }
        }
    }
    // ════════════════════════════════════════════════════════════════
    // RAW SQL HELPERS (methods not in DatabaseService)
    // ════════════════════════════════════════════════════════════════
    /**
     * Get document by its provenance_id
     */
    getDocumentByProvenanceId(provenanceId) {
        const row = this.rawDb
            .prepare('SELECT * FROM documents WHERE provenance_id = ?')
            .get(provenanceId);
        return row ? rowToDocument(row) : null;
    }
    /**
     * Get OCR result by its provenance_id
     */
    getOCRResultByProvenanceId(provenanceId) {
        const row = this.rawDb
            .prepare('SELECT * FROM ocr_results WHERE provenance_id = ?')
            .get(provenanceId);
        return row ? rowToOCRResult(row) : null;
    }
    /**
     * Get chunk by its provenance_id
     */
    getChunkByProvenanceId(provenanceId) {
        const row = this.rawDb
            .prepare('SELECT * FROM chunks WHERE provenance_id = ?')
            .get(provenanceId);
        return row ? rowToChunk(row) : null;
    }
    /**
     * Get embedding by its provenance_id (without vector)
     */
    getEmbeddingByProvenanceId(provenanceId) {
        const row = this.rawDb
            .prepare('SELECT * FROM embeddings WHERE provenance_id = ?')
            .get(provenanceId);
        return row ? rowToEmbedding(row) : null;
    }
    /**
     * Get image by its provenance_id
     */
    getImageByProvenanceId(provenanceId) {
        const row = this.rawDb
            .prepare('SELECT * FROM images WHERE provenance_id = ?')
            .get(provenanceId);
        return row ? rowToImage(row) : null;
    }
    /**
     * Get all provenance records ordered by chain_depth
     */
    getAllProvenance() {
        // H-6: Use iterate() to avoid double-allocation (.all() + .map())
        const records = [];
        const stmt = this.rawDb.prepare('SELECT * FROM provenance ORDER BY chain_depth ASC');
        for (const row of stmt.iterate()) {
            records.push(rowToProvenance(row));
        }
        return records;
    }
    /**
     * Get database name from metadata
     */
    getDatabaseName() {
        try {
            const row = this.rawDb
                .prepare('SELECT database_name FROM database_metadata LIMIT 1')
                .get();
            return row?.database_name ?? 'unknown';
        }
        catch (error) {
            console.error(`[Verifier] Failed to get database name from metadata: ${String(error)}`);
            return 'unknown';
        }
    }
}
//# sourceMappingURL=verifier.js.map