import { deleteSyncConfig, deleteSyncFilesForConfig } from './config/config.service.js';
import connectionService from '../connection.service.js';
import { deleteScheduleForSync, getSchedule, updateScheduleStatus } from './schedule.service.js';
import { getLatestSyncJob } from './job.service.js';
import telemetry, { LogTypes } from '../../utils/telemetry.js';
import { createSync, getSyncsByConnectionId, getSyncsByProviderConfigKey, getSyncsByProviderConfigAndSyncNames, getSyncByIdAndName, getSyncNamesByConnectionId, softDeleteSync } from './sync.service.js';
import { createActivityLogMessageAndEnd, createActivityLog, createActivityLogMessage, updateSuccess as updateSuccessActivityLog } from '../activity/activity.service.js';
import SyncClient from '../../clients/sync.client.js';
import configService from '../config.service.js';
import { NangoError } from '../../utils/error.js';
import { SyncStatus, ScheduleStatus, SyncConfigType, SyncCommand, CommandToActivityLog } from '../../models/Sync.js';
import { LogActionEnum } from '../../models/Activity.js';
import { stringifyError } from '@nangohq/utils';
import environmentService from '../environment.service.js';
// Should be in "logs" package but impossible thanks to CLI
export const syncCommandToOperation = {
    PAUSE: 'pause',
    UNPAUSE: 'unpause',
    RUN: 'run',
    RUN_FULL: 'run_full',
    CANCEL: 'cancel'
};
export class Orchestrator {
    async create(connections, syncName, providerConfigKey, environmentId, sync, logContextGetter, debug = false, activityLogId, logCtx) {
        try {
            const syncConfig = await configService.getProviderConfig(providerConfigKey, environmentId);
            if (debug && activityLogId) {
                await createActivityLogMessage({
                    level: 'debug',
                    environment_id: environmentId,
                    activity_log_id: activityLogId,
                    timestamp: Date.now(),
                    content: `Beginning iteration of starting syncs for ${syncName} with ${connections.length} connections`
                });
                await logCtx?.debug(`Beginning iteration of starting syncs for ${syncName} with ${connections.length} connections`);
            }
            for (const connection of connections) {
                const syncExists = await getSyncByIdAndName(connection.id, syncName);
                if (syncExists) {
                    continue;
                }
                const createdSync = await createSync(connection.id, syncName);
                const syncClient = await SyncClient.getInstance();
                await syncClient?.startContinuous(connection, createdSync, syncConfig, syncName, { ...sync, returns: sync.models, input: '' }, logContextGetter, debug);
            }
            if (debug && activityLogId) {
                await createActivityLogMessage({
                    level: 'debug',
                    environment_id: environmentId,
                    activity_log_id: activityLogId,
                    timestamp: Date.now(),
                    content: `Finished iteration of starting syncs for ${syncName} with ${connections.length} connections`
                });
                await logCtx?.debug(`Finished iteration of starting syncs for ${syncName} with ${connections.length} connections`);
            }
            return true;
        }
        catch (e) {
            const prettyError = stringifyError(e, { pretty: true });
            await createActivityLogMessage({
                level: 'error',
                environment_id: environmentId,
                activity_log_id: activityLogId,
                timestamp: Date.now(),
                content: `Error starting syncs for ${syncName} with ${connections.length} connections: ${prettyError}`
            });
            await logCtx?.error(`Error starting syncs for ${syncName} with ${connections.length} connections`, { error: e });
            return false;
        }
    }
    async createSyncs(syncArgs, logContextGetter, debug = false, activityLogId, logCtx) {
        let success = true;
        for (const syncToCreate of syncArgs) {
            const { connections, providerConfigKey, environmentId, sync, syncName } = syncToCreate;
            const result = await this.create(connections, syncName, providerConfigKey, environmentId, sync, logContextGetter, debug, activityLogId, logCtx);
            if (!result) {
                success = false;
            }
        }
        return success;
    }
    /**
     * Delete
     * @desc delete a sync and all the related objects
     * 1) sync config files
     * 2) sync config
     */
    async deleteConfig(syncConfigId, environmentId) {
        await deleteSyncFilesForConfig(syncConfigId, environmentId);
        await deleteSyncConfig(syncConfigId);
    }
    async softDeleteSync(syncId, environmentId) {
        await deleteScheduleForSync(syncId, environmentId);
        await softDeleteSync(syncId);
    }
    async softDeleteSyncsByConnection(connection) {
        const syncs = await getSyncsByConnectionId(connection.id);
        if (!syncs) {
            return;
        }
        for (const sync of syncs) {
            await this.softDeleteSync(sync.id, connection.environment_id);
        }
    }
    async deleteSyncsByProviderConfig(environmentId, providerConfigKey) {
        const syncs = await getSyncsByProviderConfigKey(environmentId, providerConfigKey);
        if (!syncs) {
            return;
        }
        for (const sync of syncs) {
            await this.softDeleteSync(sync.id, environmentId);
        }
    }
    async runSyncCommand({ recordsService, environment, providerConfigKey, syncNames, command, logContextGetter, connectionId, initiator }) {
        const action = CommandToActivityLog[command];
        const provider = await configService.getProviderConfig(providerConfigKey, environment.id);
        const account = (await environmentService.getAccountFromEnvironment(environment.id));
        const log = {
            level: 'info',
            success: false,
            action,
            start: Date.now(),
            end: Date.now(),
            timestamp: Date.now(),
            connection_id: connectionId || '',
            provider: provider.provider,
            provider_config_key: providerConfigKey,
            environment_id: environment.id
        };
        const activityLogId = await createActivityLog(log);
        if (!activityLogId) {
            return { success: false, error: new NangoError('failed_to_create_activity_log'), response: false };
        }
        const logCtx = await logContextGetter.create({ id: String(activityLogId), operation: { type: 'sync', action: syncCommandToOperation[command] }, message: '' }, { account, environment, config: { id: provider.id, name: provider.unique_key } });
        const syncClient = await SyncClient.getInstance();
        if (!syncClient) {
            return { success: false, error: new NangoError('failed_to_get_sync_client'), response: false };
        }
        if (connectionId) {
            const { success, error, response: connection } = await connectionService.getConnection(connectionId, providerConfigKey, environment.id);
            if (!success || !connection) {
                return { success: false, error, response: false };
            }
            let syncs = syncNames;
            if (syncs.length === 0) {
                syncs = await getSyncNamesByConnectionId(connection.id);
            }
            for (const syncName of syncs) {
                const sync = await getSyncByIdAndName(connection.id, syncName);
                if (!sync) {
                    throw new Error(`Sync "${syncName}" doesn't exists.`);
                }
                const schedule = await getSchedule(sync.id);
                if (!schedule) {
                    continue;
                }
                await syncClient.runSyncCommand({
                    scheduleId: schedule.schedule_id,
                    syncId: sync?.id,
                    command,
                    activityLogId,
                    environmentId: environment.id,
                    providerConfigKey,
                    connectionId,
                    syncName,
                    nangoConnectionId: connection.id,
                    logCtx,
                    recordsService,
                    initiator
                });
                // if they're triggering a sync that shouldn't change the schedule status
                if (command !== SyncCommand.RUN) {
                    await updateScheduleStatus(schedule.schedule_id, command, activityLogId, environment.id, logCtx);
                }
            }
        }
        else {
            const syncs = syncNames.length > 0
                ? await getSyncsByProviderConfigAndSyncNames(environment.id, providerConfigKey, syncNames)
                : await getSyncsByProviderConfigKey(environment.id, providerConfigKey);
            if (!syncs) {
                const error = new NangoError('no_syncs_found');
                return { success: false, error, response: false };
            }
            for (const sync of syncs) {
                const schedule = await getSchedule(sync.id);
                if (!schedule) {
                    continue;
                }
                const connection = await connectionService.getConnectionById(sync.nango_connection_id);
                if (!connection) {
                    continue;
                }
                await syncClient.runSyncCommand({
                    scheduleId: schedule.schedule_id,
                    syncId: sync.id,
                    command,
                    activityLogId,
                    environmentId: environment.id,
                    providerConfigKey,
                    connectionId: connection.connection_id,
                    syncName: sync.name,
                    nangoConnectionId: connection.id,
                    logCtx,
                    recordsService,
                    initiator
                });
                if (command !== SyncCommand.RUN) {
                    await updateScheduleStatus(schedule.schedule_id, command, activityLogId, environment.id, logCtx);
                }
            }
        }
        await createActivityLogMessageAndEnd({
            level: 'info',
            environment_id: environment.id,
            activity_log_id: activityLogId,
            timestamp: Date.now(),
            content: `Sync was updated with command: "${action}" for sync: ${syncNames.join(', ')}`
        });
        await updateSuccessActivityLog(activityLogId, true);
        await logCtx.info('Sync was successfully updated', { action, syncNames });
        await logCtx.success();
        return { success: true, error: null, response: true };
    }
    async getSyncStatus(environmentId, providerConfigKey, syncNames, connectionId, includeJobStatus = false, optionalConnection) {
        const syncsWithStatus = [];
        let connection = optionalConnection;
        if (connectionId && !connection) {
            const connectionResult = await connectionService.getConnection(connectionId, providerConfigKey, environmentId);
            if (!connectionResult.success || !connectionResult.response) {
                return { success: false, error: connectionResult.error, response: null };
            }
            connection = connectionResult.response;
        }
        if (connection) {
            for (const syncName of syncNames) {
                const sync = await getSyncByIdAndName(connection?.id, syncName);
                if (!sync) {
                    continue;
                }
                const { schedule, latestJob, status, nextScheduledSyncAt } = await this.fetchSyncData(sync?.id, environmentId);
                const reportedStatus = this.reportedStatus(sync, latestJob, schedule, status, nextScheduledSyncAt, includeJobStatus);
                syncsWithStatus.push(reportedStatus);
            }
        }
        else {
            const syncs = syncNames.length > 0
                ? await getSyncsByProviderConfigAndSyncNames(environmentId, providerConfigKey, syncNames)
                : await getSyncsByProviderConfigKey(environmentId, providerConfigKey);
            if (!syncs) {
                return { success: true, error: null, response: syncsWithStatus };
            }
            for (const sync of syncs) {
                const { schedule, latestJob, status, nextScheduledSyncAt } = await this.fetchSyncData(sync?.id, environmentId);
                const reportedStatus = this.reportedStatus(sync, latestJob, schedule, status, nextScheduledSyncAt, includeJobStatus);
                syncsWithStatus.push(reportedStatus);
            }
        }
        return { success: true, error: null, response: syncsWithStatus };
    }
    /**
     * Classify Sync Status
     * @desc categornize the different scenarios of sync status
     * 1. If the schedule is paused and the job is not running, then the sync is paused
     * 2. If the schedule is paused and the job is not running then the sync is stopped (last return case)
     * 3. If the schedule is running but the last job is null then it is an error
     * 4. If the job status is stopped then it is an error
     * 5. If the job status is running then it is running
     * 6. If the job status is success then it is success
     */
    classifySyncStatus(jobStatus, scheduleStatus) {
        if (scheduleStatus === ScheduleStatus.PAUSED && jobStatus !== SyncStatus.RUNNING) {
            return SyncStatus.PAUSED;
        }
        else if (scheduleStatus === ScheduleStatus.RUNNING && jobStatus === null) {
            return SyncStatus.ERROR;
        }
        else if (jobStatus === SyncStatus.STOPPED) {
            return SyncStatus.ERROR;
        }
        else if (jobStatus === SyncStatus.RUNNING) {
            return SyncStatus.RUNNING;
        }
        else if (jobStatus === SyncStatus.SUCCESS) {
            return SyncStatus.SUCCESS;
        }
        return SyncStatus.STOPPED;
    }
    /**
     * Trigger If Connections Exist
     * @desc for the recently deploy flows, create the sync and trigger it if there are connections
     */
    async triggerIfConnectionsExist(flows, environmentId, logContextGetter) {
        for (const flow of flows) {
            if (flow.type === SyncConfigType.ACTION) {
                continue;
            }
            const existingConnections = await connectionService.getConnectionsByEnvironmentAndConfig(environmentId, flow.providerConfigKey);
            if (existingConnections.length === 0) {
                continue;
            }
            const { providerConfigKey } = flow;
            const name = flow.name || flow.syncName;
            await this.create(existingConnections, name, providerConfigKey, environmentId, flow, logContextGetter, false);
        }
    }
    async fetchSyncData(syncId, environmentId) {
        const syncClient = await SyncClient.getInstance();
        const schedule = await getSchedule(syncId);
        const latestJob = await getLatestSyncJob(syncId);
        let status = this.classifySyncStatus(latestJob?.status, schedule?.status);
        const syncSchedule = await syncClient?.describeSchedule(schedule?.schedule_id);
        if (syncSchedule) {
            if (syncSchedule?.schedule?.state?.paused && schedule?.status === ScheduleStatus.RUNNING) {
                await updateScheduleStatus(schedule?.id, SyncCommand.PAUSE, null, environmentId);
                if (status !== SyncStatus.RUNNING) {
                    status = SyncStatus.PAUSED;
                }
                await telemetry.log(LogTypes.TEMPORAL_SCHEDULE_MISMATCH_NOT_RUNNING, 'API: Schedule is marked as paused in temporal but not in the database. The schedule has been updated in the database to be paused.', LogActionEnum.SYNC, {
                    environmentId: String(environmentId),
                    syncId,
                    scheduleId: String(schedule?.schedule_id),
                    level: 'warn'
                }, `syncId:${syncId}`);
            }
            else if (!syncSchedule?.schedule?.state?.paused && status === SyncStatus.PAUSED) {
                await updateScheduleStatus(schedule?.id, SyncCommand.UNPAUSE, null, environmentId);
                status = SyncStatus.STOPPED;
                await telemetry.log(LogTypes.TEMPORAL_SCHEDULE_MISMATCH_NOT_PAUSED, 'API: Schedule is marked as running in temporal but not in the database. The schedule has been updated in the database to be running.', LogActionEnum.SYNC, {
                    environmentId: String(environmentId),
                    syncId,
                    scheduleId: String(schedule?.schedule_id),
                    level: 'warn'
                }, `syncId:${syncId}`);
            }
        }
        let nextScheduledSyncAt = null;
        if (status !== SyncStatus.PAUSED) {
            if (syncSchedule && syncSchedule?.info && syncSchedule?.info.futureActionTimes && syncSchedule?.info?.futureActionTimes?.length > 0) {
                const futureRun = syncSchedule.info.futureActionTimes[0];
                nextScheduledSyncAt = syncClient?.formatFutureRun(futureRun?.seconds?.toNumber());
            }
        }
        return { schedule, latestJob, status, nextScheduledSyncAt };
    }
    reportedStatus(sync, latestJob, schedule, status, nextScheduledSyncAt, includeJobStatus = false) {
        const reportedStatus = {
            id: sync?.id,
            type: latestJob?.type,
            finishedAt: latestJob?.updated_at,
            nextScheduledSyncAt,
            name: sync?.name,
            status,
            frequency: schedule?.frequency,
            latestResult: latestJob?.result
        };
        if (includeJobStatus) {
            reportedStatus['jobStatus'] = latestJob?.status;
        }
        return reportedStatus;
    }
}
export default new Orchestrator();
//# sourceMappingURL=orchestrator.service.js.map