import { DiscordBot } from "./bot";
import { Log } from "./log";
import { DiscordBridgeConfig } from "./config";
import { Bridge, BridgeContext } from "matrix-appservice-bridge";
import { IMatrixEvent } from "./matrixtypes";
import { Provisioner } from "./provisioner";
import { Util } from "./util";
import * as Discord from "discord.js";
const log = new Log("MatrixCommandHandler");

/* tslint:disable:no-magic-numbers */
const PROVISIONING_DEFAULT_POWER_LEVEL = 50;
const PROVISIONING_DEFAULT_USER_POWER_LEVEL = 0;
const ROOM_CACHE_MAXAGE_MS = 15 * 60 * 1000;
/* tslint:enable:no-magic-numbers */

export class MatrixCommandHandler {
    private botJoinedRooms: Set<string> = new Set(); // roomids
    private botJoinedRoomsCacheUpdatedAt = 0;
    private provisioner: Provisioner;
    constructor(
        private discord: DiscordBot,
        private bridge: Bridge,
        private config: DiscordBridgeConfig,
    ) {
        this.provisioner = this.discord.Provisioner;
    }

    public async HandleInvite(event: IMatrixEvent) {
        log.info(`Received invite for ${event.state_key} in room ${event.room_id}`);
        if (event.state_key === this.discord.GetBotId()) {
            log.info("Accepting invite for bridge bot");
            await this.bridge.getIntent().joinRoom(event.room_id);
            this.botJoinedRooms.add(event.room_id);
        }
    }

