import { Injectable, Logger } from '@nestjs/common';
import {
  AllMiddlewareArgs,
  App,
  GenericMessageEvent,
  LogLevel,
  SlackEventMiddlewareArgs,
} from '@slack/bolt';
import { MessageChangedEvent } from '@slack/bolt/dist/types/events/message-events';
import { MessageAttachment, WebClient } from '@slack/web-api';
import { Attachment } from '../attachment/attachment.entity';
import { EEnvVariable } from '../configuration/enums/env-variable.enum';
import { SendExceptionNotificationEmailWorkflow } from '../email/workflows/send-exception-notification-email/send-exception-notification-email.workflow';
import { Ticket } from '../ticket/ticket.entity';
import { SlackMessageChangedWorkflow } from './workflows/slack-message-changed/slack-message-changed.workflow';
import { SlackMessageReceivedWorkflow } from './workflows/slack-message-received/slack-message-received.workflow';

declare const slackBoltApp: App;

@Injectable()
export class SlackService {
  private readonly logger = new Logger('SlackService');
  private readonly NOTIFIABLE_ERROR_TYPES = [
    'account_inactive',
    'invalid_auth',
  ];

  public readonly botApi: WebClient;

  private workingChannelId: string;

  constructor(
    private slackMessageReceivedWorkflow: SlackMessageReceivedWorkflow,
    private slackMessageChangedWorkflow: SlackMessageChangedWorkflow,
    private sendExceptionNotificationEmailWorkflow: SendExceptionNotificationEmailWorkflow
  ) {
    this.botApi = new WebClient(process.env[EEnvVariable.SLACK_BOT_TOKEN], {
      logLevel: LogLevel.DEBUG,
    });

    this.logger.debug('Service initialized');

    this.findWorkingChannel();
  }

  private async findWorkingChannel() {
    const channelName = process.env[EEnvVariable.SLACK_WORKING_CHANNEL_NAME];

    this.logger.debug(
      `findWorkingChannel: Initializing working channel search for string "${channelName}"`
    );

    const conversationsList: any = await this.botApi.conversations.list({
      types: 'public_channel,private_channel',
    });

    if (!conversationsList) {
      throw new Error(
        'findWorkingChannel: non-object received from Slack conversations list'
      );
    }

    this.logger.debug(
      `findWorkingChannel: Received workspace conversations(length=${
        conversationsList.channels.length
      }): ${conversationsList.channels.map((c) => c.name).join(', ')}`
    );

    const channel = conversationsList.channels.find(
      (c) => c.name === channelName
    );
    if (!channel) {
      throw new Error(
        `findWorkingChannel: Failed to find Slack channel with name "${channelName}".`
      );
    }

    this.workingChannelId = channel.id;
    this.logger.log(
      `findWorkingChannel: Working channel id found: ${this.workingChannelId}`
    );

    this.addEventListeners();
  }

  private addEventListeners() {
    slackBoltApp.event<'message'>('message', (e) => this.handleMessage(e));

    this.logger.debug('Event listeners initialized');
  }

  private async handleMessage(
    e: SlackEventMiddlewareArgs<'message'> & AllMiddlewareArgs
  ) {
    if (e.event.channel !== this.workingChannelId) {
      return;
    }

    if (e.event.subtype !== 'message_changed') {
      await this.onMessagePosted(e.message as GenericMessageEvent);
    } else {
      await this.onMessageChanged(
        (e.event as MessageChangedEvent).message as GenericMessageEvent
      );
    }
  }

  private async onMessagePosted(e: GenericMessageEvent) {
    if (e.thread_ts === undefined) {
      return;
    }

    this.logger.debug('Message received');
    await this.slackMessageReceivedWorkflow.handle(e);
  }

  private async onMessageChanged(e: GenericMessageEvent) {
    if (e.thread_ts === undefined) {
      return;
    }

    this.logger.debug('Message change received');
    await this.slackMessageChangedWorkflow.handle(e);
  }

  async postMessage(
    text: string,
    threadTs?: string,
    attachments?: MessageAttachment[]
  ): Promise<any> {
    const payload: any = {
      channel: this.workingChannelId,
      blocks: [
        {
          type: 'section',
          text: {
            type: 'mrkdwn',
            text,
          },
        },
      ],
    };

    if (threadTs !== undefined) {
      payload.thread_ts = threadTs;
    }

    if (attachments !== undefined && attachments.length > 0) {
      payload.blocks = [...payload.blocks, ...attachments];
    }

    return await this.handleNotifiablePromise(
      this.botApi.chat.postMessage(payload)
    );
  }

  addAttachmentAsRemoteFile(attachment: Attachment): Promise<any> {
    return this.botApi.files.remote.add({
      external_id: attachment.id.toString(),
      external_url: `${process.env[EEnvVariable.URL]}/api/attachment/${
        attachment.id
      }/file`,
      title: attachment.name,
      filetype: attachment.name.split('.').pop(),
    });
  }

  getSlackUserById(id: string) {
    return this.botApi.users.info({
      user: id,
    });
  }

  private async handleNotifiablePromise<T = unknown>(
    promise: Promise<T>
  ): Promise<T> {
    try {
      return await promise;
    } catch (e) {
      if (
        e.data &&
        e.data.error &&
        this.NOTIFIABLE_ERROR_TYPES.includes(e.data.error)
      ) {
        await this.sendExceptionNotificationEmailWorkflow.send(
          e,
          'Slack',
          e.data.error
        );
      }

      throw e;
    }
  }

  public async notifyInSlackAboutError(ticket: Ticket, errorMessage: string) {
    try {
      await this.postMessage(null, ticket.slackThreadTs, [
        {
          blocks: [
            {
              type: 'context',
              elements: [
                {
                  type: 'mrkdwn',
                  text: ':exclamation: *_Support Service error_*',
                },
              ],
            },
            {
              type: 'section',
              text: {
                type: 'mrkdwn',
                text: errorMessage,
              },
            },
          ],
        },
      ]);
    } catch (e) {
      this.logger.error(
        `notifyInSlackAboutError (${errorMessage}) failed: ${e.message}`
      );
      console.error(e);
    }
  }
}
