/**
 * @module botbuilder-adapter-hangouts
 */
/**
 * Copyright (c) Microsoft Corporation. All rights reserved.
 * Licensed under the MIT License.
 */

import { Activity, ActivityTypes, BotAdapter, TurnContext, ConversationReference, ResourceResponse } from 'botbuilder';
import * as Debug from 'debug';
import { google } from 'googleapis';
import { HangoutsBotWorker } from './botworker';
const debug = Debug('botkit:hangouts');

const apiVersion = 'v1';

/**
 * Connect [Botkit](https://www.npmjs.com/package/botkit) or [BotBuilder](https://www.npmjs.com/package/botbuilder) to Google Hangouts
 *
 */
export class HangoutsAdapter extends BotAdapter {
    /**
     * Name used by Botkit plugin loader
     * @ignore
     */
    public name = 'Google Hangouts Adapter';
    /**
     * Object containing one or more Botkit middlewares to bind automatically.
     * @ignore
     */
    public middlewares;
    /**
     * A customized BotWorker object that exposes additional utility methods.
     * @ignore
     */
    public botkit_worker = HangoutsBotWorker;

    /**
     * Location of configuration options.
     */
    private options: HangoutsAdapterOptions;

    /**
     * A copy of the Google Chat client.
     */
    private api: any;

    /**
     * Create an adapter to handle incoming messages from Google Hangouts and translate them into a standard format for processing by your bot.
     *
     * Use with Botkit:
     *```javascript
     * const adapter = new HangoutsAdapter({
     *      token: process.env.GOOGLE_TOKEN,
     *      google_auth_params: {
     *          credentials: process.env.GOOGLE_CREDS
     *      }
     * });
     * const controller = new Botkit({
     *      adapter: adapter,
     *      // ... other configuration options
     * });
     * ```
     *
     * Use with BotBuilder:
     *```javascript
     * const adapter = new HangoutsAdapter({
     *      token: process.env.GOOGLE_TOKEN,
     *      google_auth_params: {
     *          credentials: process.env.GOOGLE_CREDS
     *      }
     * });
     * // set up restify...
     * const server = restify.createServer();
     * server.use(restify.plugins.bodyParser());
     * server.post('/api/messages', (req, res) => {
     *      adapter.processActivity(req, res, async(context) => {
     *          // do your bot logic here!
     *      });
     * });
     * ```
     *
     * @param options An object containing API credentials and a webhook verification token
     */
    public constructor(options: HangoutsAdapterOptions) {
        super();

        this.options = options;

        if (!this.options.token) {
            const warning = [
                '',
                '****************************************************************************************',
                '* WARNING: Your bot is operating without recommended security mechanisms in place.     *',
                '* Initialize your adapter with a token parameter to enable                             *',
                '* verification that all incoming webhooks originate with Google:                       *',
                '*                                                                                      *',
                '* var adapter = new HangoutsAdapter({token: <my secret from Google>});                 *',
                '*                                                                                      *',
                '****************************************************************************************',
                '>> Official docs: https://developers.google.com/hangouts/chat/how-tos/bots-develop?hl=en_US#verifying_bot_authenticity',
                ''
            ];
            console.warn(warning.join('\n'));
            if (!this.options.enable_incomplete) {
                throw new Error('Required: include a verificationToken or clientSigningSecret to verify incoming Events API webhooks');
            }
        }

        const params = {
            scopes: 'https://www.googleapis.com/auth/chat.bot',
            ...this.options.google_auth_params
        };

        google
            .auth
            .getClient(params)
            .then(client => {
                this.api = google.chat({ version: apiVersion, auth: client });
            })
            .catch(err => {
                console.error('Could not get google auth client !');
                if (!this.options.enable_incomplete) {
                    throw new Error(err);
                }
            });

        if (this.options.enable_incomplete) {
            const warning = [
                '',
                '****************************************************************************************',
                '* WARNING: Your adapter may be running with an incomplete/unsafe configuration.        *',
                '* - Ensure all required configuration options are present                              *',
                '* - Disable the "enable_incomplete" option!                                            *',
                '****************************************************************************************',
                ''
            ];
            console.warn(warning.join('\n'));
        }

        this.middlewares = {
            spawn: [
                async (bot, next): Promise<void> => {
                    bot.api = this.api;
                    next();
                }
            ]
        };
    }

