import { UsersService } from '@app/event-emitter/users/users.service';
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import TelegramBot from 'node-telegram-bot-api';
import { UserMetaInfoService } from 'src/user-meta-info/user-meta-info.service';
import axios from 'axios';
import { UserMetaInfoModel } from 'src/models/user-meta-info.model';
import { CurrentIssuesEccmBotHandlerService } from './handlers/current-issues-eccm.bot-handler.service';
import { TelegramBotHandlerInterface } from './telegram.bot-handler.interface';
import { SetDailyEccmUserCommentBotHandlerService } from './handlers/set-daily-eccm-user-comment.bot-handler.service';
import { IssuesService } from '@app/event-emitter/issues/issues.service';
import { JSONPath } from 'jsonpath-plus';
import { RedmineTypes } from '@app/event-emitter/models/redmine-types';
import { createAppError } from '@app/event-emitter/utils/result';

const MAX_TELEGRAM_MESSAGE_LENGTH = 4000;

export function cutMessage(msg: string): string {
  if (msg.length > MAX_TELEGRAM_MESSAGE_LENGTH) {
    return msg.slice(0, 4000) + '...';
  }
  return msg;
}

export type OptionsParams = {
  parse_mode?: TelegramBot.ParseMode | undefined;
};

export type RecipientByName = {
  firstname: string;
  lastname: string;
};

export type RecipientByIssueField = {
  issueId: number;
  field: string;
};

export type SendMessageParams = {
  recipients: (RecipientByName | RecipientByIssueField)[];
  msg: string;
  options?: OptionsParams;
};

export function validateSendMessageParams(params: SendMessageParams): string[] {
  const res: string[] = [];
  if (typeof params.msg !== 'string') {
    res.push('Wrong msg field value, must be string');
  }
  if (
    typeof params.recipients !== 'object' ||
    typeof params.recipients.length !== 'number'
  ) {
    res.push('Wrong recipients field value, must be array');
  }
  for (let i = 0; i < params.recipients?.length; i++) {
    const r: any = params.recipients[i];
    const checkRecipient = Boolean(
      (typeof r.firstname === 'string' && typeof r.lastname === 'string') ||
        (typeof r.issueId === 'number' && typeof r.field === 'string'),
    );
    if (!checkRecipient) {
      res.push(
        'Wrong recipient item value, must be object with firstname+lastname or issueId+field',
      );
    }
  }
  if (
    params?.options?.parse_mode &&
    ['Markdown', 'MarkdownV2', 'HTML'].indexOf(params.options.parse_mode) < 0
  ) {
    res.push(
      `Wrong options.parse_mode value, must be one of 'Markdown', 'MarkdownV2', 'HTML'`,
    );
  }
  return res;
}

@Injectable()
export class TelegramBotService {
  private logger = new Logger(TelegramBotService.name);
  private bot: TelegramBot;
  private telegramBotToken: string;
  private redminePublicUrlPrefix: string;

  private registerRe = /\/register (\d+) (.+)/;

  private handlers: TelegramBotHandlerInterface[] = [];

  constructor(
    private userMetaInfoService: UserMetaInfoService,
    private usersService: UsersService,
    private configService: ConfigService,
    private currentIssuesBotHandlerService: CurrentIssuesEccmBotHandlerService,
    private setDailyEccmUserCommentBotHandlerService: SetDailyEccmUserCommentBotHandlerService,
    private issuesService: IssuesService,
  ) {
    this.telegramBotToken = this.configService.get<string>('telegramBotToken');
    this.redminePublicUrlPrefix =
      this.configService.get<string>('redmineUrlPublic');
    this.initTelegramBot().catch((ex) => {
      this.logger.error(`Error at init telegram bot - ${ex}`);
    });
    this.handlers.push(this.currentIssuesBotHandlerService);
    this.handlers.push(this.setDailyEccmUserCommentBotHandlerService);
  }

