/**
 * Operation Tracker Service
 *
 * Provides async operation tracking for bulk operations and GDPR requests.
 * Operations are stored in PostgreSQL with Redis-based progress caching.
 *
 * @packageDocumentation
 */
import { randomUUID } from 'node:crypto';
import { eq, and } from 'drizzle-orm';
import { createLogger } from './logger.js';
import { getConfig } from './config.js';
import { getRedis } from './redis.js';
import { getDatabase } from './db.js';
import { operations, } from '@vorionsys/contracts/db';
const logger = createLogger({ component: 'operation-tracker' });
// =============================================================================
// OPERATION TRACKER SERVICE
// =============================================================================
/**
 * Operation Tracker Service
 *
 * Tracks async operations with progress updates and result storage.
 * Uses PostgreSQL for persistence and Redis for fast progress lookups.
 */
export class OperationTrackerService {
    db;
    redis;
    config;
    cachePrefix = 'operation:';
    progressPrefix = 'operation:progress:';
    cacheTtlSeconds = 3600; // 1 hour cache
    longPollTimeoutMs = 30000; // 30 seconds max wait
    constructor(deps = {}) {
        this.db = deps.database ?? getDatabase();
        this.redis = deps.redis ?? getRedis();
        this.config = deps.config ?? getConfig();
    }
    /**
     * Create a new async operation
     */
    async createOperation(options) {
        const operationId = randomUUID();
        const now = new Date();
        const [row] = await this.db
            .insert(operations)
            .values({
            id: operationId,
            tenantId: options.tenantId,
            type: options.type,
            status: 'pending',
            progressCurrent: 0,
            progressTotal: options.totalItems ?? 0,
            metadata: options.metadata ?? null,
            createdBy: options.createdBy ?? null,
            createdAt: now,
            updatedAt: now,
        })
            .returning();
        if (!row) {
            throw new Error('Failed to create operation');
        }
        // Initialize progress in Redis for fast lookups
        await this.redis.hset(this.progressPrefix + operationId, {
            current: '0',
            total: String(options.totalItems ?? 0),
            status: 'pending',
        });
        await this.redis.expire(this.progressPrefix + operationId, this.cacheTtlSeconds);
        logger.info({ operationId, type: options.type, tenantId: options.tenantId }, 'Operation created');
        return operationId;
    }
    /**
     * Update operation progress
     */
    async updateProgress(operationId, current, total) {
        const now = new Date();
        // Update Redis first for fast reads
        await this.redis.hset(this.progressPrefix + operationId, {
            current: String(current),
            total: String(total),
            status: 'processing',
        });
        // Update database
        await this.db
            .update(operations)
            .set({
            status: 'processing',
            progressCurrent: current,
            progressTotal: total,
            updatedAt: now,
        })
            .where(eq(operations.id, operationId));
        // Publish progress update for long-polling
        await this.redis.publish(`operation:${operationId}:progress`, JSON.stringify({
            current,
            total,
            status: 'processing',
        }));
        logger.debug({ operationId, current, total }, 'Operation progress updated');
    }
    /**
     * Mark operation as completed
     */
    async completeOperation(operationId, result) {
        const now = new Date();
        // Update Redis
        await this.redis.hset(this.progressPrefix + operationId, {
            status: 'completed',
        });
        // Update database
        await this.db
            .update(operations)
            .set({
            status: 'completed',
            result: result,
            completedAt: now,
            updatedAt: now,
        })
            .where(eq(operations.id, operationId));
        // Cache the completed operation
        const operation = await this.getOperationFromDb(operationId);
        if (operation) {
            await this.redis.set(this.cachePrefix + operationId, JSON.stringify(operation), 'EX', this.cacheTtlSeconds);
        }
        // Publish completion event for long-polling
        await this.redis.publish(`operation:${operationId}:progress`, JSON.stringify({
            status: 'completed',
            result,
        }));
        logger.info({ operationId }, 'Operation completed');
    }
    /**
     * Mark operation as failed
     */
    async failOperation(operationId, error) {
        const now = new Date();
        // Update Redis
        await this.redis.hset(this.progressPrefix + operationId, {
            status: 'failed',
            error,
        });
        // Update database
        await this.db
            .update(operations)
            .set({
            status: 'failed',
            error,
            completedAt: now,
            updatedAt: now,
        })
            .where(eq(operations.id, operationId));
        // Publish failure event for long-polling
        await this.redis.publish(`operation:${operationId}:progress`, JSON.stringify({
            status: 'failed',
            error,
        }));
        logger.error({ operationId, error }, 'Operation failed');
    }
    /**
     * Get operation by ID
     */
    async getOperation(operationId, tenantId) {
        // Try cache first
        const cached = await this.redis.get(this.cachePrefix + operationId);
        if (cached) {
            const operation = JSON.parse(cached);
            return operation;
        }
        // Get from database
        const operation = await this.getOperationFromDb(operationId, tenantId);
        if (!operation) {
            return null;
        }
        // If completed, cache it
        if (operation.status === 'completed' || operation.status === 'failed') {
            await this.redis.set(this.cachePrefix + operationId, JSON.stringify(operation), 'EX', this.cacheTtlSeconds);
        }
        return operation;
    }
    /**
     * Get operation with long-polling support
     *
     * If wait=true and operation is not completed, waits for completion
     * or timeout (whichever comes first).
     */
    async getOperationWithWait(operationId, tenantId, wait = false, timeoutMs = 30000) {
        const operation = await this.getOperation(operationId, tenantId);
        if (!operation) {
            return null;
        }
        // If not waiting or already complete, return immediately
        if (!wait || operation.status === 'completed' || operation.status === 'failed') {
            return operation;
        }
        // Long-poll for completion
        const effectiveTimeout = Math.min(timeoutMs, this.longPollTimeoutMs);
        return new Promise((resolve) => {
            const channel = `operation:${operationId}:progress`;
            const subscriber = this.redis.duplicate();
            let resolved = false;
            const cleanup = async () => {
                if (!resolved) {
                    resolved = true;
                    try {
                        await subscriber.unsubscribe(channel);
                        await subscriber.quit();
                    }
                    catch {
                        // Ignore cleanup errors
                    }
                }
            };
            // Set timeout
            const timer = setTimeout(async () => {
                await cleanup();
                // Return current state on timeout
                const currentOp = await this.getOperation(operationId, tenantId);
                resolve(currentOp);
            }, effectiveTimeout);
            // Subscribe to progress updates
            subscriber.subscribe(channel, async (err) => {
                if (err) {
                    clearTimeout(timer);
                    await cleanup();
                    const currentOp = await this.getOperation(operationId, tenantId);
                    resolve(currentOp);
                    return;
                }
            });
            subscriber.on('message', async (_channel, message) => {
                try {
                    const update = JSON.parse(message);
                    if (update.status === 'completed' || update.status === 'failed') {
                        clearTimeout(timer);
                        await cleanup();
                        const finalOp = await this.getOperation(operationId, tenantId);
                        resolve(finalOp);
                    }
                }
                catch {
                    // Ignore parse errors
                }
            });
        });
    }
    /**
     * Get operation from database
     */
    async getOperationFromDb(operationId, tenantId) {
        const conditions = [eq(operations.id, operationId)];
        if (tenantId) {
            conditions.push(eq(operations.tenantId, tenantId));
        }
        const [row] = await this.db
            .select()
            .from(operations)
            .where(conditions.length > 1 ? and(...conditions) : conditions[0])
            .limit(1);
        if (!row) {
            return null;
        }
        return this.rowToOperation(row);
    }
    /**
     * Get fast progress from Redis
     * Falls back to database if not in cache
     */
    async getProgress(operationId) {
        const progress = await this.redis.hgetall(this.progressPrefix + operationId);
        if (progress && progress.status) {
            return {
                current: parseInt(progress.current ?? '0', 10),
                total: parseInt(progress.total ?? '0', 10),
                status: progress.status,
            };
        }
        // Fall back to database
        const operation = await this.getOperation(operationId);
        if (!operation) {
            return null;
        }
        return {
            current: operation.progress.current,
            total: operation.progress.total,
            status: operation.status,
        };
    }
    /**
     * Convert database row to AsyncOperation
     */
    rowToOperation(row) {
        return {
            id: row.id,
            type: row.type,
            status: row.status,
            progress: {
                current: row.progressCurrent,
                total: row.progressTotal,
            },
            result: row.result ?? undefined,
            error: row.error ?? undefined,
            metadata: row.metadata ?? undefined,
            createdAt: row.createdAt,
            completedAt: row.completedAt ?? undefined,
        };
    }
}
// =============================================================================
// SINGLETON & FACTORY
// =============================================================================
let operationTrackerInstance = null;
/**
 * Get or create the operation tracker service singleton
 */
