import { migrationHistoryService } from '../../../infrastructure/database/migration-history-service.js';
import { migrationMonitorService } from '../../../infrastructure/database/migration-monitor-service.js';
import { vectorCompatibilityService } from './vector-compatibility-service.js';
const DEFAULT_BATCH_SIZE = 500;
const DEFAULT_CREATED_BY = 'embedding_migration_service';
/**
 * 임베딩 마이그레이션 서비스
 * - 기존 임베딩 벡터를 목표 제공자의 차원에 맞게 재투영
 * - 마이그레이션 플랜/실행/진행 상태 관리
 */
export class EmbeddingMigrationService {
    createSnapshot(progress) {
        return {
            ...progress,
            stepHistory: progress.stepHistory.map(step => ({ ...step })),
            currentStep: progress.currentStep ? { ...progress.currentStep } : undefined
        };
    }
    resolveRunStatus(progress) {
        const finished = !progress.currentStep && (progress.total === 0 || progress.processed >= progress.total);
        if (finished) {
            return progress.failed > 0 ? 'failed' : 'completed';
        }
        return 'running';
    }
    notifyProgress(progress, monitor) {
        if (!monitor.onProgress && !(monitor.reporter && monitor.runId)) {
            return;
        }
        const snapshot = Object.freeze(this.createSnapshot(progress));
        if (monitor.onProgress) {
            monitor.onProgress(snapshot);
        }
        if (monitor.reporter && monitor.runId) {
            monitor.reporter.publish({
                runId: monitor.runId,
                progress: snapshot,
                status: this.resolveRunStatus(progress),
                timestamp: new Date()
            });
        }
    }
    beginStep(progress, description) {
        const step = {
            id: `step-${progress.stepHistory.length + 1}`,
            description,
            status: 'running',
            startedAt: new Date()
        };
        progress.currentStep = step;
        progress.stepHistory.push(step);
        return step;
    }
    completeStep(progress, step, status, error) {
        step.status = status;
        step.completedAt = new Date();
        if (error) {
            step.error = error;
        }
        progress.currentStep = undefined;
    }
    createPlan(sourceProvider, targetProvider, overrides = {}) {
        const { sourceProvider: _ignoredSource, targetProvider: _ignoredTarget, targetDimensions: overrideDimensions, projectionType: overrideProjection, normalization: overrideNormalization, batchSize: overrideBatch, dryRun, resumeFromId, targetModel, createdBy, autoRollbackOnFailure } = overrides;
        const targetDimensions = overrideDimensions ?? vectorCompatibilityService.getNativeDimensions(targetProvider);
        return {
            sourceProvider,
            targetProvider,
            targetDimensions,
            projectionType: overrideProjection ?? 'native',
            normalization: overrideNormalization ?? 'none',
            targetModel: targetModel ?? `compat-${targetProvider}`,
            createdBy: createdBy ?? DEFAULT_CREATED_BY,
            batchSize: overrideBatch ?? DEFAULT_BATCH_SIZE,
            dryRun: dryRun ?? false,
            resumeFromId,
            autoRollbackOnFailure: autoRollbackOnFailure ?? true
        };
    }
    listTargets(db, plan, limit) {
        const effectiveLimit = limit ?? plan.batchSize ?? DEFAULT_BATCH_SIZE;
        const resumeKey = plan.resumeFromId ?? null;
        const rows = db
            .prepare(`
        SELECT memory_id, embedding_provider, projection_type, dimensions, model
        FROM memory_embedding
        WHERE embedding_provider = ?
          AND (? IS NULL OR memory_id > ?)
        ORDER BY memory_id
        LIMIT ?
      `)
            .all(plan.sourceProvider, resumeKey, resumeKey, effectiveLimit);
        return rows.map(row => ({
            memoryId: row.memory_id,
            currentProvider: row.embedding_provider,
            currentProjection: (row.projection_type ?? 'native'),
            currentDimensions: row.dimensions ?? 0,
            currentModel: row.model,
            targetProvider: plan.targetProvider,
            targetProjection: plan.projectionType,
            targetDimensions: plan.targetDimensions,
            needsReprojection: (row.dimensions ?? 0) !== plan.targetDimensions || row.projection_type !== plan.projectionType,
            needsProviderSwitch: row.embedding_provider !== plan.targetProvider
        }));
    }
    initializeProgress(total, resumeFromId) {
        const now = new Date();
        return {
            total,
            processed: 0,
            succeeded: 0,
            failed: 0,
            startedAt: now,
            updatedAt: now,
            lastMemoryId: resumeFromId,
            stepHistory: []
        };
    }
    resolveEffectiveMonitor(monitor) {
        return monitor.runId && !monitor.reporter
            ? { ...monitor, reporter: migrationMonitorService }
            : monitor;
    }
    async execute(db, plan, monitor = {}) {
        const effectiveMonitor = this.resolveEffectiveMonitor(monitor);
        const totalRow = db
            .prepare('SELECT COUNT(*) AS count FROM memory_embedding WHERE embedding_provider = ?')
            .get(plan.sourceProvider);
        const total = totalRow?.count ?? 0;
        const progress = this.initializeProgress(total, plan.resumeFromId);
        const errors = [];
        const rollbackEntries = [];
        const reportEvery = effectiveMonitor.reportEvery && effectiveMonitor.reportEvery > 0
            ? effectiveMonitor.reportEvery
            : Math.max(1, Math.floor((plan.batchSize ?? DEFAULT_BATCH_SIZE) / 2));
        const step = this.beginStep(progress, effectiveMonitor.stepDescription ?? 'Embedding vector migration');
        this.notifyProgress(progress, effectiveMonitor);
        if (total === 0) {
            const now = new Date();
            this.completeStep(progress, step, 'completed');
            this.notifyProgress(progress, effectiveMonitor);
            return {
                success: true,
                processed: 0,
                succeeded: 0,
                failed: 0,
                startTime: now,
                endTime: now,
                nextResumeFromId: plan.resumeFromId ?? undefined,
                rollbackEntries: [],
                rolledBack: false
            };
        }
        const selectStatement = db.prepare(`
      SELECT memory_id, embedding, model, embedding_provider, projection_type, dim, dimensions, created_at
      FROM memory_embedding
      WHERE embedding_provider = ?
        AND (? IS NULL OR memory_id > ?)
      ORDER BY memory_id
      LIMIT ?
    `);
        const upsertStatement = db.prepare(`
      INSERT INTO memory_embedding (
        memory_id,
        embedding_provider,
        projection_type,
        embedding,
        dim,
        model,
        dimensions,
        precision,
        normalized,
        version,
        created_by,
        created_at
      ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
      ON CONFLICT(memory_id, embedding_provider, projection_type) DO UPDATE SET
        embedding = excluded.embedding,
        dim = excluded.dim,
        model = excluded.model,
        dimensions = excluded.dimensions,
        precision = excluded.precision,
        normalized = excluded.normalized,
        version = excluded.version,
        created_by = excluded.created_by,
        created_at = CURRENT_TIMESTAMP
    `);
        const normalizationMode = plan.normalization ?? 'none';
        const existingStatement = db.prepare(`
      SELECT embedding, dim, model, dimensions, precision, normalized, version, created_by, created_at
      FROM memory_embedding
      WHERE memory_id = ? AND embedding_provider = ? AND projection_type = ?
    `);
        const processBatch = (batch) => {
            for (const row of batch) {
                this.processMigrationRow({
                    row,
                    plan,
                    normalizationMode,
                    existingStatement,
                    upsertStatement,
                    progress,
                    errors,
                    rollbackEntries,
                    reportEvery,
                    effectiveMonitor
                });
            }
        };
        this.runMigrationBatchLoop(plan, selectStatement, progress, plan.resumeFromId ?? null, processBatch, effectiveMonitor);
        return this.finalizeMigrationExecution({
            db,
            plan,
            progress,
            step,
            errors,
            rollbackEntries,
            effectiveMonitor
        });
    }
    processMigrationRow(params) {
        const { row, plan, normalizationMode, existingStatement, upsertStatement, progress, errors, rollbackEntries, reportEvery, effectiveMonitor } = params;
        progress.processed += 1;
        progress.lastMemoryId = row.memory_id;
        try {
            const parsed = this.safeParseEmbedding(row.embedding);
            const assessment = vectorCompatibilityService.assessProviderCompatibility(parsed, plan.targetProvider, {
                targetDimensions: plan.targetDimensions,
                normalization: normalizationMode
            });
            const storedVector = assessment.projection.vector;
            const serialized = JSON.stringify(storedVector);
            const projectionType = assessment.projection.projectionType;
            const normalizedFlag = assessment.projection.normalized ? 1 : 0;
            const targetModel = plan.targetModel ?? row.model ?? `compat-${plan.targetProvider}`;
            const createdBy = plan.createdBy ?? DEFAULT_CREATED_BY;
            if (!plan.dryRun) {
                const existing = existingStatement.get(row.memory_id, plan.targetProvider, projectionType);
                upsertStatement.run(row.memory_id, plan.targetProvider, projectionType, serialized, plan.targetDimensions, targetModel, plan.targetDimensions, 32, normalizedFlag, 1, createdBy);
                if (existing) {
                    rollbackEntries.push({
                        memoryId: row.memory_id,
                        provider: plan.targetProvider,
                        projectionType,
                        operation: 'restore',
                        embedding: existing.embedding,
                        dim: existing.dim,
                        dimensions: existing.dimensions,
                        model: existing.model,
                        precision: existing.precision,
                        normalized: existing.normalized,
                        version: existing.version,
                        createdBy: existing.created_by,
                        createdAt: existing.created_at ?? undefined
                    });
                }
                else {
                    rollbackEntries.push({
                        memoryId: row.memory_id,
                        provider: plan.targetProvider,
                        projectionType,
                        operation: 'delete'
                    });
                }
            }
            progress.succeeded += 1;
        }
        catch (error) {
            progress.failed += 1;
            errors.push({
                memoryId: row.memory_id,
                provider: plan.targetProvider,
                message: error instanceof Error ? error.message : 'Unknown error'
            });
        }
        finally {
            progress.updatedAt = new Date();
            if (reportEvery > 0 && progress.processed % reportEvery === 0) {
                this.notifyProgress(progress, effectiveMonitor);
            }
        }
    }
    runMigrationBatchLoop(plan, selectStatement, progress, initialLastId, processBatch, effectiveMonitor) {
        let lastProcessedId = initialLastId;
        for (;;) {
            const batch = selectStatement.all(plan.sourceProvider, lastProcessedId, lastProcessedId, plan.batchSize ?? DEFAULT_BATCH_SIZE);
            if (!batch.length) {
                break;
            }
            processBatch(batch);
            lastProcessedId = batch[batch.length - 1]?.memory_id ?? lastProcessedId;
            this.notifyProgress(progress, effectiveMonitor);
        }
    }
    finalizeMigrationExecution(params) {
        const { db, plan, progress, step, errors, rollbackEntries, effectiveMonitor } = params;
        const endTime = new Date();
        const success = progress.failed === 0;
        let rolledBack = false;
        this.completeStep(progress, step, success ? 'completed' : 'failed', success ? undefined : 'One or more records failed');
        this.notifyProgress(progress, effectiveMonitor);
        const shouldRollback = !success && !plan.dryRun && (plan.autoRollbackOnFailure ?? true);
        if (shouldRollback) {
            try {
                this.rollback(db, rollbackEntries);
                rolledBack = true;
            }
            catch (error) {
                const message = error instanceof Error ? `자동 롤백 실패: ${error.message}` : '자동 롤백 중 알 수 없는 오류가 발생했습니다';
                errors.push({
                    memoryId: '*',
                    provider: plan.targetProvider,
                    message
                });
            }
        }
        const result = {
            success,
            processed: progress.processed,
            succeeded: progress.succeeded,
            failed: progress.failed,
            startTime: progress.startedAt,
            endTime,
            nextResumeFromId: progress.lastMemoryId,
            errors: errors.length > 0 ? errors : undefined,
            rollbackEntries,
            rolledBack
        };
        if (!plan.dryRun) {
            migrationHistoryService.recordHistory(db, plan, result);
        }
        return result;
    }
    safeParseEmbedding(raw) {
        try {
            const parsed = JSON.parse(raw);
            if (!Array.isArray(parsed)) {
                throw new Error('임베딩 데이터가 배열 형식이 아닙니다');
            }
            return parsed.map(value => (Number.isFinite(value) ? value : 0));
        }
        catch (error) {
            throw new Error(error instanceof Error ? `임베딩 벡터 파싱 실패: ${error.message}` : '임베딩 벡터 파싱 실패');
        }
    }
    rollback(db, entries) {
        if (!entries.length) {
            return;
        }
        const deleteStatement = db.prepare(`DELETE FROM memory_embedding WHERE memory_id = ? AND embedding_provider = ? AND projection_type = ?`);
        const restoreStatement = db.prepare(`
      INSERT INTO memory_embedding (
        memory_id,
        embedding_provider,
        projection_type,
        embedding,
        dim,
        model,
        dimensions,
        precision,
        normalized,
        version,
        created_by,
        created_at
      ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
      ON CONFLICT(memory_id, embedding_provider, projection_type) DO UPDATE SET
        embedding = excluded.embedding,
        dim = excluded.dim,
        model = excluded.model,
        dimensions = excluded.dimensions,
        precision = excluded.precision,
        normalized = excluded.normalized,
        version = excluded.version,
        created_by = excluded.created_by,
        created_at = excluded.created_at
    `);
        for (const entry of [...entries].reverse()) {
            if (entry.operation === 'delete') {
                this.applyRollbackDelete(deleteStatement, entry);
            }
            else {
                this.applyRollbackRestore(restoreStatement, entry);
            }
        }
    }
    applyRollbackDelete(deleteStatement, entry) {
        deleteStatement.run(entry.memoryId, entry.provider, entry.projectionType);
    }
    applyRollbackRestore(restoreStatement, entry) {
        if (!entry.embedding) {
            throw new Error(`Rollback 데이터가 누락되었습니다: ${entry.memoryId}`);
        }
        restoreStatement.run(entry.memoryId, entry.provider, entry.projectionType, entry.embedding, entry.dim ?? entry.dimensions ?? 0, entry.model ?? null, entry.dimensions ?? entry.dim ?? 0, entry.precision ?? 32, entry.normalized ?? 0, entry.version ?? 1, entry.createdBy ?? DEFAULT_CREATED_BY, entry.createdAt ?? new Date().toISOString());
    }
    listHistory(db, limit = 20) {
        return migrationHistoryService.listHistory(db, { limit });
    }
}
export const embeddingMigrationService = new EmbeddingMigrationService();
//# sourceMappingURL=embedding-migration-service.js.map