import https from 'node:https';
import { Nango, getUserAgent } from '@nangohq/node';
import configService from '../services/config.service.js';
import paginateService from '../services/paginate.service.js';
import proxyService from '../services/proxy.service.js';
import axios from 'axios';
import { getPersistAPIUrl, safeStringify } from '../utils/utils.js';
import { getLogger, metrics } from '@nangohq/utils';
const logger = getLogger('SDK');
var PaginationType;
(function (PaginationType) {
    PaginationType["CURSOR"] = "cursor";
    PaginationType["LINK"] = "link";
    PaginationType["OFFSET"] = "offset";
})(PaginationType || (PaginationType = {}));
var AuthModes;
(function (AuthModes) {
    AuthModes["OAuth1"] = "OAUTH1";
    AuthModes["OAuth2"] = "OAUTH2";
    AuthModes["OAuth2CC"] = "OAUTH2_CC";
    AuthModes["Basic"] = "BASIC";
    AuthModes["ApiKey"] = "API_KEY";
    AuthModes["AppStore"] = "APP_STORE";
    AuthModes["App"] = "APP";
    AuthModes["Custom"] = "CUSTOM";
    AuthModes["None"] = "NONE";
})(AuthModes || (AuthModes = {}));
export class ActionError extends Error {
    type;
    payload;
    constructor(payload) {
        super();
        this.type = 'action_script_runtime_error';
        if (payload) {
            this.payload = payload;
        }
    }
}
const MEMOIZED_CONNECTION_TTL = 60000;
export class NangoAction {
    nango;
    attributes = {};
    activityLogId;
    syncId;
    nangoConnectionId;
    environmentId;
    syncJobId;
    dryRun;
    abortSignal;
    dryRunService;
    connectionId;
    providerConfigKey;
    provider;
    ActionError = ActionError;
    memoizedConnections = new Map();
    memoizedIntegration;
    constructor(config) {
        this.connectionId = config.connectionId;
        this.providerConfigKey = config.providerConfigKey;
        if (config.activityLogId) {
            this.activityLogId = config.activityLogId;
        }
        this.nango = new Nango({
            isSync: true,
            ...config
        }, {
            userAgent: 'sdk'
        });
        if (config.syncId) {
            this.syncId = config.syncId;
        }
        if (config.nangoConnectionId) {
            this.nangoConnectionId = config.nangoConnectionId;
        }
        if (config.syncJobId) {
            this.syncJobId = config.syncJobId;
        }
        if (config.dryRun) {
            this.dryRun = config.dryRun;
        }
        if (config.environmentId) {
            this.environmentId = config.environmentId;
        }
        if (config.provider) {
            this.provider = config.provider;
        }
        if (config.attributes) {
            this.attributes = config.attributes;
        }
        if (config.abortSignal) {
            this.abortSignal = config.abortSignal;
        }
        if (config.dryRunService) {
            this.dryRunService = config.dryRunService;
        }
    }
    stringify() {
        return JSON.stringify(this, (key, value) => {
            if (key === 'secretKey') {
                return '********';
            }
            return value;
        });
    }
    proxyConfig(config) {
        if (!config.connectionId && this.connectionId) {
            config.connectionId = this.connectionId;
        }
        if (!config.providerConfigKey && this.providerConfigKey) {
            config.providerConfigKey = this.providerConfigKey;
        }
        if (!config.connectionId) {
            throw new Error('Missing connection id');
        }
        if (!config.providerConfigKey) {
            throw new Error('Missing provider config key');
        }
        return {
            ...config,
            providerConfigKey: config.providerConfigKey,
            connectionId: config.connectionId,
            headers: {
                ...(config.headers || {}),
                'user-agent': this.nango.userAgent
            }
        };
    }
    exitSyncIfAborted() {
        if (this.abortSignal?.aborted) {
            process.exit(0);
        }
    }
    async proxy(config) {
        this.exitSyncIfAborted();
        if (this.dryRun) {
            return this.nango.proxy(config);
        }
        else {
            const { connectionId, providerConfigKey } = config;
            const connection = await this.getConnection(providerConfigKey, connectionId);
            if (!connection) {
                throw new Error(`Connection not found using the provider config key ${this.providerConfigKey} and connection id ${this.connectionId}`);
            }
            const proxyConfig = this.proxyConfig(config);
            const { response, activityLogs: activityLogs } = await proxyService.route(proxyConfig, {
                existingActivityLogId: this.activityLogId,
                connection,
                provider: this.provider
            });
            if (activityLogs) {
                for (const log of activityLogs) {
                    if (log.level === 'debug')
                        continue;
                    await this.log(log.content, { level: log.level });
                    switch (log.level) {
                        case 'error':
                            logger.error(log.content);
                            break;
                        case 'warn':
                            logger.warn(log.content);
                            break;
                        case 'info':
                            logger.info(log.content);
                            break;
                        default:
                            logger.debug(log.content);
                    }
                }
            }
            if (response instanceof Error) {
                throw response;
            }
            return response;
        }
    }
    async get(config) {
        return this.proxy({
            ...config,
            method: 'GET'
        });
    }
    async post(config) {
        return this.proxy({
            ...config,
            method: 'POST'
        });
    }
    async put(config) {
        return this.proxy({
            ...config,
            method: 'PUT'
        });
    }
    async patch(config) {
        return this.proxy({
            ...config,
            method: 'PATCH'
        });
    }
    async delete(config) {
        return this.proxy({
            ...config,
            method: 'DELETE'
        });
    }
    async getToken() {
        this.exitSyncIfAborted();
        return this.nango.getToken(this.providerConfigKey, this.connectionId);
    }
    async getConnection(providerConfigKeyOverride, connectionIdOverride) {
        this.exitSyncIfAborted();
        const providerConfigKey = providerConfigKeyOverride || this.providerConfigKey;
        const connectionId = connectionIdOverride || this.connectionId;
        const credentialsPair = `${providerConfigKey}${connectionId}`;
        const cachedConnection = this.memoizedConnections.get(credentialsPair);
        if (!cachedConnection || Date.now() - cachedConnection.timestamp > MEMOIZED_CONNECTION_TTL) {
            const connection = await this.nango.getConnection(providerConfigKey, connectionId);
            this.memoizedConnections.set(credentialsPair, { connection, timestamp: Date.now() });
            return connection;
        }
        return cachedConnection.connection;
    }
    async setMetadata(metadata) {
        this.exitSyncIfAborted();
        try {
            return await this.nango.setMetadata(this.providerConfigKey, this.connectionId, metadata);
        }
        finally {
            this.memoizedConnections.delete(`${this.providerConfigKey}${this.connectionId}`);
        }
    }
    async updateMetadata(metadata) {
        this.exitSyncIfAborted();
        try {
            return await this.nango.updateMetadata(this.providerConfigKey, this.connectionId, metadata);
        }
        finally {
            this.memoizedConnections.delete(`${this.providerConfigKey}${this.connectionId}`);
        }
    }
    /**
     * @deprecated please use setMetadata instead.
     */
    async setFieldMapping(fieldMapping) {
        logger.warn('setFieldMapping is deprecated. Please use setMetadata instead.');
        return this.setMetadata(fieldMapping);
    }
    async getMetadata() {
        this.exitSyncIfAborted();
        return (await this.getConnection(this.providerConfigKey, this.connectionId)).metadata;
    }
    async getWebhookURL() {
        this.exitSyncIfAborted();
        if (this.memoizedIntegration) {
            return this.memoizedIntegration.webhook_url;
        }
        const { config: integration } = await this.nango.getIntegration(this.providerConfigKey, true);
        if (!integration || !integration.provider) {
            throw Error(`There was no provider found for the provider config key: ${this.providerConfigKey}`);
        }
        this.memoizedIntegration = integration;
        return this.memoizedIntegration.webhook_url;
    }
    /**
     * @deprecated please use getMetadata instead.
     */
    async getFieldMapping() {
        logger.warn('getFieldMapping is deprecated. Please use getMetadata instead.');
        const metadata = await this.getMetadata();
        return metadata['fieldMapping'] || {};
    }
    /**
     * Log
     * @desc Log a message to the activity log which shows up in the Nango Dashboard
     * note that the last argument can be an object with a level property to specify the log level
     * example: await nango.log('This is a log message', { level: 'error' })
     * error = red
     * warn = orange
     * info = white
     * debug = grey
     * http = green
     * silly = light green
     */
    async log(...args) {
        this.exitSyncIfAborted();
        if (args.length === 0) {
            return;
        }
        const lastArg = args[args.length - 1];
        const isUserDefinedLevel = (object) => {
            return lastArg && typeof lastArg === 'object' && 'level' in object;
        };
        const userDefinedLevel = isUserDefinedLevel(lastArg) ? lastArg : undefined;
        if (userDefinedLevel) {
            args.pop();
        }
        const content = safeStringify(args);
        if (this.dryRun) {
            logger.info([...args]);
            return;
        }
        if (!this.activityLogId) {
            throw new Error('There is no current activity log stream to log to');
        }
        const response = await persistApi({
            method: 'POST',
            url: `/environment/${this.environmentId}/log`,
            headers: {
                Authorization: `Bearer ${this.nango.secretKey}`
            },
            data: {
                activityLogId: this.activityLogId,
                level: userDefinedLevel?.level ?? 'info',
                msg: content
            }
        });
        if (response.status > 299) {
            logger.error(`Request to persist API (log) failed: errorCode=${response.status} response='${JSON.stringify(response.data)}'`, this.stringify());
            throw new Error(`Failed to log: ${JSON.stringify(response.data)}`);
        }
        return;
    }
    async getEnvironmentVariables() {
        if (!this.environmentId) {
            throw new Error('There is no current environment to get variables from');
        }
        return await this.nango.getEnvironmentVariables();
    }
    getFlowAttributes() {
        if (!this.syncJobId) {
            throw new Error('There is no current sync to get attributes from');
        }
        return this.attributes;
    }
    async *paginate(config) {
        const template = configService.getTemplate(this.provider);
        const templatePaginationConfig = template.proxy?.paginate;
        if (!templatePaginationConfig && (!config.paginate || !config.paginate.type)) {
            throw Error('There was no pagination configuration for this integration or configuration passed in.');
        }
        const paginationConfig = {
            ...(templatePaginationConfig || {}),
            ...(config.paginate || {})
        };
        paginateService.validateConfiguration(paginationConfig);
        config.method = config.method || 'GET';
        const configMethod = config.method.toLocaleLowerCase();
        const passPaginationParamsInBody = ['post', 'put', 'patch'].includes(configMethod);
        const updatedBodyOrParams = (passPaginationParamsInBody ? config.data : config.params) ?? {};
        const limitParameterName = paginationConfig.limit_name_in_request;
        if (paginationConfig['limit']) {
            updatedBodyOrParams[limitParameterName] = paginationConfig['limit'];
        }
        const proxyConfig = this.proxyConfig(config);
        switch (paginationConfig.type.toLowerCase()) {
            case PaginationType.CURSOR:
                return yield* paginateService.cursor(proxyConfig, paginationConfig, updatedBodyOrParams, passPaginationParamsInBody, this.proxy.bind(this));
            case PaginationType.LINK:
                return yield* paginateService.link(proxyConfig, paginationConfig, updatedBodyOrParams, passPaginationParamsInBody, this.proxy.bind(this));
            case PaginationType.OFFSET:
                return yield* paginateService.offset(proxyConfig, paginationConfig, updatedBodyOrParams, passPaginationParamsInBody, this.proxy.bind(this));
            default:
                throw Error(`'${paginationConfig.type} ' pagination is not supported. Please, make sure it's one of ${Object.values(PaginationType)}`);
        }
    }
    async triggerAction(providerConfigKey, connectionId, actionName, input) {
        return this.nango.triggerAction(providerConfigKey, connectionId, actionName, input);
    }
    async triggerSync(providerConfigKey, connectionId, syncName, fullResync) {
        if (this.dryRun && this.dryRunService) {
            return this.dryRunService.run({
                sync: syncName,
                connectionId,
                autoConfirm: true,
                debug: false
            });
        }
        else {
            return this.nango.triggerSync(providerConfigKey, [syncName], connectionId, fullResync);
        }
    }
}
export class NangoSync extends NangoAction {
    lastSyncDate;
    track_deletes = false;
    logMessages = {
        counts: { updated: 0, added: 0, deleted: 0 },
        messages: []
    };
    stubbedMetadata = undefined;
    batchSize = 1000;
    constructor(config) {
        super(config);
        if (config.lastSyncDate) {
            this.lastSyncDate = config.lastSyncDate;
        }
        if (config.track_deletes) {
            this.track_deletes = config.track_deletes;
        }
        if (config.logMessages) {
            this.logMessages = config.logMessages;
        }
        if (config.stubbedMetadata) {
            this.stubbedMetadata = config.stubbedMetadata;
        }
    }
    /**
     * @deprecated please use batchSave
     */
    async batchSend(results, model) {
        logger.warn('batchSend will be deprecated in future versions. Please use batchSave instead.');
        return this.batchSave(results, model);
    }
    async batchSave(results, model) {
        this.exitSyncIfAborted();
        if (!results || results.length === 0) {
            if (this.dryRun) {
                logger.info('batchSave received an empty array. No records to save.');
            }
            return true;
        }
        if (!this.environmentId || !this.nangoConnectionId || !this.syncId || !this.activityLogId || !this.syncJobId) {
            throw new Error('Nango environment Id, Connection Id, Sync Id, Activity Log Id and Sync Job Id are all required');
        }
        if (this.dryRun) {
            this.logMessages?.messages.push(`A batch save call would save the following data to the ${model} model:`);
            for (const msg of results) {
                this.logMessages?.messages.push(msg);
            }
            if (this.logMessages && this.logMessages.counts) {
                this.logMessages.counts.added = Number(this.logMessages.counts.added) + results.length;
            }
            return null;
        }
        for (let i = 0; i < results.length; i += this.batchSize) {
            const batch = results.slice(i, i + this.batchSize);
            const response = await persistApi({
                method: 'POST',
                url: `/environment/${this.environmentId}/connection/${this.nangoConnectionId}/sync/${this.syncId}/job/${this.syncJobId}/records`,
                headers: {
                    Authorization: `Bearer ${this.nango.secretKey}`
                },
                data: {
                    model,
                    records: batch,
                    providerConfigKey: this.providerConfigKey,
                    connectionId: this.connectionId,
                    activityLogId: this.activityLogId
                }
            });
            if (response.status > 299) {
                logger.error(`Request to persist API (batchSave) failed: errorCode=${response.status} response='${JSON.stringify(response.data)}'`, this.stringify());
                throw new Error(`cannot save records for sync '${this.syncId}': ${JSON.stringify(response.data)}`);
            }
        }
        return true;
    }
    async batchDelete(results, model) {
        this.exitSyncIfAborted();
        if (!results || results.length === 0) {
            if (this.dryRun) {
                logger.info('batchDelete received an empty array. No records to delete.');
            }
            return true;
        }
        if (!this.environmentId || !this.nangoConnectionId || !this.syncId || !this.activityLogId || !this.syncJobId) {
            throw new Error('Nango environment Id, Connection Id, Sync Id, Activity Log Id and Sync Job Id are all required');
        }
        if (this.dryRun) {
            this.logMessages?.messages.push(`A batch delete call would delete the following data:`);
            for (const msg of results) {
                this.logMessages?.messages.push(msg);
            }
            if (this.logMessages && this.logMessages.counts) {
                this.logMessages.counts.deleted = Number(this.logMessages.counts.deleted) + results.length;
            }
            return null;
        }
        for (let i = 0; i < results.length; i += this.batchSize) {
            const batch = results.slice(i, i + this.batchSize);
            const response = await persistApi({
                method: 'DELETE',
                url: `/environment/${this.environmentId}/connection/${this.nangoConnectionId}/sync/${this.syncId}/job/${this.syncJobId}/records`,
                headers: {
                    Authorization: `Bearer ${this.nango.secretKey}`
                },
                data: {
                    model,
                    records: batch,
                    providerConfigKey: this.providerConfigKey,
                    connectionId: this.connectionId,
                    activityLogId: this.activityLogId
                }
            });
            if (response.status > 299) {
                logger.error(`Request to persist API (batchDelete) failed: errorCode=${response.status} response='${JSON.stringify(response.data)}'`, this.stringify());
                throw new Error(`cannot delete records for sync '${this.syncId}': ${JSON.stringify(response.data)}`);
            }
        }
        return true;
    }
    async batchUpdate(results, model) {
        this.exitSyncIfAborted();
        if (!results || results.length === 0) {
            if (this.dryRun) {
                logger.info('batchUpdate received an empty array. No records to update.');
            }
            return true;
        }
        if (!this.environmentId || !this.nangoConnectionId || !this.syncId || !this.activityLogId || !this.syncJobId) {
            throw new Error('Nango environment Id, Connection Id, Sync Id, Activity Log Id and Sync Job Id are all required');
        }
        if (this.dryRun) {
            this.logMessages?.messages.push(`A batch update call would update the following data to the ${model} model:`);
            for (const msg of results) {
                this.logMessages?.messages.push(msg);
            }
            if (this.logMessages && this.logMessages.counts) {
                this.logMessages.counts.updated = Number(this.logMessages.counts.updated) + results.length;
            }
            return null;
        }
        for (let i = 0; i < results.length; i += this.batchSize) {
            const batch = results.slice(i, i + this.batchSize);
            const response = await persistApi({
                method: 'PUT',
                url: `/environment/${this.environmentId}/connection/${this.nangoConnectionId}/sync/${this.syncId}/job/${this.syncJobId}/records`,
                headers: {
                    Authorization: `Bearer ${this.nango.secretKey}`
                },
                data: {
                    model,
                    records: batch,
                    providerConfigKey: this.providerConfigKey,
                    connectionId: this.connectionId,
                    activityLogId: this.activityLogId
                }
            });
            if (response.status > 299) {
                logger.error(`Request to persist API (batchUpdate) failed: errorCode=${response.status} response='${JSON.stringify(response.data)}'`, this.stringify());
                throw new Error(`cannot update records for sync '${this.syncId}': ${JSON.stringify(response.data)}`);
            }
        }
        return true;
    }
    async getMetadata() {
        this.exitSyncIfAborted();
        if (this.dryRun && this.stubbedMetadata) {
            return this.stubbedMetadata;
        }
        return super.getMetadata();
    }
}
const persistApi = axios.create({
    baseURL: getPersistAPIUrl(),
    httpsAgent: new https.Agent({ keepAlive: true }),
    headers: {
        'User-Agent': getUserAgent('sdk')
    },
    validateStatus: (_status) => {
        return true;
    }
});
const TELEMETRY_ALLOWED_METHODS = [
    'batchDelete',
    'batchSave',
    'batchSend',
    'getConnection',
    'getEnvironmentVariables',
    'getMetadata',
    'proxy',
    'log',
    'triggerAction',
    'triggerSync'
];
/* eslint-disable no-inner-declarations */
/**
 * This function will enable tracing on the SDK
 * It has been split from the actual code to avoid making the code too dirty and to easily enable/disable tracing if there is an issue with it
 */
export function instrumentSDK(rawNango) {
    return new Proxy(rawNango, {
        get(target, propKey) {
            // Method name is not matching the allowList we don't do anything else
            if (!TELEMETRY_ALLOWED_METHODS.includes(propKey)) {
                return target[propKey];
            }
            return metrics.time(`${metrics.Types.RUNNER_SDK}.${propKey}`, target[propKey].bind(target));
        }
    });
}
/* eslint-enable no-inner-declarations */
//# sourceMappingURL=sync.js.map