export function getOperationTracker() {
    if (!operationTrackerInstance) {
        operationTrackerInstance = new OperationTrackerService();
    }
    return operationTrackerInstance;
}
/**
 * Create a new operation tracker service instance
 */
export function createOperationTracker(deps = {}) {
    return new OperationTrackerService(deps);
}
// =============================================================================
// CONVENIENCE FUNCTIONS
// =============================================================================
/**
 * Create a new async operation
 */
export async function createOperation(type, tenantId, metadata) {
    const tracker = getOperationTracker();
    return tracker.createOperation({ type, tenantId, metadata });
}
/**
 * Update operation progress
 */
export async function updateProgress(operationId, current, total) {
    const tracker = getOperationTracker();
    return tracker.updateProgress(operationId, current, total);
}
/**
 * Mark operation as completed
 */
export async function completeOperation(operationId, result) {
    const tracker = getOperationTracker();
    return tracker.completeOperation(operationId, result);
}
/**
 * Mark operation as failed
 */
export async function failOperation(operationId, error) {
    const tracker = getOperationTracker();
    return tracker.failOperation(operationId, error);
}
/**
 * Get operation by ID
 */
export async function getOperation(operationId) {
    const tracker = getOperationTracker();
    return tracker.getOperation(operationId);
}
//# sourceMappingURL=operation-tracker.js.map