  private async initTelegramBot(): Promise<void> {
    const Telegram = await require('node-telegram-bot-api');
    if (!this.telegramBotToken) return;
    this.logger.debug('Telegram bot instance creation ... ');
    this.bot = new Telegram(this.telegramBotToken, { polling: true });
    this.logger.debug('Telegram bot instance created');
    this.bot.onText(/\/start/, async (msg) => {
      await this.showHelpMessage(msg);
    });
    this.bot.onText(/\/help/, async (msg) => {
      await this.showHelpMessage(msg);
    });
    this.bot.onText(this.registerRe, async (msg) => {
      await this.register(msg);
    });
    this.bot.onText(/\/leave/, async (msg) => {
      await this.leave(msg);
    });
    this.bot.on('polling_error', (error) => {
      this.logger.error(`polling_error from telegram bot instance - ${error}`);
    });
    this.bot.on('webhook_error', (error) => {
      this.logger.error(`webhook_error from telegram bot instance - ${error}`);
    });
    for (let i = 0; i < this.handlers.length; i++) {
      const handler = this.handlers[i];
      await handler.init(this, this.bot);
    }
  }

  private async showHelpMessage(msg: TelegramBot.Message): Promise<void> {
    if (!this.telegramBotToken) return;
    const userMetaInfo = await this.userMetaInfoService.findByTelegramId(
      msg.chat.id,
    );
    let helpMessage: string;
    /* eslint-disable */
    if (userMetaInfo) {
      const handlersMessages = this.handlers
        .map((handler) => handler.getHelpMsg())
        .join('\n');
      helpMessage = [
        handlersMessages,
        `/help`
      ].join('\n');
    } else {
      helpMessage = [
        `/register <redmine_id> <redmine_token>`, 
        `/help`,
      ].join('\n');
    }
    /* eslint-enable */
    this.logger.debug(
      `Sent help message for telegramChatId = ${msg.chat.id}, ` +
        `message = ${helpMessage}`,
    );
    try {
      this.bot.sendMessage(msg.chat.id, cutMessage(helpMessage));
    } catch (ex) {
      this.logger.error(`Error at send help message - ${ex?.message}`);
    }
  }

  async sendMessageByRedmineId(
    redmineId: number,
    msg: string,
    options?: TelegramBot.SendMessageOptions,
  ): Promise<boolean> {
    if (!this.telegramBotToken) return false;
    const userMetaInfo = await this.userMetaInfoService.findByRedmineId(
      redmineId,
    );
    if (!userMetaInfo) return false;
    const chatId = userMetaInfo.telegram_chat_id;
    const formattedMsg = cutMessage(msg);
    try {
      await this.bot.sendMessage(chatId, formattedMsg, options);
    } catch (ex) {
      this.logger.error(
        `Error at send message to telegram: ` +
          `redmineId - ${redmineId}; ` +
          `message - ${formattedMsg}; ` +
          `exception - ${ex?.message || '<null message>'}`
      );
      return false;
    }
    this.logger.debug(
      `Sent message for redmineUserId = ${redmineId}, ` +
        `telegramChatId = ${chatId}, ` +
        `message = ${msg}, ` +
        `options = ${JSON.stringify(options)}`,
    );
    return true;
  }

  async sendMessageByName(
    firstname: string,
    lastname: string,
    msg: string,
    options?: TelegramBot.SendMessageOptions,
  ): Promise<boolean> {
    if (!this.telegramBotToken) return false;
    const user = await this.usersService.findUserByName(firstname, lastname);
    if (!user) return false;
    return await this.sendMessageByRedmineId(user.id, msg, options);
  }