    /**
     * Formats a BotBuilder activity into an outgoing Hangouts event.
     * @param activity A BotBuilder Activity object
     */
    private activityToHangouts(activity: Activity): any {
        const message = {
            parent: activity.conversation.id,
            // @ts-ignore Ignore the presence of this unofficial field
            threadKey: activity.conversation.threadKey || null,
            requestBody: {
                text: activity.text,
                // @ts-ignore Ignore the presence of this unofficial field
                thread: activity.conversation.thread ? { name: activity.conversation.thread } : null
            }
        };

        // if channelData is specified, overwrite any fields in message object
        if (activity.channelData) {
            Object.keys(activity.channelData).forEach(function(key) {
                message.requestBody[key] = activity.channelData[key];
            });
        }

        debug('OUT TO HANGOUTS > ', message);

        return message;
    }

    /**
     * Standard BotBuilder adapter method to send a message from the bot to the messaging API.
     * [BotBuilder reference docs](https://docs.microsoft.com/en-us/javascript/api/botbuilder-core/botadapter?view=botbuilder-ts-latest#sendactivities).
     * @param context A TurnContext representing the current incoming message and environment. (Not used)
     * @param activities An array of outgoing activities to be sent back to the messaging API.
     */
    public async sendActivities(context: TurnContext, activities: Partial<Activity>[]): Promise<ResourceResponse[]> {
        const responses = [];
        for (let a = 0; a < activities.length; a++) {
            const activity = activities[a] as Activity;
            if (activity.type === ActivityTypes.Message) {
                const message = this.activityToHangouts(activity);
                try {
                    const res = await this.api.spaces.messages.create(message);
                    responses.push({ id: res.data.name });
                } catch (err) {
                    console.error('Error sending activity to API:', err);
                }
            } else {
                // If there are ever any non-message type events that need to be sent, do it here.
                debug('Unknown message type encountered in sendActivities: ', activity.type);
            }
        }

        return responses;
    }

    /**
     * Standard BotBuilder adapter method to update a previous message with new content.
     * [BotBuilder reference docs](https://docs.microsoft.com/en-us/javascript/api/botbuilder-core/botadapter?view=botbuilder-ts-latest#updateactivity).
     * @param context A TurnContext representing the current incoming message and environment. (Not used)
     * @param activity The updated activity in the form `{id: <id of activity to update>, text: <updated text>, cards?: [<array of updated hangouts cards>]}`
     */
    public async updateActivity(context: TurnContext, activity: Partial<Activity>): Promise<void> {
        if (activity.id) {
            try {
                const results = await this.api.spaces.messages.update({
                    name: activity.id,
                    updateMask: 'text,cards',
                    resource: {
                        text: activity.text,
                        // @ts-ignore allow cards field
                        cards: activity.cards ? activity.cards : (activity.channelData ? activity.channelData.cards : null)
                    }
                });

                if (!results || results.status !== 200) {
                    throw new Error('updateActivity failed: ' + results.statusText);
                }
            } catch (err) {
                console.error('Error updating activity on Hangouts.');
                throw (err);
            }
        } else {
            throw new Error('Cannot update activity: activity is missing id');
        }
    }

    /**
     * Standard BotBuilder adapter method to delete a previous message.
     * [BotBuilder reference docs](https://docs.microsoft.com/en-us/javascript/api/botbuilder-core/botadapter?view=botbuilder-ts-latest#deleteactivity).
     * @param context A TurnContext representing the current incoming message and environment. (Not used)
     * @param reference An object in the form `{activityId: <id of message to delete>}`
     */
    public async deleteActivity(context: TurnContext, reference: Partial<ConversationReference>): Promise<void> {
        if (reference.activityId) {
            try {
                const results = await this.api.spaces.messages.delete({
                    name: reference.activityId
                });
                if (!results || results.status !== 200) {
                    throw new Error('deleteActivity failed: ' + results.statusText);
                }
            } catch (err) {
                console.error('Error deleting activity', err);
                throw err;
            }
        } else {
            throw new Error('Cannot delete activity: reference is missing activityId');
        }
    }

