import { deleteSyncConfig, deleteSyncFilesForConfig, getSyncConfig, getSyncConfigByParams } 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 { errorNotificationService } from '../notification/error.service.js';
import SyncClient from '../../clients/sync.client.js';
import configService from '../config.service.js';
import { NangoError } from '../../utils/error.js';
import { SyncStatus, ScheduleStatus, SyncCommand } from '../../models/Sync.js';
import { LogActionEnum } from '../../models/Telemetry.js';
import { getLogger, stringifyError } from '@nangohq/utils';
import environmentService from '../environment.service.js';
import { featureFlags } from '../../index.js';
// Should be in "logs" package but impossible thanks to CLI
export const syncCommandToOperation = {
    PAUSE: 'pause',
    UNPAUSE: 'unpause',
    RUN: 'request_run',
    RUN_FULL: 'request_run_full',
    CANCEL: 'cancel'
};
const logger = getLogger('sync.manager');
export class SyncManagerService {
    async createSyncForConnection(nangoConnectionId, logContextGetter, orchestrator) {
        const nangoConnection = (await connectionService.getConnectionById(nangoConnectionId));
        const nangoConfig = await getSyncConfig({ nangoConnection });
        if (!nangoConfig) {
            logger.error('Failed to load the Nango config - will not start any syncs! If you expect to see a sync make sure you used the nango cli deploy command');
            return;
        }
        const { integrations } = nangoConfig;
        const providerConfigKey = nangoConnection?.provider_config_key;
        if (!integrations[providerConfigKey]) {
            return;
        }
        const syncClient = await SyncClient.getInstance();
        if (!syncClient) {
            return;
        }
        const providerConfig = (await configService.getProviderConfig(nangoConnection?.provider_config_key, nangoConnection?.environment_id));
        const syncObject = integrations[providerConfigKey];
        const syncNames = Object.keys(syncObject);
        for (const syncName of syncNames) {
            const syncData = syncObject[syncName];
            if (!syncData.enabled) {
                continue;
            }
            const sync = await createSync(nangoConnectionId, syncName);
            if (sync) {
                await orchestrator.scheduleSyncHelper(nangoConnection, sync, providerConfig, syncName, syncData, logContextGetter);
            }
        }
    }
    async createSyncForConnections(connections, syncName, providerConfigKey, environmentId, sync, logContextGetter, orchestrator, debug = false, logCtx) {
        try {
            const syncConfig = await configService.getProviderConfig(providerConfigKey, environmentId);
            if (debug) {
                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);
                await orchestrator.scheduleSyncHelper(connection, createdSync, syncConfig, syncName, { ...sync, returns: sync.models, input: '' }, logContextGetter, debug);
            }
            if (debug) {
                await logCtx?.debug(`Finished iteration of starting syncs for ${syncName} with ${connections.length} connections`);
            }
            return true;
        }
        catch (e) {
            await logCtx?.error(`Error starting syncs for ${syncName} with ${connections.length} connections`, { error: e });
            return false;
        }
    }
    async createSyncs(syncArgs, logContextGetter, orchestrator, debug = false, logCtx) {
        let success = true;
        for (const syncToCreate of syncArgs) {
            const { connections, providerConfigKey, environmentId, sync, syncName } = syncToCreate;
            const result = await this.createSyncForConnections(connections, syncName, providerConfigKey, environmentId, sync, logContextGetter, orchestrator, debug, 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, orchestrator) {
        await deleteScheduleForSync(syncId, environmentId); // TODO: legacy, to remove once temporal is removed
        await orchestrator.deleteSync({ syncId, environmentId });
        await softDeleteSync(syncId);
        await errorNotificationService.sync.clearBySyncId({ sync_id: syncId });
    }
    async softDeleteSyncsByConnection(connection, orchestrator) {
        const syncs = await getSyncsByConnectionId(connection.id);
        if (!syncs) {
            return;
        }
        for (const sync of syncs) {
            await this.softDeleteSync(sync.id, connection.environment_id, orchestrator);
        }
    }
    async deleteSyncsByProviderConfig(environmentId, providerConfigKey, orchestrator) {
        const syncs = await getSyncsByProviderConfigKey(environmentId, providerConfigKey);
        if (!syncs) {
            return;
        }
        for (const sync of syncs) {
            await this.softDeleteSync(sync.id, environmentId, orchestrator);
        }
    }
    async runSyncCommand({ recordsService, orchestrator, environment, providerConfigKey, syncNames, command, logContextGetter, connectionId, initiator }) {
        const provider = await configService.getProviderConfig(providerConfigKey, environment.id);
        const account = (await environmentService.getAccountFromEnvironment(environment.id));
        const logCtx = await logContextGetter.create({ operation: { type: 'sync', action: syncCommandToOperation[command] }, message: '' }, { account, environment, integration: { id: provider.id, name: provider.unique_key, provider: provider.provider } });
        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 orchestrator.runSyncCommandHelper({
                    scheduleId: schedule.schedule_id,
                    syncId: sync?.id,
                    command,
                    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, 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 orchestrator.runSyncCommandHelper({
                    scheduleId: schedule.schedule_id,
                    syncId: sync.id,
                    command,
                    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, logCtx);
                }
            }
        }
        await logCtx.info('Sync was successfully updated', { command, syncNames });
        await logCtx.success();
        return { success: true, error: null, response: true };
    }
    async getSyncStatus(environmentId, providerConfigKey, syncNames, orchestrator, 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 reportedStatus = await this.syncStatus({ sync, environmentId, providerConfigKey, includeJobStatus, orchestrator });
                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 reportedStatus = await this.syncStatus({ sync, environmentId, providerConfigKey, includeJobStatus, orchestrator });
                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
     */
    legacyClassifySyncStatus(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;
    }
    classifySyncStatus(jobStatus, scheduleState) {
        if (jobStatus === SyncStatus.RUNNING) {
            return SyncStatus.RUNNING;
        }
        switch (scheduleState) {
            case 'PAUSED':
                return SyncStatus.PAUSED;
            case 'STARTED':
                if (jobStatus === SyncStatus.STOPPED) {
                    // job status doesn't have a ERROR status
                    return SyncStatus.ERROR;
                }
                return jobStatus || SyncStatus.SUCCESS;
            default:
                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, orchestrator) {
        for (const flow of flows) {
            if (flow.type === '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.createSyncForConnections(existingConnections, name, providerConfigKey, environmentId, flow, logContextGetter, orchestrator, false);
        }
    }
    async syncStatus({ sync, environmentId, providerConfigKey, includeJobStatus, orchestrator }) {
        const isGloballyEnabled = await featureFlags.isEnabled('orchestrator:schedule', 'global', false);
        const isEnvEnabled = await featureFlags.isEnabled('orchestrator:schedule', `${environmentId}`, false);
        const isOrchestrator = isGloballyEnabled || isEnvEnabled;
        if (isOrchestrator) {
            const latestJob = await getLatestSyncJob(sync.id);
            const schedules = await orchestrator.searchSchedules([{ syncId: sync.id, environmentId }]);
            let frequency = sync.frequency;
            if (!frequency) {
                const syncConfig = await getSyncConfigByParams(environmentId, sync.name, providerConfigKey);
                frequency = syncConfig?.runs || null;
            }
            if (schedules.isErr()) {
                throw new Error(`Failed to get schedule for sync ${sync.id} in environment ${environmentId}: ${stringifyError(schedules.error)}`);
            }
            const schedule = schedules.value.get(sync.id);
            if (!schedule) {
                throw new Error(`Schedule for sync ${sync.id} and environment ${environmentId} not found`);
            }
            return {
                id: sync.id,
                type: latestJob?.type,
                finishedAt: latestJob?.updated_at,
                nextScheduledSyncAt: schedule.nextDueDate,
                name: sync.name,
                status: this.classifySyncStatus(latestJob?.status, schedule.state),
                frequency,
                latestResult: latestJob?.result,
                latestExecutionStatus: latestJob?.status,
                ...(includeJobStatus ? { jobStatus: latestJob?.status } : {})
            };
        }
        return this.legacySyncStatus(sync, environmentId, includeJobStatus);
    }
    async legacySyncStatus(sync, environmentId, includeJobStatus) {
        const syncClient = await SyncClient.getInstance();
        const schedule = await getSchedule(sync.id);
        const latestJob = await getLatestSyncJob(sync.id);
        let status = this.legacyClassifySyncStatus(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);
                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: sync.id,
                    scheduleId: String(schedule?.schedule_id),
                    level: 'warn'
                }, `syncId:${sync.id}`);
            }
            else if (!syncSchedule?.schedule?.state?.paused && status === SyncStatus.PAUSED) {
                await updateScheduleStatus(schedule?.id, SyncCommand.UNPAUSE);
                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: sync.id,
                    scheduleId: String(schedule?.schedule_id),
                    level: 'warn'
                }, `syncId:${sync.id}`);
            }
        }
        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()) || null;
            }
        }
        const reportedStatus = {
            id: sync?.id,
            type: latestJob?.type,
            finishedAt: latestJob?.updated_at,
            nextScheduledSyncAt,
            name: sync?.name,
            status,
            frequency: schedule?.frequency,
            latestResult: latestJob?.result,
            latestExecutionStatus: latestJob?.status
        };
        if (includeJobStatus) {
            reportedStatus['jobStatus'] = latestJob?.status;
        }
        return reportedStatus;
    }
}
export default new SyncManagerService();
//# sourceMappingURL=manager.service.js.map