import { schema, dbNamespace } from '../../db/database.js';
import { SyncType, SyncConfigType } from '../../models/Sync.js';
import environmentService from '../environment.service.js';
import { LogActionEnum } from '../../models/Activity.js';
import { updateSuccess as updateSuccessActivityLog, createActivityLogMessage, createActivityLog } from '../activity/activity.service.js';
import { basePublicUrl, getLogger } from '@nangohq/utils';
import connectionService from '../connection.service.js';
import accountService from '../account.service.js';
import SyncClient from '../../clients/sync.client.js';
const logger = getLogger('SlackService');
const TABLE = dbNamespace + 'slack_notifications';
/**
 * _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
 */
class SlackService {
    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';
    /**
     * 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;
    }
    async getAdminEnvironmentId() {
        const info = await accountService.getAccountAndEnvironmentIdByUUID(this.nangoAdminUUID, this.env);
        return info?.environmentId;
    }
    /**
     * 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, activityLogId, environment_id, logCtx, // TODO: we should not reuse this ctx
    id, ts) {
        const nangoAdminConnection = await this.getNangoAdminConnection();
        if (!nangoAdminConnection) {
            return;
        }
        const syncClient = await SyncClient.getInstance();
        if (!syncClient) {
            return;
        }
        const account = await environmentService.getAccountFromEnvironment(environment_id);
        if (!account) {
            throw new Error('failed_to_get_account');
        }
        payload.content = `${payload.content} [Account ${account.uuid} Environment ${environment_id}]`;
        if (ts) {
            payload.ts = ts;
        }
        const actionResponse = await syncClient.triggerAction({
            connection: nangoAdminConnection,
            actionName: this.actionName,
            input: payload,
            activityLogId,
            environment_id: nangoAdminConnection?.environment_id,
            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, syncName, syncType, originalActivityLogId, environment_id, provider, logContextGetter) {
        const slackNotificationsEnabled = await environmentService.getSlackNotificationsEnabled(nangoConnection.environment_id);
        if (!slackNotificationsEnabled) {
            return;
        }
        if (syncName === this.actionName) {
            return;
        }
        const envName = (await environmentService.getEnvironmentName(nangoConnection.environment_id));
        const { success, error, response: slackNotificationStatus } = await this.addFailingConnection(nangoConnection, syncName, syncType);
        const account = await environmentService.getAccountFromEnvironment(environment_id);
        if (!account) {
            throw new Error('failed_to_get_account');
        }
        const slackConnectionId = `account-${account.uuid}-${envName}`;
        const nangoEnvironmentId = await this.getAdminEnvironmentId();
        // 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, nangoEnvironmentId);
        if (!connectionSuccess || !slackConnection) {
            logger.error(slackConnectionError);
            return;
        }
        const log = {
            level: 'info',
            success: false,
            action: LogActionEnum.ACTION,
            start: Date.now(),
            end: Date.now(),
            timestamp: Date.now(),
            connection_id: slackConnection?.connection_id,
            provider_config_key: slackConnection?.provider_config_key,
            provider: this.integrationKey,
            environment_id: slackConnection?.environment_id,
            operation_name: this.actionName
        };
        const activityLogId = await createActivityLog(log);
        const logCtx = await logContextGetter.create({ id: String(activityLogId), operation: { type: 'action' }, message: 'Start action' }, {
            account,
            environment: { id: environment_id, name: envName },
            config: { id: slackConnection.config_id, name: slackConnection.provider_config_key },
            connection: { id: slackConnection.id, name: slackConnection.connection_id }
        });
        if (!success || !slackNotificationStatus) {
            await createActivityLogMessage({
                level: 'error',
                environment_id,
                activity_log_id: activityLogId,
                content: `Failed looking up the slack notification using the slack notification service. The error was: ${error}`,
                timestamp: Date.now()
            });
            await logCtx.error('Failed looking up the slack notification using the slack notification service', { error });
            await logCtx.failed();
            return;
        }
        const count = slackNotificationStatus.connectionCount;
        const connection = count === 1 ? 'connection' : 'connections';
        const flowType = syncType === SyncType.ACTION ? SyncConfigType.ACTION : SyncConfigType.SYNC;
        const date = new Date();
        const dateString = date.toISOString().split('T')[0];
        const payload = {
            content: `*${syncName}* (${flowType.toLowerCase()}) is failing for ${count} ${connection} in *${envName}*. Read <${basePublicUrl}/${envName}/activity?activity_log_id=${originalActivityLogId}&script=${syncName}&date=${dateString}|logs>.`,
            status: 'open',
            providerConfigKey: nangoConnection.provider_config_key,
            provider
        };
        if (slackNotificationStatus.slack_timestamp) {
            payload.ts = slackNotificationStatus.slack_timestamp;
        }
        const syncClient = await SyncClient.getInstance();
        if (!syncClient) {
            return;
        }
        const actionResponse = await syncClient.triggerAction({
            connection: slackConnection,
            actionName: this.actionName,
            input: payload,
            activityLogId: activityLogId,
            environment_id,
            logCtx
        });
        if (actionResponse.isOk() && actionResponse.value.ts) {
            await this.updateNotificationWithTimestamp(slackNotificationStatus.id, actionResponse.value.ts);
        }
        await this.sendDuplicateNotificationToNangoAdmins(payload, originalActivityLogId, environment_id, logCtx, slackNotificationStatus.id, slackNotificationStatus.admin_slack_timestamp);
        const content = actionResponse.isOk()
            ? `The action ${this.actionName} was successfully triggered for the ${flowType} ${syncName} for environment ${slackConnection?.environment_id} for account ${account.uuid}.`
            : `The action ${this.actionName} failed to trigger for the ${flowType} ${syncName} with the error: ${actionResponse.error.message} for environment ${slackConnection?.environment_id} for account ${account.uuid}.`;
        await createActivityLogMessage({
            level: actionResponse.isOk() ? 'info' : 'error',
            activity_log_id: activityLogId,
            environment_id: slackConnection?.environment_id,
            timestamp: Date.now(),
            content,
            params: payload
        });
        await updateSuccessActivityLog(activityLogId, actionResponse.isOk());
        if (actionResponse.isOk()) {
            await logCtx.info(`The action ${this.actionName} was successfully triggered for the ${flowType} ${syncName} 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} ${syncName} with the error: ${actionResponse.error.message} for environment ${slackConnection?.environment_id} for account ${account.uuid}.`, { error: actionResponse.error, payload });
            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, syncType, originalActivityLogId, environment_id, provider, slack_timestamp, admin_slack_timestamp, connectionCount, logContextGetter) {
        if (syncName === this.actionName) {
            return;
        }
        const envName = (await environmentService.getEnvironmentName(nangoConnection.environment_id));
        let payloadContent = '';
        if (connectionCount === 0) {
            payloadContent = `[Resolved] *${syncName}* (${syncType.toLowerCase()}) failed. Read <${basePublicUrl}/${envName}/activity?activity_log_id=${originalActivityLogId}|logs>.`;
        }
        else {
            const count = connectionCount;
            const connection = count === 1 ? 'connection' : 'connections';
            payloadContent = `*${syncName}* (${syncType.toLowerCase()}) is failing for ${count} ${connection} in *${envName}*. Read <${basePublicUrl}/${envName}/activity?activity_log_id=${originalActivityLogId}|logs>.`;
        }
        const payload = {
            content: payloadContent,
            status: connectionCount === 0 ? 'closed' : 'open',
            providerConfigKey: nangoConnection.provider_config_key,
            provider,
            ts: slack_timestamp
        };
        const syncClient = await SyncClient.getInstance();
        if (!syncClient) {
            return;
        }
        const account = await environmentService.getAccountFromEnvironment(environment_id);
        if (!account) {
            throw new Error('failed_to_get_account');
        }
        const nangoEnvironmentId = await this.getAdminEnvironmentId();
        const slackConnectionId = `account-${account.uuid}-${envName}`;
        const { success: connectionSuccess, response: slackConnection } = await connectionService.getConnection(slackConnectionId, this.integrationKey, nangoEnvironmentId);
        if (!connectionSuccess || !slackConnection) {
            return;
        }
        const log = {
            level: 'info',
            success: false,
            action: LogActionEnum.ACTION,
            start: Date.now(),
            end: Date.now(),
            timestamp: Date.now(),
            connection_id: slackConnection?.connection_id,
            provider_config_key: slackConnection?.provider_config_key,
            provider: this.integrationKey,
            environment_id: slackConnection?.environment_id,
            operation_name: this.actionName
        };
        const activityLogId = await createActivityLog(log);
        const logCtx = await logContextGetter.create({ id: String(activityLogId), operation: { type: 'action' }, message: 'Start action' }, {
            account,
            environment: { id: environment_id, name: envName },
            config: { id: slackConnection.config_id, name: slackConnection.provider_config_key },
            connection: { id: slackConnection.id, name: slackConnection.connection_id }
        });
        const actionResponse = await syncClient.triggerAction({
            connection: slackConnection,
            actionName: this.actionName,
            input: payload,
            activityLogId: activityLogId,
            environment_id,
            logCtx
        });
        await this.sendDuplicateNotificationToNangoAdmins(payload, activityLogId, environment_id, logCtx, undefined, admin_slack_timestamp);
        const content = actionResponse.isOk()
            ? `The action ${this.actionName} was successfully triggered for the ${syncType} ${syncName} for environment ${slackConnection?.environment_id} for account ${account.uuid}.`
            : `The action ${this.actionName} failed to trigger for the ${syncType} ${syncName} with the error: ${actionResponse.error.message} for environment ${slackConnection?.environment_id} for account ${account.uuid}.`;
        await createActivityLogMessage({
            level: actionResponse.isOk() ? 'info' : 'error',
            activity_log_id: activityLogId,
            environment_id: slackConnection?.environment_id,
            timestamp: Date.now(),
            content,
            params: payload
        });
        await updateSuccessActivityLog(activityLogId, actionResponse.isOk());
        if (actionResponse.isOk()) {
            await logCtx.info(content, payload);
            await logCtx.success();
        }
        else {
            await logCtx.error(content, { error: actionResponse.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) {
        const hasOpenNotification = await schema()
            .select('id', 'connection_list', 'slack_timestamp', 'admin_slack_timestamp')
            .from(TABLE)
            .where({
            open: true,
            environment_id: nangoConnection.environment_id,
            name
        });
        if (!hasOpenNotification || !hasOpenNotification.length) {
            return null;
        }
        return hasOpenNotification[0];
    }
    /**
     * 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) {
        const result = await schema()
            .from(TABLE)
            .insert({
            open: true,
            environment_id: nangoConnection.environment_id,
            name,
            type: type === SyncType.ACTION ? SyncConfigType.ACTION : SyncConfigType.SYNC,
            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) {
        const isOpen = await this.hasOpenNotification(nangoConnection, name);
        if (!isOpen) {
            const created = await this.createNotification(nangoConnection, name, type);
            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: 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 schema()
            .from(TABLE)
            .where({ id: id })
            .update({
            connection_list,
            updated_at: new Date()
        });
        return {
            success: true,
            error: null,
            response: {
                id: 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(nangoConnection, name, type, originalActivityLogId, environment_id, provider, logContextGetter) {
        const slackNotificationsEnabled = await environmentService.getSlackNotificationsEnabled(nangoConnection.environment_id);
        if (!slackNotificationsEnabled) {
            return;
        }
        const isOpen = await this.hasOpenNotification(nangoConnection, name);
        if (!isOpen) {
            return;
        }
        const { id, connection_list, slack_timestamp, admin_slack_timestamp } = isOpen;
        const index = connection_list.indexOf(nangoConnection.id);
        if (index === -1) {
            return;
        }
        connection_list.splice(index, 1);
        await schema()
            .from(TABLE)
            .where({ id: 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 === SyncType.ACTION ? SyncConfigType.ACTION : SyncConfigType.SYNC, originalActivityLogId, environment_id, provider, slack_timestamp, admin_slack_timestamp, connection_list.length, logContextGetter);
    }
    async closeAllOpenNotifications(environment_id) {
        await schema()
            .from(TABLE)
            .where({
            environment_id,
            open: true
        })
            .update({
            open: false,
            updated_at: new Date()
        });
    }
}
export default new SlackService();
//# sourceMappingURL=slack.service.js.map