import db, { schema, dbNamespace } from '@nangohq/database';
import environmentService from '../environment.service.js';
import { basePublicUrl, getLogger, stringToHash } from '@nangohq/utils';
import connectionService from '../connection.service.js';
import accountService from '../account.service.js';
const logger = getLogger('SlackService');
const TABLE = dbNamespace + 'slack_notifications';
export const generateSlackConnectionId = (accountUUID, environmentName) => `account-${accountUUID}-${environmentName}`;
/**
 * _nango_slack_notifications
 * @desc persistence layer for slack notifications and the connection list
 * to be able to trigger or resolve notifications
 *
 *  index:
 *      - open
 *      - environment_id
 *      - name
 */
export class SlackService {
    orchestrator;
    logContextGetter;
    actionName = 'flow-result-notifier-action';
    adminConnectionId = process.env['NANGO_ADMIN_CONNECTION_ID'] || 'admin-slack';
    integrationKey = process.env['NANGO_SLACK_INTEGRATION_KEY'] || 'slack';
    nangoAdminUUID = process.env['NANGO_ADMIN_UUID'];
    env = 'prod';
    constructor({ orchestrator, logContextGetter }) {
        this.orchestrator = orchestrator;
        this.logContextGetter = logContextGetter;
    }
    /**
     * Get Nango Admin Connection
     * @desc get the admin connection information to be able to send a duplicate
     * notification to the Nango admin account
     */
    async getNangoAdminConnection() {
        const info = await accountService.getAccountAndEnvironmentIdByUUID(this.nangoAdminUUID, this.env);
        const { success, response: slackConnection } = await connectionService.getConnection(this.adminConnectionId, this.integrationKey, info?.environmentId);
        if (!success || !slackConnection) {
            return null;
        }
        return slackConnection;
    }
    /**
     * Send Duplicate Notification to Nango Admins
     * @desc append the account and environment information to the notification content,
     * add the payload timestamp if available and send the notification to the Nango Admins
     * and with the action response update the slack timestamp to the notification
     * record. This is so future notifications can be sent as updates to the original
     */
    async sendDuplicateNotificationToNangoAdmins(payload, environment_id, logCtx, // TODO: we should not reuse this ctx
    id, ts) {
        const nangoAdminConnection = await this.getNangoAdminConnection();
        if (!nangoAdminConnection) {
            return;
        }
        const account = await environmentService.getAccountFromEnvironment(environment_id);
        if (!account) {
            throw new Error('failed_to_get_account');
        }
        payload.meta = {
            accountName: account.name,
            accountUuid: account.uuid
        };
        if (ts) {
            payload.ts = ts;
        }
        const actionResponse = await this.orchestrator.triggerAction({
            connection: nangoAdminConnection,
            actionName: this.actionName,
            input: payload,
            logCtx
        });
        if (id && actionResponse.isOk() && actionResponse.value.ts) {
            await this.updateNotificationWithAdminTimestamp(id, actionResponse.value.ts);
        }
    }
    /**
     * Update Notification with Timestamp
     * @desc used to keep the slack_timestamp up to date to be able to
     * send updates to the original notification
     */
    async updateNotificationWithTimestamp(id, ts) {
        await schema()
            .from(TABLE)
            .update({
            slack_timestamp: ts
        })
            .where('id', id);
    }
    /**
     * Update Notification with Admin Timestamp
     * @desc used to keep the admin_slack_timestamp up to date to be able to
     * send updates to the original notification
     */
    async updateNotificationWithAdminTimestamp(id, ts) {
        await schema()
            .from(TABLE)
            .update({
            admin_slack_timestamp: ts
        })
            .where('id', id);
    }
    /**
     * Report Failure
     * @desc
     *      1) if slack notifications are enabled and the name is not itself (to avoid an infinite loop)
     *      add the connection to the notification list, grab the connection information
     *      of the admin slack notification action and send the notification to the slack channel
     *      by triggering the action.
     *      2) Update the notification record with the slack timestamp
     *      so future notifications can be sent as updates to the original.
     *      3) Send a duplicate notification to the Nango Admins
     *      4) Add an activity log entry for the notification to the admin account
     */
    async reportFailure(nangoConnection, name, type, originalActivityLogId, environment_id, provider) {
        const slackNotificationsEnabled = await environmentService.getSlackNotificationsEnabled(nangoConnection.environment_id);
        if (!slackNotificationsEnabled) {
            return;
        }
        if (name === this.actionName) {
            return;
        }
        const envName = (await environmentService.getEnvironmentName(nangoConnection.environment_id));
        const { success, error, response: slackNotificationStatus } = await this.addFailingConnection(nangoConnection, name, type);
        // this must mean we don't want to trigger the slack notification
        // b/c it is auth and we don't increment the connection count
        if (success && !slackNotificationStatus) {
            return;
        }
        const account = await environmentService.getAccountFromEnvironment(environment_id);
        if (!account) {
            throw new Error('failed_to_get_account');
        }
        const slackConnectionId = generateSlackConnectionId(account.uuid, envName);
        const adminEnvironment = await environmentService.getAccountAndEnvironment({ accountUuid: this.nangoAdminUUID, envName: this.env });
        if (!adminEnvironment) {
            throw new Error('failed_to_get_admin_env');
        }
        // we get the connection on the nango admin account to be able to send the notification
        const { success: connectionSuccess, error: slackConnectionError, response: slackConnection } = await connectionService.getConnection(slackConnectionId, this.integrationKey, adminEnvironment.environment.id);
        if (!connectionSuccess || !slackConnection) {
            logger.error(slackConnectionError);
            return;
        }
        const logCtx = await this.logContextGetter.create({ operation: { type: 'action', action: 'run' } }, {
            account: adminEnvironment.account,
            environment: adminEnvironment.environment,
            integration: { id: slackConnection.config_id, name: slackConnection.provider_config_key, provider: 'slack' },
            connection: { id: slackConnection.id, name: slackConnection.connection_id }
        });
        if (!success || !slackNotificationStatus) {
            await logCtx.error('Failed looking up the slack notification using the slack notification service', { error });
            await logCtx.failed();
            return;
        }
        const count = slackNotificationStatus.connectionCount;
        const connectionWord = count === 1 ? 'connection' : 'connections';
        const flowType = type;
        const date = new Date();
        const payload = {
            content: this.getMessage({
                type,
                count,
                connectionWord,
                flowType,
                name,
                providerConfigKey: nangoConnection.provider_config_key,
                envName,
                originalActivityLogId,
                date,
                resolved: false
            }),
            status: 'open',
            providerConfigKey: nangoConnection.provider_config_key,
            provider
        };
        if (slackNotificationStatus.slack_timestamp) {
            payload.ts = slackNotificationStatus.slack_timestamp;
        }
        try {
            const actionResponse = await this.orchestrator.triggerAction({
                connection: slackConnection,
                actionName: this.actionName,
                input: payload,
                logCtx
            });
            if (actionResponse.isOk() && actionResponse.value.ts) {
                await this.updateNotificationWithTimestamp(slackNotificationStatus.id, actionResponse.value.ts);
            }
            await this.sendDuplicateNotificationToNangoAdmins(payload, environment_id, logCtx, slackNotificationStatus.id, slackNotificationStatus.admin_slack_timestamp);
            if (actionResponse.isOk()) {
                await logCtx.info(`The action ${this.actionName} was successfully triggered for the ${flowType} ${name} for environment ${slackConnection?.environment_id} for account ${account.uuid}.`, { payload });
                await logCtx.success();
            }
            else {
                await logCtx.error(`The action ${this.actionName} failed to trigger for the ${flowType} ${name} with the error: ${actionResponse.error.message} for environment ${slackConnection?.environment_id} for account ${account.uuid}.`, { error: actionResponse.error, payload });
                await logCtx.failed();
            }
        }
        catch (error) {
            await logCtx.error('Failed to trigger slack notification', { error });
            await logCtx.failed();
        }
    }
    /**
     * Report Resolution
     * @desc
     *      1) if there are no more connections that are failing then send
     *      a resolution notification to the slack channel, otherwise update the message
     *      with the decremented connection count.
     *      2) Send a duplicate notification to the Nango Admins
     *      3) Add an activity log entry for the notification to the admin account
     *
     */
    async reportResolution(nangoConnection, syncName, type, originalActivityLogId, environment_id, provider, slack_timestamp, admin_slack_timestamp, connectionCount) {
        if (syncName === this.actionName) {
            return;
        }
        const envName = (await environmentService.getEnvironmentName(nangoConnection.environment_id));
        let payloadContent = '';
        if (connectionCount === 0) {
            payloadContent = this.getMessage({
                type,
                count: connectionCount,
                connectionWord: 'connections',
                flowType: type,
                name: syncName,
                providerConfigKey: nangoConnection.provider_config_key,
                envName,
                originalActivityLogId,
                date: new Date(),
                resolved: true
            });
        }
        else {
            const count = connectionCount;
            const connection = count === 1 ? 'connection' : 'connections';
            payloadContent = this.getMessage({
                type,
                count,
                connectionWord: connection,
                flowType: type,
                name: syncName,
                providerConfigKey: nangoConnection.provider_config_key,
                envName,
                originalActivityLogId,
                date: new Date(),
                resolved: false
            });
        }
        const payload = {
            content: payloadContent,
            status: connectionCount === 0 ? 'closed' : 'open',
            providerConfigKey: nangoConnection.provider_config_key,
            provider,
            ts: slack_timestamp
        };
        const account = await environmentService.getAccountFromEnvironment(environment_id);
        if (!account) {
            throw new Error('failed_to_get_account');
        }
        const adminEnvironment = await environmentService.getAccountAndEnvironment({ accountUuid: this.nangoAdminUUID, envName: this.env });
        if (!adminEnvironment) {
            throw new Error('failed_to_get_admin_env');
        }
        const slackConnectionId = generateSlackConnectionId(account.uuid, envName);
        const { success: connectionSuccess, response: slackConnection } = await connectionService.getConnection(slackConnectionId, this.integrationKey, adminEnvironment.environment.id);
        if (!connectionSuccess || !slackConnection) {
            return;
        }
        const logCtx = await this.logContextGetter.create({ operation: { type: 'action', action: 'run' } }, {
            account: adminEnvironment.account,
            environment: adminEnvironment.environment,
            integration: { id: slackConnection.config_id, name: slackConnection.provider_config_key, provider: 'slack' },
            connection: { id: slackConnection.id, name: slackConnection.connection_id }
        });
        try {
            const actionResponse = await this.orchestrator.triggerAction({
                connection: slackConnection,
                actionName: this.actionName,
                input: payload,
                logCtx
            });
            await this.sendDuplicateNotificationToNangoAdmins(payload, environment_id, logCtx, undefined, admin_slack_timestamp);
            const content = actionResponse.isOk()
                ? `The action ${this.actionName} was successfully triggered for the ${type} ${syncName} for environment ${slackConnection?.environment_id} for account ${account.uuid}.`
                : `The action ${this.actionName} failed to trigger for the ${type} ${syncName} with the error: ${actionResponse.error.message} for environment ${slackConnection?.environment_id} for account ${account.uuid}.`;
            if (actionResponse.isOk()) {
                await logCtx.info(content, payload);
                await logCtx.success();
            }
            else {
                await logCtx.error(content, { error: actionResponse.error });
                await logCtx.failed();
            }
        }
        catch (error) {
            await logCtx.error('Failed to trigger slack notification', { error });
            await logCtx.failed();
        }
    }
    /**
     * Has Open Notification
     * @desc Check if there is an open notification for the given name
     * and environment id and if so return the necessary information to be able
     * to update the notification.
     */
    async hasOpenNotification(nangoConnection, name, type, trx = db.knex) {
        const hasOpenNotification = await trx
            .select('*')
            .from(TABLE)
            .forUpdate()
            .where({
            open: true,
            environment_id: nangoConnection.environment_id,
            name,
            type
        })
            .first();
        return hasOpenNotification || null;
    }
    /**
     * Create Notification
     * @desc create a new notification for the given name and environment id
     * and return the id of the created notification.
     */
    async createNotification(nangoConnection, name, type, trx = db.knex) {
        const result = await trx
            .from(TABLE)
            .insert({
            open: true,
            environment_id: nangoConnection.environment_id,
            name,
            type,
            connection_list: [nangoConnection.id]
        })
            .returning('id');
        if (result && result.length > 0 && result[0]) {
            return result[0];
        }
        return null;
    }
    /**
     * Add Failing Connection
     * @desc check if there is an open notification for the given name and environment id
     * and if so add the connection id to the connection list.
     */
    async addFailingConnection(nangoConnection, name, type) {
        return await db.knex.transaction(async (trx) => {
            const lockKey = stringToHash(`${nangoConnection.environment_id}-${name}-${type}-add`);
            const { rows } = await trx.raw(`SELECT pg_try_advisory_xact_lock(?) as lock_slack_add_connection`, [lockKey]);
            if (!rows?.[0]?.lock_slack_add_connection) {
                logger.info(`addFailingConnection operation: ${lockKey} could not acquire lock, skipping`);
                return { success: true, error: null, response: null };
            }
            const isOpen = await this.hasOpenNotification(nangoConnection, name, type, trx);
            if (isOpen && type === 'auth') {
                return {
                    success: true,
                    error: null,
                    response: null
                };
            }
            logger.info(`Notifying ${nangoConnection.id} type:${type} name:${name}`);
            if (!isOpen) {
                const created = await this.createNotification(nangoConnection, name, type, trx);
                return {
                    success: true,
                    error: null,
                    response: {
                        id: created?.id,
                        isOpen: false,
                        connectionCount: 1
                    }
                };
            }
            const { id, connection_list } = isOpen;
            if (connection_list.includes(nangoConnection.id)) {
                return {
                    success: true,
                    error: null,
                    response: {
                        id,
                        isOpen: true,
                        slack_timestamp: isOpen.slack_timestamp,
                        admin_slack_timestamp: isOpen.admin_slack_timestamp,
                        connectionCount: connection_list.length
                    }
                };
            }
            connection_list.push(nangoConnection.id);
            await trx.from(TABLE).where({ id }).update({
                connection_list,
                updated_at: new Date()
            });
            return {
                success: true,
                error: null,
                response: {
                    id,
                    isOpen: true,
                    slack_timestamp: isOpen.slack_timestamp,
                    admin_slack_timestamp: isOpen.admin_slack_timestamp,
                    connectionCount: connection_list.length
                }
            };
        });
    }
    /**
     * Remove Failing Connection
     * @desc check if there is an open notification for the given name and environment id
     * and if so remove the connection id from the connection list and report
     * resolution to the slack channel.
     */
    async removeFailingConnection({ connection: nangoConnection, name, type, originalActivityLogId, environment_id, provider }) {
        await db.knex.transaction(async (trx) => {
            const slackNotificationsEnabled = await environmentService.getSlackNotificationsEnabled(nangoConnection.environment_id);
            if (!slackNotificationsEnabled) {
                return;
            }
            const lockKey = stringToHash(`${nangoConnection.environment_id}-${name}-${type}-remove`);
            const { rows } = await trx.raw(`SELECT pg_try_advisory_xact_lock(?) as lock_slack_remove_connection`, [lockKey]);
            if (!rows?.[0]?.lock_slack_remove_connection) {
                logger.info(`removeFailingConnection operation: ${lockKey} could not acquire lock, skipping`);
                return;
            }
            const isOpen = await this.hasOpenNotification(nangoConnection, name, type, trx);
            if (!isOpen) {
                return;
            }
            const { id, connection_list, slack_timestamp, admin_slack_timestamp } = isOpen;
            const index = connection_list.indexOf(nangoConnection.id);
            if (index === -1) {
                return;
            }
            logger.info(`Resolving ${nangoConnection.id} type:${type} name:${name}`);
            connection_list.splice(index, 1);
            await trx
                .from(TABLE)
                .where({ id })
                .update({
                open: connection_list.length > 0,
                connection_list,
                updated_at: new Date()
            });
            // we report resolution to the slack channel which could be either
            // 1) The slack notification is resolved, connection_list === 0
            // 2) The list of failing connections has been decremented
            await this.reportResolution(nangoConnection, name, type, originalActivityLogId, environment_id, provider, slack_timestamp, admin_slack_timestamp, connection_list.length);
        });
    }
    async closeAllOpenNotificationsForEnv(environment_id) {
        await db.knex
            .from(TABLE)
            .where({
            environment_id,
            open: true
        })
            .update({
            open: false,
            updated_at: new Date()
        });
    }
    getLogUrl({ envName, originalActivityLogId, name, date, type }) {
        const usp = new URLSearchParams();
        if (originalActivityLogId) {
            usp.set('operationId', String(originalActivityLogId));
        }
        const from = new Date(date);
        from.setHours(0, 0);
        const to = new Date(date);
        to.setHours(23, 59);
        usp.set('from', from.toISOString());
        usp.set('to', to.toISOString());
        if (type === 'auth') {
            usp.set('connections', name);
        }
        else if (type === 'integration') {
            usp.set('integrations', name);
        }
        else {
            usp.set('syncs', name);
        }
        return `${basePublicUrl}/${envName}/logs?${usp.toString()}`;
    }
    getPageUrl({ envName, providerConfigKey, name, type }) {
        return `${basePublicUrl}/${envName}/${type}/${providerConfigKey}/${name}`;
    }
    getMessage({ type, count, connectionWord, flowType, name, providerConfigKey, envName, originalActivityLogId, date, resolved }) {
        switch (type) {
            case 'sync':
            case 'action': {
                if (resolved) {
                    return `[Resolved] \`${name}\` ${flowType.toLowerCase()} (integration: \`${providerConfigKey}\`) in *${envName}* failed. Read <${this.getLogUrl({ envName, originalActivityLogId, name, date, type })}|logs>.`;
                }
                else {
                    return `\`${name}\` ${flowType.toLowerCase()} (integration: \`${providerConfigKey}\`) is failing for ${count} ${connectionWord} in *${envName}*. Read <${this.getLogUrl({ envName, originalActivityLogId, name, date, type })}|logs>.`;
                }
            }
            case 'auth': {
                if (resolved) {
                    return `[Resolved] connection <${this.getPageUrl({ envName, name, providerConfigKey, type: 'connections' })}|*${name}*> (integration: \`${providerConfigKey}\`) in *${envName}* refresh failed.`;
                }
                else {
                    return `Could not refresh token of connection <${this.getPageUrl({ envName, name, providerConfigKey, type: 'connections' })}|*${name}*> in *${envName}* (integration: \`${providerConfigKey}\`). Read <${this.getLogUrl({ envName, originalActivityLogId, name, date, type })}|logs>.`;
                }
            }
        }
        return '';
    }
    async closeOpenNotificationForConnection({ connectionId, environmentId }) {
        await db.knex.transaction(async (trx) => {
            const rows = await trx
                .select('*')
                .from(TABLE)
                .forUpdate()
                .where({
                open: true,
                environment_id: environmentId
            })
                .whereRaw(`connection_list && '{${connectionId}}'`);
            for (const row of rows) {
                const connectionIds = row.connection_list.filter((id) => id !== connectionId);
                await trx
                    .from(TABLE)
                    .where({ id: row.id })
                    .update({
                    open: connectionIds.length > 0,
                    connection_list: connectionIds,
                    updated_at: new Date()
                });
            }
        });
    }
}
//# sourceMappingURL=slack.service.js.map