import ms from 'ms';
import { Err, Ok, stringifyError, metrics, errorToObject } from '@nangohq/utils';
import { NangoError, deserializeNangoError } from '../utils/error.js';
import telemetry, { LogTypes } from '../utils/telemetry.js';
import { v4 as uuid } from 'uuid';
import errorManager, { ErrorSourceEnum } from '../utils/error.manager.js';
import { LogActionEnum } from '../models/Telemetry.js';
import { SyncCommand, SyncStatus } from '../models/index.js';
import tracer from 'dd-trace';
import { clearLastSyncDate } from '../services/sync/sync.service.js';
import { isSyncJobRunning, updateSyncJobStatus } from '../services/sync/job.service.js';
import { getSyncConfigRaw, getSyncConfigBySyncId } from '../services/sync/config/config.service.js';
import environmentService from '../services/environment.service.js';
const ScheduleName = {
    get: ({ environmentId, syncId }) => {
        return `environment:${environmentId}:sync:${syncId}`;
    },
    parse: (scheduleName) => {
        const parts = scheduleName.split(':');
        if (parts.length !== 4 || parts[0] !== 'environment' || isNaN(Number(parts[1])) || parts[2] !== 'sync' || !parts[3] || parts[3].length === 0) {
            return Err(`Invalid schedule name: ${scheduleName}. expected format: environment:<environmentId>:sync:<syncId>`);
        }
        return Ok({ environmentId: Number(parts[1]), syncId: parts[3] });
    }
};
export class Orchestrator {
    client;
    constructor(client) {
        this.client = client;
    }
    async searchSchedules(props) {
        const scheduleNames = props.map(({ syncId, environmentId }) => ScheduleName.get({ environmentId, syncId }));
        const schedules = await this.client.searchSchedules({ scheduleNames, limit: scheduleNames.length });
        if (schedules.isErr()) {
            return Err(`Failed to get schedules: ${stringifyError(schedules.error)}`);
        }
        const scheduleMap = schedules.value.reduce((map, schedule) => {
            const parsed = ScheduleName.parse(schedule.name);
            if (parsed.isOk()) {
                map.set(parsed.value.syncId, schedule);
            }
            return map;
        }, new Map());
        return Ok(scheduleMap);
    }
    async triggerAction({ connection, actionName, input, logCtx }) {
        const activeSpan = tracer.scope().active();
        const spanTags = {
            'action.name': actionName,
            'connection.id': connection.id,
            'connection.connection_id': connection.connection_id,
            'connection.provider_config_key': connection.provider_config_key,
            'connection.environment_id': connection.environment_id
        };
        const span = tracer.startSpan('execute.action', {
            tags: spanTags,
            ...(activeSpan ? { childOf: activeSpan } : {})
        });
        const startTime = Date.now();
        try {
            let parsedInput = null;
            try {
                parsedInput = input ? JSON.parse(JSON.stringify(input)) : null;
            }
            catch (err) {
                const errorMsg = `Execute: Failed to parse input '${JSON.stringify(input)}': ${stringifyError(err)}`;
                const error = new NangoError('action_failure', { error: errorMsg });
                throw error;
            }
            const groupKey = 'action';
            const executionId = `${groupKey}:environment:${connection.environment_id}:connection:${connection.id}:action:${actionName}:at:${new Date().toISOString()}:${uuid()}`;
            const args = {
                actionName,
                connection: {
                    id: connection.id,
                    connection_id: connection.connection_id,
                    provider_config_key: connection.provider_config_key,
                    environment_id: connection.environment_id
                },
                activityLogId: logCtx.id,
                input: parsedInput
            };
            const actionResult = await this.client.executeAction({
                name: executionId,
                groupKey,
                args
            });
            const res = actionResult.mapError((err) => {
                return (deserializeNangoError(err.payload) ||
                    new NangoError('action_script_failure', { error: err.message, ...(err.payload ? { payload: err.payload } : {}) }));
            });
            if (res.isErr()) {
                throw res.error;
            }
            const content = `The action was successfully run`;
            await logCtx.info(content, {
                action: actionName,
                connection: connection.connection_id,
                integration: connection.provider_config_key,
                truncated_response: JSON.stringify(res.value, null, 2)?.slice(0, 100)
            });
            await telemetry.log(LogTypes.ACTION_SUCCESS, content, LogActionEnum.ACTION, {
                input: JSON.stringify(input, null, 2),
                environmentId: String(connection.environment_id),
                connectionId: connection.connection_id,
                providerConfigKey: connection.provider_config_key,
                actionName
            }, `actionName:${actionName}`);
            metrics.increment(metrics.Types.ACTION_SUCCESS);
            return res;
        }
        catch (err) {
            let formattedError;
            if (err instanceof NangoError) {
                formattedError = err;
            }
            else {
                formattedError = new NangoError('action_failure', { error: errorToObject(err) });
            }
            const content = `Action '${actionName}' failed`;
            await logCtx.error(content, {
                error: formattedError,
                action: actionName,
                connection: connection.connection_id,
                integration: connection.provider_config_key
            });
            await logCtx.enrichOperation({ error: formattedError });
            errorManager.report(err, {
                source: ErrorSourceEnum.PLATFORM,
                operation: LogActionEnum.SYNC_CLIENT,
                environmentId: connection.environment_id,
                metadata: {
                    actionName,
                    connectionDetails: JSON.stringify(connection),
                    input
                }
            });
            await telemetry.log(LogTypes.ACTION_FAILURE, content, LogActionEnum.ACTION, {
                error: stringifyError(err),
                input: JSON.stringify(input, null, 2),
                environmentId: String(connection.environment_id),
                connectionId: connection.connection_id,
                providerConfigKey: connection.provider_config_key,
                actionName,
                level: 'error'
            }, `actionName:${actionName}`);
            metrics.increment(metrics.Types.ACTION_FAILURE);
            span.setTag('error', formattedError);
            return Err(formattedError);
        }
        finally {
            const endTime = Date.now();
            const totalRunTime = (endTime - startTime) / 1000;
            metrics.duration(metrics.Types.ACTION_TRACK_RUNTIME, totalRunTime);
            span.finish();
        }
    }
    async triggerWebhook({ account, environment, integration, connection, webhookName, syncConfig, input, logContextGetter }) {
        const activeSpan = tracer.scope().active();
        const spanTags = {
            'webhook.name': webhookName,
            'connection.id': connection.id,
            'connection.connection_id': connection.connection_id,
            'connection.provider_config_key': connection.provider_config_key,
            'connection.environment_id': connection.environment_id
        };
        const span = tracer.startSpan('execute.webhook', {
            tags: spanTags,
            ...(activeSpan ? { childOf: activeSpan } : {})
        });
        const logCtx = await logContextGetter.create({ operation: { type: 'webhook', action: 'incoming' }, expiresAt: new Date(Date.now() + 15 * 60 * 1000).toISOString() }, {
            account,
            environment,
            integration: { id: integration.id, name: integration.unique_key, provider: integration.provider },
            connection: { id: connection.id, name: connection.connection_id },
            syncConfig: { id: syncConfig.id, name: syncConfig.sync_name }
        });
        try {
            let parsedInput = null;
            try {
                parsedInput = input ? JSON.parse(JSON.stringify(input)) : null;
            }
            catch (err) {
                const errorMsg = `Execute: Failed to parse input '${JSON.stringify(input)}': ${stringifyError(err)}`;
                const error = new NangoError('webhook_failure', { error: errorMsg });
                throw error;
            }
            const groupKey = 'webhook';
            const executionId = `${groupKey}:environment:${connection.environment_id}:connection:${connection.id}:webhook:${webhookName}:at:${new Date().toISOString()}:${uuid()}`;
            const args = {
                webhookName,
                parentSyncName: syncConfig.sync_name,
                connection: {
                    id: connection.id,
                    connection_id: connection.connection_id,
                    provider_config_key: connection.provider_config_key,
                    environment_id: connection.environment_id
                },
                input: parsedInput,
                activityLogId: logCtx.id
            };
            const webhookResult = await this.client.executeWebhook({
                name: executionId,
                groupKey,
                args
            });
            const res = webhookResult.mapError((err) => {
                return (deserializeNangoError(err.payload) ||
                    new NangoError('webhook_script_failure', { error: err.message, ...(err.payload ? { payload: err.payload } : {}) }));
            });
            if (res.isErr()) {
                throw res.error;
            }
            await logCtx.info('The webhook was successfully run', {
                action: webhookName,
                connection: connection.connection_id,
                integration: connection.provider_config_key
            });
            await logCtx.success();
            metrics.increment(metrics.Types.WEBHOOK_SUCCESS);
            return res;
        }
        catch (err) {
            let formattedError;
            if (err instanceof NangoError) {
                formattedError = err;
            }
            else {
                formattedError = new NangoError('webhook_failure', { error: errorToObject(err) });
            }
            await logCtx.error('The webhook failed', {
                error: err,
                webhook: webhookName,
                connection: connection.connection_id,
                integration: connection.provider_config_key
            });
            await logCtx.enrichOperation({ error: formattedError });
            await logCtx.failed();
            errorManager.report(err, {
                source: ErrorSourceEnum.PLATFORM,
                operation: LogActionEnum.SYNC_CLIENT,
                environmentId: connection.environment_id,
                metadata: {
                    parentSyncName: syncConfig.sync_name,
                    webhookName,
                    connectionDetails: JSON.stringify(connection),
                    input
                }
            });
            metrics.increment(metrics.Types.WEBHOOK_FAILURE);
            span.setTag('error', formattedError);
            return Err(formattedError);
        }
        finally {
            span.finish();
        }
    }
    async triggerOnEventScript({ connection, version, name, fileLocation, logCtx }) {
        const activeSpan = tracer.scope().active();
        const spanTags = {
            'onEventScript.name': name,
            'connection.id': connection.id,
            'connection.connection_id': connection.connection_id,
            'connection.provider_config_key': connection.provider_config_key,
            'connection.environment_id': connection.environment_id
        };
        const span = tracer.startSpan('execute.onEventScript', {
            tags: spanTags,
            ...(activeSpan ? { childOf: activeSpan } : {})
        });
        const startTime = Date.now();
        try {
            const groupKey = 'on-event';
            const executionId = `${groupKey}:environment:${connection.environment_id}:connection:${connection.id}:on-event-script:${name}:at:${new Date().toISOString()}:${uuid()}`;
            const args = {
                onEventName: name,
                connection: {
                    id: connection.id,
                    connection_id: connection.connection_id,
                    provider_config_key: connection.provider_config_key,
                    environment_id: connection.environment_id
                },
                version,
                activityLogId: logCtx.id,
                fileLocation
            };
            const result = await this.client.executeOnEvent({
                name: executionId,
                groupKey,
                args
            });
            const res = result.mapError((err) => {
                return (deserializeNangoError(err.payload) ||
                    new NangoError('on_event_script_failure', { error: err.message, ...(err.payload ? { payload: err.payload } : {}) }));
            });
            if (res.isErr()) {
                throw res.error;
            }
            const content = `Script was successfully run.`;
            await logCtx.info(content, {
                onEventScript: name,
                connection: connection.connection_id,
                integration: connection.provider_config_key
            });
            await telemetry.log(LogTypes.ON_EVENT_SCRIPT_SUCCESS, content, LogActionEnum.ON_EVENT_SCRIPT, {
                environmentId: String(connection.environment_id),
                connectionId: connection.connection_id,
                providerConfigKey: connection.provider_config_key,
                name
            }, `onEventScript:${name}`);
            metrics.increment(metrics.Types.ON_EVENT_SCRIPT_SUCCESS);
            return res;
        }
        catch (err) {
            let formattedError;
            if (err instanceof NangoError) {
                formattedError = err;
            }
            else {
                formattedError = new NangoError('on_event_failure', { error: errorToObject(err) });
            }
            const content = `Script failed`;
            await logCtx.error(content, {
                error: formattedError,
                onEvent: name,
                connection: connection.connection_id,
                integration: connection.provider_config_key
            });
            await logCtx.enrichOperation({ error: formattedError });
            errorManager.report(err, {
                source: ErrorSourceEnum.PLATFORM,
                operation: LogActionEnum.SYNC_CLIENT,
                environmentId: connection.environment_id,
                metadata: {
                    name,
                    connectionDetails: JSON.stringify(connection)
                }
            });
            await telemetry.log(LogTypes.ON_EVENT_SCRIPT_FAILURE, content, LogActionEnum.ON_EVENT_SCRIPT, {
                environmentId: String(connection.environment_id),
                connectionId: connection.connection_id,
                providerConfigKey: connection.provider_config_key,
                name,
                level: 'error'
            }, `onEventScript:${name}`);
            metrics.increment(metrics.Types.ON_EVENT_SCRIPT_FAILURE);
            span.setTag('error', formattedError);
            return Err(formattedError);
        }
        finally {
            const endTime = Date.now();
            const totalRunTime = (endTime - startTime) / 1000;
            metrics.duration(metrics.Types.ON_EVENT_SCRIPT_RUNTIME, totalRunTime);
            span.finish();
        }
    }
    async updateSyncFrequency({ syncId, interval, syncName, environmentId, logCtx }) {
        const scheduleName = ScheduleName.get({ environmentId, syncId });
        const frequencyMs = this.getFrequencyMs(interval);
        if (frequencyMs.isErr()) {
            errorManager.report(frequencyMs.error, {
                source: ErrorSourceEnum.CUSTOMER,
                operation: LogActionEnum.SYNC_CLIENT,
                environmentId,
                metadata: {
                    syncName,
                    scheduleName,
                    interval
                }
            });
            return Err(frequencyMs.error);
        }
        const res = await this.client.updateSyncFrequency({ scheduleName, frequencyMs: frequencyMs.value });
        if (res.isErr()) {
            errorManager.report(res.error, {
                source: ErrorSourceEnum.PLATFORM,
                operation: LogActionEnum.SYNC_CLIENT,
                environmentId,
                metadata: {
                    syncName,
                    scheduleName,
                    interval
                }
            });
        }
        else {
            await logCtx?.info(`Sync frequency for "${syncName}" updated to ${interval}`);
        }
        return res;
    }
    async runSyncCommand({ connectionId, syncId, command, environmentId, logCtx, recordsService, initiator, delete_records }) {
        try {
            const cancelling = async (syncId) => {
                const syncJob = await isSyncJobRunning(syncId);
                if (!syncJob || !syncJob?.run_id) {
                    return Err(`Sync job not found for syncId: ${syncId}`);
                }
                await updateSyncJobStatus(syncJob.id, SyncStatus.STOPPED);
                await this.client.cancel({ taskId: syncJob?.run_id, reason: initiator });
                return Ok(undefined);
            };
            const scheduleName = ScheduleName.get({ environmentId, syncId });
            let res;
            switch (command) {
                case SyncCommand.CANCEL:
                    res = await cancelling(syncId);
                    break;
                case SyncCommand.PAUSE:
                    res = await this.client.pauseSync({ scheduleName });
                    break;
                case SyncCommand.UNPAUSE:
                    res = await this.client.unpauseSync({ scheduleName });
                    break;
                case SyncCommand.RUN:
                    res = await this.client.executeSync({ scheduleName });
                    break;
                case SyncCommand.RUN_FULL: {
                    await cancelling(syncId);
                    await clearLastSyncDate(syncId);
                    if (delete_records) {
                        const syncConfig = await getSyncConfigBySyncId(syncId);
                        for (const model of syncConfig?.models || []) {
                            const del = await recordsService.deleteRecordsBySyncId({ syncId, connectionId, environmentId, model });
                            await logCtx.info(`Records for model ${model} were deleted successfully`, del);
                        }
                    }
                    res = await this.client.executeSync({ scheduleName });
                    break;
                }
            }
            if (res.isErr()) {
                await logCtx.error(`Sync command '${command}' failed`, { error: res.error, command });
            }
            return res;
        }
        catch (err) {
            await logCtx.error(`Sync command '${command}' failed`, { error: err, command });
            return Err(err);
        }
    }
    async deleteSync({ syncId, environmentId }) {
        const res = await this.client.deleteSync({ scheduleName: `environment:${environmentId}:sync:${syncId}` });
        if (res.isErr()) {
            errorManager.report(res.error, {
                source: ErrorSourceEnum.PLATFORM,
                operation: LogActionEnum.SYNC,
                environmentId,
                metadata: { syncId, environmentId }
            });
        }
        return res;
    }
    async scheduleSync({ nangoConnection, sync, providerConfig, syncName, syncData, logContextGetter, debug = false }) {
        let logCtx;
        try {
            const syncConfig = await getSyncConfigRaw({
                environmentId: nangoConnection.environment_id,
                config_id: providerConfig.id,
                name: syncName,
                isAction: false
            });
            const { account, environment } = (await environmentService.getAccountAndEnvironment({ environmentId: nangoConnection.environment_id }));
            logCtx = await logContextGetter.create({ operation: { type: 'sync', action: 'init' } }, {
                account,
                environment,
                integration: { id: providerConfig.id, name: providerConfig.unique_key, provider: providerConfig.provider },
                connection: { id: nangoConnection.id, name: nangoConnection.connection_id },
                syncConfig: { id: syncConfig.id, name: syncConfig.sync_name }
            });
            const frequencyMs = this.getFrequencyMs(syncData.runs);
            if (frequencyMs.isErr()) {
                const content = `The sync was not scheduled due to an error with the sync interval "${syncData.runs}": ${frequencyMs.error.message}`;
                await logCtx.error('The sync was not created or started due to an error with the sync interval', {
                    error: frequencyMs.error,
                    runs: syncData.runs
                });
                await logCtx.failed();
                errorManager.report(content, {
                    source: ErrorSourceEnum.CUSTOMER,
                    operation: LogActionEnum.SYNC_CLIENT,
                    environmentId: nangoConnection.environment_id,
                    metadata: {
                        connectionDetails: nangoConnection,
                        providerConfig,
                        syncName,
                        sync,
                        syncData
                    }
                });
                return Err(frequencyMs.error);
            }
            const groupKey = 'sync';
            const schedule = await this.client.recurring({
                name: ScheduleName.get({ environmentId: nangoConnection.environment_id, syncId: sync.id }),
                state: syncData.auto_start ? 'STARTED' : 'PAUSED',
                frequencyMs: frequencyMs.value,
                groupKey,
                retry: { max: 0 },
                timeoutSettingsInSecs: {
                    createdToStarted: 60 * 60, // 1 hour
                    startedToCompleted: 60 * 60 * 24, // 1 day
                    heartbeat: 5 * 60 // 5 minutes
                },
                startsAt: new Date(),
                args: {
                    type: 'sync',
                    syncId: sync.id,
                    syncName,
                    debug,
                    connection: {
                        id: nangoConnection.id,
                        provider_config_key: nangoConnection.provider_config_key,
                        environment_id: nangoConnection.environment_id,
                        connection_id: nangoConnection.connection_id
                    }
                }
            });
            if (schedule.isErr()) {
                throw schedule.error;
            }
            await logCtx.info('Scheduled successfully', { runs: syncData.runs });
            await logCtx.success();
            return Ok(undefined);
        }
        catch (err) {
            errorManager.report(err, {
                source: ErrorSourceEnum.PLATFORM,
                operation: LogActionEnum.SYNC_CLIENT,
                environmentId: nangoConnection.environment_id,
                metadata: {
                    syncName,
                    connectionDetails: JSON.stringify(nangoConnection),
                    syncId: sync.id,
                    providerConfig,
                    syncData: JSON.stringify(syncData)
                }
            });
            if (logCtx) {
                await logCtx.error('Failed to init sync', { error: err });
                await logCtx.failed();
            }
            return Err(`Failed to schedule sync: ${err}`);
        }
    }
    getFrequencyMs(runs) {
        const runsMap = new Map([
            ['every half day', '12h'],
            ['every half hour', '30m'],
            ['every quarter hour', '15m'],
            ['every hour', '1h'],
            ['every day', '1d'],
            ['every month', '30d'],
            ['every week', '7d']
        ]);
        const interval = runsMap.get(runs) || runs.replace('every ', '');
        const intervalMs = ms(interval);
        if (!intervalMs) {
            const error = new NangoError('sync_interval_invalid');
            return Err(error);
        }
        if (intervalMs < ms('30s')) {
            const error = new NangoError('sync_interval_too_short');
            return Err(error);
        }
        return Ok(intervalMs);
    }
}
//# sourceMappingURL=orchestrator.js.map