/*****************************************************************************\
|                                               ( )_  _                       |
|    _ _    _ __   _ _    __    ___ ___     _ _ | ,_)(_)  ___   ___     _     |
|   ( '_`\ ( '__)/'_` ) /'_ `\/' _ ` _ `\ /'_` )| |  | |/',__)/' v `\ /'_`\   |
|   | (_) )| |  ( (_| |( (_) || ( ) ( ) |( (_| || |_ | |\__, \| (˅) |( (_) )  |
|   | ,__/'(_)  `\__,_)`\__  |(_) (_) (_)`\__,_)`\__)(_)(____/(_) (_)`\___/'  |
|   | |                ( )_) |                                                |
|   (_)                 \___/'                                                |
|                                                                             |
| General Bots Copyright (c) Pragmatismo.io. All rights reserved.             |
| Licensed under the AGPL-3.0.                                                |
|                                                                             |
| According to our dual licensing model, this program can be used either      |
| under the terms of the GNU Affero General Public License, version 3,        |
| or under a proprietary license.                                             |
|                                                                             |
| The texts of the GNU Affero General Public License with an additional       |
| permission and of our proprietary license can be found at and               |
| in the LICENSE file you have received along with this program.              |
|                                                                             |
| This program is distributed in the hope that it will be useful,             |
| but WITHOUT ANY WARRANTY, without even the implied warranty of              |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the                |
| GNU Affero General Public License for more details.                         |
|                                                                             |
| "General Bots" is a registered trademark of Pragmatismo.io.                 |
| The licensing of the program under the AGPLv3 does not imply a              |
| trademark license. Therefore any rights, title and interest in              |
| our trademarks remain entirely with us.                                     |
|                                                                             |
\*****************************************************************************/

const Swagger = require('swagger-client');
const fs = require('fs');
const { google } = require('googleapis')
const { promisify } = require('util');
const { PubSub } = require('@google-cloud/pubsub');
import { GBLog, GBMinInstance, GBService } from 'botlib';
import { GBServer } from '../../../src/app';
import { SecService } from '../../security.gbapp/services/SecService';

/**
 * Support for Google Chat.
 */
export class GoogleChatDirectLine extends GBService {

  public static conversationIds = {};
  public pollInterval = 5000;
  public directLineClientName = 'DirectLineClient';

  public directLineClient: any;
  public GoogleChatSubscriptionName: string;
  public botId: string;
  public min: GBMinInstance;
  private directLineSecret: string;
  pubSubClient: any;
  GoogleChatApiKey: any;
  GoogleClientEmail: any;
  GoogleClientPrivateKey: any;
  GoogleProjectId: any;

  constructor(
    min: GBMinInstance,
    botId,
    directLineSecret,
    GoogleChatSubscriptionName,
    GoogleChatApiKey,
    GoogleClientEmail,
    GoogleClientPrivateKey,
    GoogleProjectId
  ) {
    super();

    this.min = min;
    this.botId = botId;
    this.directLineSecret = directLineSecret;
    this.GoogleChatSubscriptionName = GoogleChatSubscriptionName;
    this.GoogleChatApiKey = GoogleChatApiKey;
    this.GoogleClientEmail = GoogleClientEmail;
    this.GoogleClientPrivateKey = GoogleClientPrivateKey;
    this.GoogleProjectId = GoogleProjectId;

    this.pubSubClient = new PubSub({
      projectId: this.GoogleProjectId,
      credentials: { client_email: GoogleClientEmail, private_key: GoogleClientPrivateKey }
    });

  }

  public static async asyncForEach(array, callback) {
    for (let index = 0; index < array.length; index++) {
      await callback(array[index], index, array);
    }
  }

  public async setup(setUrl) {

    this.directLineClient =
      new Swagger({
        spec: JSON.parse(fs.readFileSync('directline-3.0.json', 'utf8')),
        usePromise: true
      });
    const client = await this.directLineClient;

    client.clientAuthorizations.add(
      'AuthorizationBotConnector',
      new Swagger.ApiKeyAuthorization('Authorization', `Bearer ${this.directLineSecret}`, 'header')
    );

    if (setUrl) {
      try {

        const subscription = this.pubSubClient.subscription(this.GoogleChatSubscriptionName);
        subscription.on('message', this.receiver.bind(this));

      } catch (error) {
        GBLog.error(`Error initializing 3rd party GoogleChat provider(1) ${error.message}`);
      }
    }

  }

  public async resetConversationId(key) {
    GoogleChatDirectLine.conversationIds[key] = undefined;
  }

  public async check() {

    GBLog.info(`GBGoogleChat: Checking server...`);
  }