    public async Process(event: IMatrixEvent, context: BridgeContext) {
        const intent = this.bridge.getIntent();
        if (!(await this.isBotInRoom(event.room_id))) {
            log.warn(`Bot is not in ${event.room_id}. Ignoring command`);
            return;
        }

        if (!this.config.bridge.enableSelfServiceBridging) {
            // We can do this here because the only commands we support are self-service bridging
            return this.bridge.getIntent().sendMessage(event.room_id, {
                body: "The owner of this bridge does not permit self-service bridging.",
                msgtype: "m.notice",
            });
        }

        // Check to make sure the user has permission to do anything in the room. We can do this here
        // because the only commands we support are self-service commands (which therefore require some
        // level of permissions)
        const plEvent = await this.bridge.getIntent().getClient()
            .getStateEvent(event.room_id, "m.room.power_levels", "");
        let userLevel = PROVISIONING_DEFAULT_USER_POWER_LEVEL;
        let requiredLevel = PROVISIONING_DEFAULT_POWER_LEVEL;
        if (plEvent && plEvent.state_default) {
            requiredLevel = plEvent.state_default;
        }
        if (plEvent && plEvent.users_default) {
            userLevel = plEvent.users_default;
        }
        if (plEvent && plEvent.users && plEvent.users[event.sender]) {
            userLevel = plEvent.users[event.sender];
        }

        if (userLevel < requiredLevel) {
            return this.bridge.getIntent().sendMessage(event.room_id, {
                body: "You do not have the required power level in this room to create a bridge to a Discord channel.",
                msgtype: "m.notice",
            });
        }

        const {command, args} = Util.MsgToArgs(event.content!.body as string, "!discord");

        if (command === "help" && args[0] === "bridge") {
            const link = Util.GetBotLink(this.config);
            // tslint:disable prefer-template
            return this.bridge.getIntent().sendMessage(event.room_id, {
                body: "How to bridge a Discord guild:\n" +
                "1. Invite the bot to your Discord guild using this link: " + link + "\n" +
                "2. Invite me to the matrix room you'd like to bridge\n" +
                "3. Open the Discord channel you'd like to bridge in a web browser\n" +
                "4. In the matrix room, send the message `!discord bridge <guild id> <channel id>` " +
                "(without the backticks)\n" +
                "   Note: The Guild ID and Channel ID can be retrieved from the URL in your web browser.\n" +
                "   The URL is formatted as https://discordapp.com/channels/GUILD_ID/CHANNEL_ID\n" +
                "5. Enjoy your new bridge!",
                msgtype: "m.notice",
            });
            // tslint:enable prefer-template
        } else if (command === "bridge") {
            if (context.rooms.remote) {
                return this.bridge.getIntent().sendMessage(event.room_id, {
                    body: "This room is already bridged to a Discord guild.",
                    msgtype: "m.notice",
                });
            }

            const MAXARGS = 2;
            if (args.length > MAXARGS || args.length < 1) {
                return this.bridge.getIntent().sendMessage(event.room_id, {
                    body: "Invalid syntax. For more information try !discord help bridge",
                    msgtype: "m.notice",
                });
            }

            let guildId: string;
            let channelId: string;

            const AMOUNT_OF_IDS_DISCORD_IDENTIFIES_ROOMS_BY = 2;

            if (args.length === AMOUNT_OF_IDS_DISCORD_IDENTIFIES_ROOMS_BY) { // "x y" syntax
                guildId = args[0];
                channelId = args[1];
            } else if (args.length === 1 && args[0].includes("/")) { // "x/y" syntax
                const split = args[0].split("/");
                guildId = split[0];
                channelId = split[1];
            } else {
                return this.bridge.getIntent().sendMessage(event.room_id, {
                    body: "Invalid syntax: See `!discord help`",
                    formatted_body: "Invalid syntax: See <code>!discord help</code>",
                    msgtype: "m.notice",
                });
            }

            try {
                const discordResult = await this.discord.LookupRoom(guildId, channelId);
                const channel = discordResult.channel as Discord.TextChannel;

                log.info(`Bridging matrix room ${event.room_id} to ${guildId}/${channelId}`);
                this.bridge.getIntent().sendMessage(event.room_id, {
                    body: "I'm asking permission from the guild administrators to make this bridge.",
                    msgtype: "m.notice",
                });

                await this.provisioner.AskBridgePermission(channel, event.sender);
                await this.provisioner.BridgeMatrixRoom(channel, event.room_id);
                return this.bridge.getIntent().sendMessage(event.room_id, {
                    body: "I have bridged this room to your channel",
                    msgtype: "m.notice",
                });
            } catch (err) {
                if (err.message === "Timed out waiting for a response from the Discord owners"
                    || err.message === "The bridge has been declined by the Discord guild") {
                    return this.bridge.getIntent().sendMessage(event.room_id, {
                        body: err.message,
                        msgtype: "m.notice",
                    });
                }

                log.error(`Error bridging ${event.room_id} to ${guildId}/${channelId}`);
                log.error(err);
                return this.bridge.getIntent().sendMessage(event.room_id, {
                    body: "There was a problem bridging that channel - has the guild owner approved the bridge?",
                    msgtype: "m.notice",
                });
            }
        } else if (command === "unbridge") {
            const remoteRoom = context.rooms.remote;

            if (!remoteRoom) {
                return this.bridge.getIntent().sendMessage(event.room_id, {
                    body: "This room is not bridged.",
                    msgtype: "m.notice",
                });
            }

            if (!remoteRoom.data.plumbed) {
                return this.bridge.getIntent().sendMessage(event.room_id, {
                    body: "This room cannot be unbridged.",
                    msgtype: "m.notice",
                });
            }

            try {
                await this.provisioner.UnbridgeRoom(remoteRoom);
                return this.bridge.getIntent().sendMessage(event.room_id, {
                    body: "This room has been unbridged",
                    msgtype: "m.notice",
                });
            } catch (err) {
                log.error("Error while unbridging room " + event.room_id);
                log.error(err);
                return this.bridge.getIntent().sendMessage(event.room_id, {
                    body: "There was an error unbridging this room. " +
                      "Please try again later or contact the bridge operator.",
                    msgtype: "m.notice",
                });
            }
        } else if (command === "help") {
            // Unknown command or no command given to get help on, so we'll just give them the help
            // tslint:disable prefer-template
            return this.bridge.getIntent().sendMessage(event.room_id, {
                body: "Available commands:\n" +
                "!discord bridge <guild id> <channel id>   - Bridges this room to a Discord channel\n" +
                "!discord unbridge                         - Unbridges a Discord channel from this room\n" +
                "!discord help <command>                   - Help menu for another command. Eg: !discord help bridge\n",
                msgtype: "m.notice",
            });
            // tslint:enable prefer-template
        }
    }

    private async isBotInRoom(roomId: string): Promise<boolean> {
        // Update the room cache, if not done already.
        if (Date.now () - this.botJoinedRoomsCacheUpdatedAt > ROOM_CACHE_MAXAGE_MS) {
            log.verbose("Updating room cache for bot...");
            try {
                log.verbose("Got new room cache for bot");
                this.botJoinedRoomsCacheUpdatedAt = Date.now();
                const rooms = (await this.bridge.getBot().getJoinedRooms()) as string[];
                this.botJoinedRooms = new Set(rooms);
            } catch (e) {
                log.error("Failed to get room cache for bot, ", e);
                return false;
            }
        }
        return this.botJoinedRooms.has(roomId);
    }
}