// SPDX-License-Identifier: Apache-2.0
// Copyright 2024-2026 Vorion LLC
/**
 * Scheduled Exports
 *
 * Manages automatic scheduled exports of compliance evidence.
 * Supports weekly and monthly schedules with S3 or email delivery.
 *
 * @packageDocumentation
 */
import { createLogger } from '../../common/logger.js';
import { EvidenceCollector } from './evidence-collector.js';
import { HashVerifier } from './hash-verifier.js';
import { ReportGenerator } from './report-generator.js';
const logger = createLogger({ component: 'scheduled-exports' });
// =============================================================================
// SCHEDULED EXPORT MANAGER
// =============================================================================
/**
 * Manages scheduled compliance evidence exports
 */
export class ScheduledExportManager {
    config;
    schedules = new Map();
    jobs = new Map();
    checkTimer = null;
    running = false;
    activeExports = 0;
    evidenceCollector;
    hashVerifier;
    reportGenerator;
    constructor(config = {}) {
        this.config = {
            checkIntervalMs: config.checkIntervalMs ?? 60000, // 1 minute
            maxConcurrentExports: config.maxConcurrentExports ?? 5,
            retryAttempts: config.retryAttempts ?? 3,
            retryDelayMs: config.retryDelayMs ?? 5000,
        };
        this.evidenceCollector = new EvidenceCollector();
        this.hashVerifier = new HashVerifier();
        this.reportGenerator = new ReportGenerator();
        logger.info('Scheduled export manager initialized');
    }
    // ===========================================================================
    // SCHEDULE MANAGEMENT
    // ===========================================================================
    /**
     * Create a new export schedule
     */
    createSchedule(tenantId, config) {
        const id = crypto.randomUUID();
        const now = new Date().toISOString();
        const schedule = {
            ...config,
            id,
            tenantId,
            createdAt: now,
            updatedAt: now,
            nextRunAt: this.calculateNextRun(config),
        };
        this.schedules.set(id, schedule);
        logger.info({
            scheduleId: id,
            tenantId,
            frequency: config.frequency,
            nextRunAt: schedule.nextRunAt,
        }, 'Export schedule created');
        return schedule;
    }
    /**
     * Update an export schedule
     */
    updateSchedule(scheduleId, updates) {
        const schedule = this.schedules.get(scheduleId);
        if (!schedule)
            return undefined;
        const updated = {
            ...schedule,
            ...updates,
            updatedAt: new Date().toISOString(),
        };
        // Recalculate next run if schedule timing changed
        if (updates.frequency !== undefined ||
            updates.dayOfWeek !== undefined ||
            updates.dayOfMonth !== undefined ||
            updates.hourUtc !== undefined ||
            updates.minuteUtc !== undefined) {
            updated.nextRunAt = this.calculateNextRun(updated);
        }
        this.schedules.set(scheduleId, updated);
        logger.info({ scheduleId, nextRunAt: updated.nextRunAt }, 'Export schedule updated');
        return updated;
    }
    /**
     * Delete an export schedule
     */
    deleteSchedule(scheduleId) {
        const deleted = this.schedules.delete(scheduleId);
        if (deleted) {
            logger.info({ scheduleId }, 'Export schedule deleted');
        }
        return deleted;
    }
    /**
     * Get a schedule by ID
     */
    getSchedule(scheduleId) {
        return this.schedules.get(scheduleId);
    }
    /**
     * List schedules for a tenant
     */
    listSchedules(tenantId) {
        return Array.from(this.schedules.values())
            .filter((s) => s.tenantId === tenantId)
            .sort((a, b) => a.name.localeCompare(b.name));
    }
    /**
     * Calculate the next run time for a schedule
     */
    calculateNextRun(schedule) {
        const now = new Date();
        let nextRun = new Date();
        // Set the time
        nextRun.setUTCHours(schedule.hourUtc, schedule.minuteUtc, 0, 0);
        switch (schedule.frequency) {
            case 'daily':
                // If today's time has passed, schedule for tomorrow
                if (nextRun <= now) {
                    nextRun.setDate(nextRun.getDate() + 1);
                }
                break;
            case 'weekly': {
                const dayMap = {
                    sunday: 0,
                    monday: 1,
                    tuesday: 2,
                    wednesday: 3,
                    thursday: 4,
                    friday: 5,
                    saturday: 6,
                };
                const targetDay = dayMap[schedule.dayOfWeek ?? 'monday'];
                const currentDay = nextRun.getUTCDay();
                let daysUntilTarget = targetDay - currentDay;
                if (daysUntilTarget < 0 || (daysUntilTarget === 0 && nextRun <= now)) {
                    daysUntilTarget += 7;
                }
                nextRun.setDate(nextRun.getDate() + daysUntilTarget);
                break;
            }
            case 'monthly': {
                const targetDay = Math.min(schedule.dayOfMonth ?? 1, 28);
                nextRun.setUTCDate(targetDay);
                // If this month's date has passed, move to next month
                if (nextRun <= now) {
                    nextRun.setUTCMonth(nextRun.getUTCMonth() + 1);
                }
                break;
            }
        }
        return nextRun.toISOString();
    }
    // ===========================================================================
    // SCHEDULER
    // ===========================================================================
    /**
     * Start the scheduler
     */
    start() {
        if (this.running) {
            logger.warn('Scheduler already running');
            return;
        }
        this.running = true;
        this.checkTimer = setInterval(() => this.checkSchedules(), this.config.checkIntervalMs);
        logger.info({ checkIntervalMs: this.config.checkIntervalMs }, 'Scheduled export manager started');
    }
    /**
     * Stop the scheduler
     */
    stop() {
        if (!this.running)
            return;
        this.running = false;
        if (this.checkTimer) {
            clearInterval(this.checkTimer);
            this.checkTimer = null;
        }
        logger.info('Scheduled export manager stopped');
    }
    /**
     * Check for schedules that need to run
     */
    async checkSchedules() {
        if (this.activeExports >= this.config.maxConcurrentExports) {
            return;
        }
        const now = new Date();
        for (const schedule of this.schedules.values()) {
            if (!schedule.enabled)
                continue;
            const nextRun = new Date(schedule.nextRunAt);
            if (nextRun <= now) {
                if (this.activeExports >= this.config.maxConcurrentExports) {
                    break;
                }
                // Run the export
                this.runScheduledExport(schedule).catch((err) => {
                    logger.error({ scheduleId: schedule.id, error: err }, 'Scheduled export failed');
                });
            }
        }
    }
    /**
     * Run a scheduled export
     */
    async runScheduledExport(schedule) {
        this.activeExports++;
        const jobId = crypto.randomUUID();
        const now = new Date();
        // Calculate report period based on frequency
        const periodEnd = new Date(schedule.nextRunAt);
        let periodStart;
        switch (schedule.frequency) {
            case 'daily':
                periodStart = new Date(periodEnd);
                periodStart.setDate(periodStart.getDate() - 1);
                break;
            case 'weekly':
                periodStart = new Date(periodEnd);
                periodStart.setDate(periodStart.getDate() - 7);
                break;
            case 'monthly':
                periodStart = new Date(periodEnd);
                periodStart.setMonth(periodStart.getMonth() - 1);
                break;
        }
        const job = {
            id: jobId,
            scheduleId: schedule.id,
            tenantId: schedule.tenantId,
            status: 'running',
            startedAt: now.toISOString(),
            periodStart: periodStart.toISOString(),
            periodEnd: periodEnd.toISOString(),
            deliveryResults: [],
        };
        this.jobs.set(jobId, job);
        logger.info({
            jobId,
            scheduleId: schedule.id,
            periodStart: job.periodStart,
            periodEnd: job.periodEnd,
        }, 'Starting scheduled export');
        try {
            // Initialize collectors
            await this.evidenceCollector.initialize();
            // Collect evidence
            const collection = await this.evidenceCollector.collectEvidence({
                tenantId: schedule.tenantId,
                startDate: periodStart,
                endDate: periodEnd,
                eventTypes: schedule.eventTypes,
                includePayloads: schedule.includePayloads,
            });
            // Create tamper evidence if requested
            let tamperEvidence;
            if (schedule.includeTamperEvidence) {
                tamperEvidence = await this.hashVerifier.createTamperEvidentPackage(collection);
            }
            // Generate report
            const report = await this.reportGenerator.generateReport(collection, tamperEvidence, {
                format: schedule.format,
                includeTamperEvidence: schedule.includeTamperEvidence,
                includePayloads: schedule.includePayloads,
                title: `Scheduled Compliance Export - ${schedule.name}`,
            });
            // Format report
            let formattedReport;
            switch (schedule.format) {
                case 'json':
                    formattedReport = this.reportGenerator.formatAsJson(report);
                    break;
                case 'csv':
                    formattedReport = this.reportGenerator.formatAsCsv(report);
                    break;
                case 'pdf':
                    formattedReport = this.reportGenerator.formatAsPdfHtml(report);
                    break;
            }
            // Deliver to destinations
            for (const destination of schedule.destinations) {
                const result = await this.deliverToDestination(destination, formattedReport, schedule, job);
                job.deliveryResults.push(result);
            }
            // Update job status
            job.status = job.deliveryResults.every((r) => r.success)
                ? 'completed'
                : 'failed';
            job.completedAt = new Date().toISOString();
            // Update schedule next run
            schedule.lastRunAt = now.toISOString();
            schedule.nextRunAt = this.calculateNextRun(schedule);
            schedule.updatedAt = new Date().toISOString();
            this.schedules.set(schedule.id, schedule);
            logger.info({
                jobId,
                scheduleId: schedule.id,
                status: job.status,
                deliveries: job.deliveryResults.length,
                nextRunAt: schedule.nextRunAt,
            }, 'Scheduled export completed');
        }
        catch (error) {
            job.status = 'failed';
            job.completedAt = new Date().toISOString();
            job.error = error instanceof Error ? error.message : 'Unknown error';
            logger.error({ jobId, scheduleId: schedule.id, error }, 'Scheduled export failed');
            // Still update next run time to avoid infinite retry
            schedule.nextRunAt = this.calculateNextRun(schedule);
            schedule.updatedAt = new Date().toISOString();
            this.schedules.set(schedule.id, schedule);
        }
        finally {
            this.jobs.set(jobId, job);
            this.activeExports--;
        }
    }
    /**
     * Deliver report to a destination
     */
    async deliverToDestination(destination, report, schedule, job) {
        const destinationId = destination.type === 's3'
            ? destination.bucket
            : destination.type === 'email'
                ? destination.recipients[0] ?? 'unknown'
                : destination.url;
        try {
            switch (destination.type) {
                case 's3':
                    return await this.deliverToS3(destination, report, schedule, job);
                case 'email':
                    return await this.deliverToEmail(destination, report, schedule, job);
                case 'webhook':
                    return await this.deliverToWebhook(destination, report, schedule, job);
                default:
                    throw new Error(`Unsupported destination type: ${destination.type}`);
            }
        }
        catch (error) {
            return {
                destinationType: destination.type,
                destinationId,
                success: false,
                error: error instanceof Error ? error.message : 'Unknown error',
            };
        }
    }
    /**
     * Deliver to S3 bucket
     */
    async deliverToS3(destination, report, schedule, job) {
        // In a real implementation, this would use the AWS SDK
        // For now, we'll simulate the upload
        const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
        const prefix = destination.prefix ?? 'compliance-exports';
        const extension = schedule.format === 'csv' ? 'zip' : schedule.format;
        const key = `${prefix}/${schedule.tenantId}/${timestamp}-${schedule.name}.${extension}`;
        logger.info({
            bucket: destination.bucket,
            key,
            region: destination.region,
        }, 'Uploading to S3 (simulated)');
        // Simulate S3 upload
        // In production, this would be:
        // const s3Client = new S3Client({ region: destination.region });
        // await s3Client.send(new PutObjectCommand({
        //   Bucket: destination.bucket,
        //   Key: key,
        //   Body: typeof report === 'string' ? report : JSON.stringify(report),
        //   ContentType: schedule.format === 'json' ? 'application/json' : 'text/html',
        //   ...(destination.kmsKeyId && { ServerSideEncryption: 'aws:kms', SSEKMSKeyId: destination.kmsKeyId }),
        // }));
        return {
            destinationType: 's3',
            destinationId: destination.bucket,
            success: true,
            deliveredAt: new Date().toISOString(),
            details: {
                bucket: destination.bucket,
                key,
                region: destination.region,
            },
        };
    }
    /**
     * Deliver via email
     */
    async deliverToEmail(destination, report, schedule, job) {
        // In a real implementation, this would use nodemailer or a mail service
        // For now, we'll simulate the email send
        const subject = destination.subject ??
            `Compliance Export: ${schedule.name} - ${new Date(job.periodEnd).toISOString().split('T')[0]}`;
        logger.info({
            recipients: destination.recipients,
            subject,
            includeReport: destination.includeReport,
        }, 'Sending email (simulated)');
        // Simulate email send
        // In production, this would be:
        // const transporter = nodemailer.createTransport(destination.smtpConfig);
        // await transporter.sendMail({
        //   to: destination.recipients.join(', '),
        //   cc: destination.cc?.join(', '),
        //   bcc: destination.bcc?.join(', '),
        //   subject,
        //   text: `Compliance evidence export for period ${job.periodStart} to ${job.periodEnd}`,
        //   attachments: destination.includeReport ? [{
        //     filename: `compliance-export.${schedule.format}`,
        //     content: typeof report === 'string' ? report : JSON.stringify(report),
        //   }] : undefined,
        // });
        return {
            destinationType: 'email',
            destinationId: destination.recipients[0] ?? 'unknown',
            success: true,
            deliveredAt: new Date().toISOString(),
            details: {
                recipients: destination.recipients,
                subject,
            },
        };
    }
    /**
     * Deliver via webhook
     */
    async deliverToWebhook(destination, report, schedule, job) {
        const method = destination.method ?? 'POST';
        const timeout = destination.timeout ?? 30000;
        logger.info({
            url: destination.url,
            method,
            includeReport: destination.includeReport,
        }, 'Calling webhook');
        try {
            const body = {
                scheduleId: schedule.id,
                scheduleName: schedule.name,
                tenantId: schedule.tenantId,
                jobId: job.id,
                periodStart: job.periodStart,
                periodEnd: job.periodEnd,
                format: schedule.format,
                ...(destination.includeReport && {
                    report: typeof report === 'string' ? report : report,
                }),
            };
            const controller = new AbortController();
            const timeoutId = setTimeout(() => controller.abort(), timeout);
            const response = await fetch(destination.url, {
                method,
                headers: {
                    'Content-Type': 'application/json',
                    ...destination.headers,
                },
                body: JSON.stringify(body),
                signal: controller.signal,
            });
            clearTimeout(timeoutId);
            if (!response.ok) {
                throw new Error(`Webhook returned ${response.status}: ${response.statusText}`);
            }
            return {
                destinationType: 'webhook',
                destinationId: destination.url,
                success: true,
                deliveredAt: new Date().toISOString(),
                details: {
                    url: destination.url,
                    method,
                    status: response.status,
                },
            };
        }
        catch (error) {
            throw new Error(`Webhook delivery failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
        }
    }
    // ===========================================================================
    // JOB MANAGEMENT
    // ===========================================================================
    /**
     * Get a job by ID
     */
    getJob(jobId) {
        return this.jobs.get(jobId);
    }
    /**
     * List jobs for a schedule
     */
    listJobsForSchedule(scheduleId) {
        return Array.from(this.jobs.values())
            .filter((j) => j.scheduleId === scheduleId)
            .sort((a, b) => {
            const aTime = a.startedAt ? new Date(a.startedAt).getTime() : 0;
            const bTime = b.startedAt ? new Date(b.startedAt).getTime() : 0;
            return bTime - aTime;
        });
    }
    /**
     * Manually trigger a scheduled export
     */
    async triggerExport(scheduleId) {
        const schedule = this.schedules.get(scheduleId);
        if (!schedule)
            return undefined;
        // Run immediately
        await this.runScheduledExport(schedule);
        // Return the latest job for this schedule
        const jobs = this.listJobsForSchedule(scheduleId);
        return jobs[0];
    }
}
// =============================================================================
// SINGLETON
// =============================================================================
let scheduledExportManagerInstance = null;
/**
 * Get the singleton scheduled export manager instance
 */
export function getScheduledExportManager(config) {
    if (!scheduledExportManagerInstance) {
        scheduledExportManagerInstance = new ScheduledExportManager(config);
    }
    return scheduledExportManagerInstance;
}
/**
 * Reset the singleton (for testing)
 */
export function resetScheduledExportManager() {
    if (scheduledExportManagerInstance) {
        scheduledExportManagerInstance.stop();
    }
    scheduledExportManagerInstance = null;
}
//# sourceMappingURL=scheduled-exports.js.map