  // TODO: Check service.Users.Messages.List("me").
  public async receiver(message) {

    const event = JSON.parse(Buffer.from(message.data, 'binary').toString());

    let from = '';
    let fromName = '';
    let text;
    const threadName = event.message.thread.name;

    if (event['type'] === 'ADDED_TO_SPACE' && event['space']['singleUserBotDm']) {

    } else if (event['type'] === 'MESSAGE') {
      text = event.message.text;
      fromName = event.message.sender.displayName;
      from = event.message.sender.email;
      GBLog.info(`Received message from ${from} (${fromName}): ${text}.`);
    }
    message.ack();

    const sec = new SecService();
    const user = await sec.ensureUser(this.min.instance.instanceId, from,
      from, '', 'googlechat', fromName, from);

    await sec.updateConversationReferenceById(user.userId, threadName);

    GBLog.info(`GBGoogleChat: RCV ${from}: ${text})`);

    const client = await this.directLineClient;
    const conversationId = GoogleChatDirectLine.conversationIds[from];

    if (GoogleChatDirectLine.conversationIds[from] === undefined) {
      GBLog.info(`GBGoogleChat: Starting new conversation on Bot.`);
      const response = await client.Conversations.Conversations_StartConversation();
      const generatedConversationId = response.obj.conversationId;

      GoogleChatDirectLine.conversationIds[from] = generatedConversationId;

      this.pollMessages(client, generatedConversationId, threadName, from, fromName);
      this.inputMessage(client, generatedConversationId, threadName, text, from, fromName);
    } else {

      this.inputMessage(client, conversationId, threadName, text, from, fromName);
    }
  }

  public inputMessage(client, conversationId, threadName, text, from, fromName) {
    return client.Conversations.Conversations_PostActivity({
      conversationId: conversationId,
      activity: {
        textFormat: 'plain',
        text: text,
        type: 'message',
        mobile: from,
        from: {
          id: from,
          name: fromName
        },
        replyToId: from
      }
    });
  }

  public pollMessages(client, conversationId, threadName, from, fromName) {
    GBLog.info(`GBGoogleChat: Starting message polling(${from}, ${conversationId}).`);

    let watermark: any;

    const worker = async () => {
      try {
        const response = await client.Conversations.Conversations_GetActivities({
          conversationId: conversationId,
          watermark: watermark
        });
        watermark = response.obj.watermark;
        await this.printMessages(response.obj.activities, conversationId, threadName, from, fromName);
      } catch (err) {
        GBLog.error(`Error calling printMessages on GoogleChat channel ${err.data === undefined ? err : err.data}`);
      }
    };
    setInterval(worker, this.pollInterval);
  }

  public async printMessages(activities, conversationId, threadName, from, fromName) {
    if (activities && activities.length) {
      // Ignore own messages.

      activities = activities.filter(m => m.from.id === this.botId && m.type === 'message');

      if (activities.length) {
        // Print other messages.

        await GoogleChatDirectLine.asyncForEach(activities, async activity => {
          await this.printMessage(activity, conversationId, threadName, from, fromName);
        });
      }
    }
  }

  public async printMessage(activity, conversationId, threadName, from, fromName) {
    let output = '';

    if (activity.text) {
      GBLog.info(`GBGoogleChat: SND ${from}(${fromName}): ${activity.text}`);
      output = activity.text;
    }

    if (activity.attachments) {
      activity.attachments.forEach(attachment => {
        switch (attachment.contentType) {
          case 'image/png':
            GBLog.info(`Opening the requested image ${attachment.contentUrl}`);
            output += `\n${attachment.contentUrl}`;
            break;
          default:
            GBLog.info(`Unknown content type: ${attachment.contentType}`);
        }
      });
    }

    await this.sendToDevice(from, conversationId, threadName, output);
  }

  public async sendToDevice(from: string, conversationId: string, threadName, msg: string) {

    try {

      let threadParts = threadName.split('/');
      let spaces = threadParts[1];
      let threadKey = threadParts[3];
      const scopes = ['https://www.googleapis.com/auth/chat.bot'];

      const jwtClient = new google.auth.JWT(
        this.GoogleClientEmail,
        null,
        this.GoogleClientPrivateKey,
        scopes,
        null
      );
      await jwtClient.authorize();
      const chat = google.chat({version: 'v1', auth: jwtClient});
      chat.spaces.messages.createAsync = promisify(chat.spaces.messages.create);

      const res = await chat.spaces.messages.createAsync({
        parent: `spaces/${spaces}`,
        threadKey: threadKey,
        requestBody: {
          text: msg
        }
      });
      GBLog.info(res);
      GBLog.info(`Message [${msg}] sent to ${from}: `);
    } catch (error) {
      GBLog.error(`Error sending message to GoogleChat provider ${error.message}`);
    }
  }


  public async sendToDeviceEx(to, conversationId, threadName, text, locale) {
    const minBoot = GBServer.globals.minBoot as any;

    text = await minBoot.conversationalService.translate(
      minBoot,
      text,
      locale
    );
    await this.sendToDevice(to, conversationId, threadName, text);
  }
}