    /**
     * Standard BotBuilder adapter method for continuing an existing conversation based on a conversation reference.
     * [BotBuilder reference docs](https://docs.microsoft.com/en-us/javascript/api/botbuilder-core/botadapter?view=botbuilder-ts-latest#continueconversation)
     * @param reference A conversation reference to be applied to future messages.
     * @param logic A bot logic function that will perform continuing action in the form `async(context) => { ... }`
     */
    public async continueConversation(reference: Partial<ConversationReference>, logic: (context: TurnContext) => Promise<void>): Promise<void> {
        const request = TurnContext.applyConversationReference(
            { type: 'event', name: 'continueConversation' },
            reference,
            true
        );
        const context = new TurnContext(this, request);

        return this.runMiddleware(context, logic);
    }

    /**
     * Accept an incoming webhook request and convert it into a TurnContext which can be processed by the bot's logic.
     * @param req A request object from Restify or Express
     * @param res A response object from Restify or Express
     * @param logic A bot logic function in the form `async(context) => { ... }`
     */
    public async processActivity(req, res, logic: (context: TurnContext) => Promise<void>): Promise<void> {
        const event = req.body;

        debug('IN FROM HANGOUTS >', event);

        if (this.options.token && this.options.token !== event.token) {
            res.status(401);
            debug('Token verification failed, Ignoring message');
        } else {
            const activity: Activity = {
                id: event.message ? event.message.name : event.eventTime,
                timestamp: new Date(),
                channelId: 'googlehangouts',
                conversation: {
                    id: event.space.name,
                    // @ts-ignore
                    thread: (event.message && !event.threadKey) ? event.message.thread.name : null,
                    // @ts-ignore
                    threadKey: event.threadKey || null
                },
                from: {
                    id: event.user.name,
                    name: event.user.name
                },
                channelData: event,
                text: event.message ? (event.message.argumentText ? event.message.argumentText.trim() : '') : '',
                type: event.message ? ActivityTypes.Message : ActivityTypes.Event
            };

            // Change type of message event for private messages
            if (event.space.type === 'DM') {
                activity.channelData.botkitEventType = 'direct_message';
            }

            if (event.type === 'ADDED_TO_SPACE') {
                activity.type = ActivityTypes.Event;
                activity.channelData.botkitEventType = event.space.type === 'ROOM' ? 'bot_room_join' : 'bot_dm_join';
            }

            if (event.type === 'REMOVED_FROM_SPACE') {
                activity.type = ActivityTypes.Event;
                activity.channelData.botkitEventType = event.space.type === 'ROOM' ? 'bot_room_leave' : 'bot_dm_leave';
            }

            if (event.type === 'CARD_CLICKED') {
                activity.type = ActivityTypes.Event;
                activity.channelData.botkitEventType = event.type.toLowerCase();
            }

            // create a conversation reference
            const context = new TurnContext(this, activity);

            if (event.type !== 'CARD_CLICKED') {
                // send 200 status immediately, otherwise
                // hangouts does not mark the incoming message as received
                res.status(200);
                res.end();
            } else {
                context.turnState.set('httpStatus', 200);
            }

            await this.runMiddleware(context, logic);

            if (event.type === 'CARD_CLICKED') {
                // send http response back
                res.status(context.turnState.get('httpStatus'));
                if (context.turnState.get('httpBody')) {
                    res.send(context.turnState.get('httpBody'));
                } else {
                    res.end();
                }
            }
        }
    }
}

export interface HangoutsAdapterOptions {
    /**
     * Parameters passed to the [Google API client library](https://www.npmjs.com/package/googleapis) which is in turn used to send messages.
     * Define credentials per [the GoogleAuthOptions defined here](https://github.com/googleapis/google-auth-library-nodejs/blob/master/src/auth/googleauth.ts#L54),
     * OR, specify GOOGLE_APPLICATION_CREDENTIALS in environment [as described in the Google docs](https://cloud.google.com/docs/authentication/getting-started).
     */
    google_auth_params?: {
        client_email?: string;
        private_key?: string;
    };
    /**
     * Shared secret token used to validate the origin of incoming webhooks.
     * Get this from the [Google API console for your bot app](https://console.cloud.google.com/apis/api/chat.googleapis.com/hangouts-chat) - it is found on the Configuration tab under the heading "Verification Token".
     * If defined, the origin of all incoming webhooks will be validated and any non-matching requests will be rejected.
     */
    token: string; // webhook validation token

    /**
     * Allow the adapter to startup without a complete configuration.
     * This is risky as it may result in a non-functioning or insecure adapter.
     * This should only be used when getting started.
     */
    enable_incomplete?: boolean;

}