  async sendMessageByParams(params: SendMessageParams): Promise<void> {
    const paramsErrors = validateSendMessageParams(params);
    if (paramsErrors && paramsErrors.length > 0) {
      throw createAppError(`Params errors - ${JSON.stringify(paramsErrors)}`);
    }
    const issuesStore = await this.getIssuesStore(params);
    for (let i = 0; i < params.recipients.length; i++) {
      const recipient: any = params.recipients[i];
      if (recipient.firstname && recipient.lastname) {
        await this.sendMessageByName(
          recipient.firstname,
          recipient.lastname,
          params.msg,
          params.options,
        );
      } else if (recipient.issueId && recipient.field) {
        const issue = issuesStore[recipient.issueId];
        if (!issue) continue;
        let fieldValue: any;
        try {
          fieldValue = JSONPath({ json: issue, path: recipient.field });
        } catch (ex) {
          const warnMsg =
            `Error at get value from issueId = ${recipient.issueId}; ` +
            `field = ${JSON.stringify(recipient.field)} ` +
            `- ${ex.message}`;
          this.logger.warn(warnMsg);
          continue;
        }
        if (fieldValue && fieldValue.length > 0) {
          fieldValue = fieldValue[0];
        } else {
          continue;
        }
        if (
          typeof fieldValue === 'object' &&
          fieldValue.firstname &&
          fieldValue.lastname
        ) {
          await this.sendMessageByName(
            fieldValue.firstname,
            fieldValue.lastname,
            params.msg,
            params.options,
          );
        } else if (typeof fieldValue === 'number') {
          await this.sendMessageByRedmineId(
            fieldValue,
            params.msg,
            params.options,
          );
        }
      }
    }
  }

  private async register(
    msg: TelegramBot.Message,
  ): Promise<{ result: boolean; message: string }> {
    const items = (msg.text || '').match(this.registerRe);

    const result = (result: boolean, message: string, logData?: any) => {
      const logMsg =
        `Telegram registration ${result ? 'successed' : 'failed'} ` +
        `with message ${message}, ` +
        `log data = ${JSON.stringify(logData || null)}`;
      this.logger.log(logMsg);
      try {
        this.bot.sendMessage(msg.chat.id, message);
      } catch (ex) {
        this.logger.error(
          `Error at send message to telegram: ` +
            `message - ${message}; ` +
            `exception - ${ex?.message || '<null message>'}`
        );
      }
      return { result: result, message: message };
    };

    if (!items || !items[1] || !items[2]) {
      return result(false, 'Не указаны необходимые данные');
    }

    let redmineId: number, redmineToken: string;
    try {
      redmineId = Number(items[1]);
      redmineToken = items[2];
    } catch (ex) {
      return result(false, 'Введены неверные значения', {});
    }

    const url = `${this.redminePublicUrlPrefix}/users/current.json`;

    const resp = await axios.get(url, {
      headers: { 'X-Redmine-API-Key': redmineToken },
    });

    if (resp && resp.data && resp.statusText == 'OK') {
      const data = resp.data;
      if (data.user.id == redmineId) {
        const userData = await this.usersService.getUser(redmineId);
        const userMetaInfo: UserMetaInfoModel = {
          telegram_chat_id: msg.chat.id,
          user_id: userData.id,
        };
        await this.userMetaInfoService.save(userMetaInfo);
        return result(true, `Данные для ${userData.name} подтверждены`, {
          redmineId: redmineId,
        });
      }
    }

    return result(false, 'Не удалось проверить подлинность указанных данных');
  }

  private async leave(msg: TelegramBot.Message): Promise<void> {
    const telegramChatId = msg.chat.id;
    const userMetaInfo = await this.userMetaInfoService.findByTelegramId(
      telegramChatId,
    );
    if (userMetaInfo) {
      await this.userMetaInfoService.delete(userMetaInfo);
    }
  }

  private async getIssuesStore(
    params: SendMessageParams,
  ): Promise<Record<number, RedmineTypes.Issue>> {
    const issueIds: number[] = [];
    for (let i = 0; i < params.recipients.length; i++) {
      const recipient: any = params.recipients[i];
      if (typeof recipient.issueId === 'number' && recipient.issueId > 0) {
        issueIds.push(recipient.issueId);
      }
    }
    if (issueIds.length <= 0) {
      return [];
    }
    const issues = await this.issuesService.getIssues(issueIds);
    return issues.reduce((acc, issue) => {
      acc[issue.id] = issue;
      return acc;
    }, {});
  }
}
