import 'reflect-metadata';

import commandLineArgs, { CommandLineOptions } from 'command-line-args';
import * as Discord from 'discord.js';
import _ from 'lodash';
import moment from 'moment';
import { BotORMOptions, BotPlugin } from 'willow-contracts';
import { Guild, GuildUser, PluginExpUser, UserExp, UserLevel, UserRank } from 'willow-models';
import { Between, createConnection, Repository } from 'typeorm';

export const commandOptions = [
    { name: 'exp', alias: 'e', type: Boolean },
    { name: 'rank', alias: 'r', type: Boolean },
    { name: 'help', alias: 'h', type: Boolean }
];

export enum CommandType { Quote, Random }

export default class PsyduckExampleOrm implements BotPlugin {
    public users!: Repository<PluginExpUser>;
    public levels!: Repository<UserLevel>;
    public ranks!: Repository<UserRank>;
    public exps!: Repository<UserExp>;

    public connectionOptions: BotORMOptions;
    public announceLevelUp: { announceChannel: string } | boolean;
    public minExp: number;
    public maxExp: number;

    constructor(options: BotORMOptions, announceLevelUp: { announceChannel: string } | boolean = false, minExp: number = 1, maxExp: number = 10) {
        this.connectionOptions = options;
        this.announceLevelUp = announceLevelUp;
        this.minExp = minExp;
        this.maxExp = maxExp;
    }
    /**
     * This method is to be called after instantiation to connect to the data store. This is intentional to allow for unit testing methods without requiring a connection.
     */
    connect() {
        createConnection({
            type: this.connectionOptions.type as any, // This is a workaround for TypeORM. Although the interfaces match, it is checking design time values.
            host: this.connectionOptions.host,
            port: this.connectionOptions.port,
            username: this.connectionOptions.username,
            password: this.connectionOptions.password,
            database: this.connectionOptions.database,
            synchronize: true,
            logging: false,
            entities: [
                PluginExpUser, UserLevel, UserExp, UserRank
            ]
        }).then(async connection => {
            this.users = connection.getRepository(PluginExpUser);
            this.levels = connection.getRepository(UserLevel);
            this.ranks = connection.getRepository(UserRank);
            this.exps = connection.getRepository(UserExp);
            this.buildLevels();
        }).catch(error => console.log(error));
    }
    async buildLevels() {
        const levels = await this.levels.find();
        if (!levels || levels.length < 1) {
            for (let i = 0; i < 100; i++) {
                const level = new UserLevel();
                level.level = i + 1;
                level.requiredExp = (i + 1) * 1993 - 1185;
                await this.levels.save(level);
            }
        }
    }
    /**
     * This event is emitted from Psyduck when a message is recieved in a discord channel. From there you may do as you with with it in your plugin.
     */
    onMessage(message: Discord.Message) {
        // If the message came from a bot, ignore.
        if (message.author.bot) {
            return;
        }
        // If the message is too small to be a command, ignore.
        if (message.content.length <= 1) {
            this.gainExp(message, message.author.id, (message.channel as Discord.TextChannel).name);
            return;
        }
        // If the message is a string of repeated characters, ignore. For example !!!!!!!
        const dupe = RegExp(/^(.)\1*$/gm);
        if (dupe.test(message.content)) {
            this.gainExp(message, message.author.id, (message.channel as Discord.TextChannel).name);
            return;
        }
        // Extract the command code, if it is in the list of commands we want to handle, continue.
        if (message.content.startsWith('!')) {
            const commandCodes = ['level', 'lvl', 'l'];
            const baseCommand = message.content.slice(1);
            const commandWithArgs = message.content.slice(1, message.content.indexOf(' '));
            if (_.includes(commandCodes, baseCommand)) {
                this.explainLevel(message, (message.channel as Discord.TextChannel).name);
            }
            else if (_.includes(commandCodes, commandWithArgs)) {
            }
        }
        else {
            this.gainExp(message, message.author.id, (message.channel as Discord.TextChannel).name);
        }
    }
    async getUser(userId: string) {
        return await this.users.findOne({
            user: {
                id: userId
            }
        }, { relations: ['exps', 'level'] });
    }
    createUser(guildId: string, serverName: string, guildAvatarUrl: string, userId: string, userName: string, avatarUrl: string) {
        const user = new PluginExpUser();

        const guildUser = new GuildUser();
        guildUser.id = userId;
        guildUser.userName = userName;
        guildUser.avatarUrl = avatarUrl || '';

        const guild = new Guild();
        guild.id = guildId;
        guild.serverName = serverName;
        guild.avatarUrl = guildAvatarUrl || '';

        guildUser.guild = guild;

        user.user = guildUser;
        user.exps = [];
        return user;
    }
    async explainLevel(discord: Discord.Message, channelName: string) {
        let user = await this.getUser(discord.author.id);

        if (!user) {
            user = this.createUser(discord.guild.id, discord.guild.name, discord.guild.iconURL, discord.author.id, discord.author.username, discord.author.avatarURL);
            user.exps.push(this.buildExp(channelName));
            await this.users.save(user);
        }

        let users = await this.users.find({ relations: ['level', 'exps'] });
        users = _.orderBy(users, u => _.sum(u.exps.map(e => e.amount)), ['desc']);

        let currentExp = 0;
        user.exps.forEach(exp => currentExp += exp.amount);

        const nextLevel = await this.levels.findOne({ level: user.level ? user.level.level + 1 : 1 });
        const userIndex = users.findIndex(u => u.user.id === user!.user.id);
        const nextPlaceUser = users[userIndex === 0 ? 0 : userIndex - 1];

        let nextPlaceUserExp = 0;
        let nextPlaceUserName = undefined;
        if (nextPlaceUser) {
            try {
                const du = await discord.guild.fetchMember(nextPlaceUser.user.id);
                nextPlaceUserName = du.user.username;
            } catch (error) {
                console.log(error);
            }
            nextPlaceUser.exps.forEach(exp => nextPlaceUserExp += exp.amount);
        }

        const embed = {
            'color': 11812284,
            'author': {
                'name': '💯 Your Level Report'
            },
            'fields': [
                {
                    'name': 'Overall Rank',
                    'value': `${userIndex + 1}/${users.length}`,
                    'inline': true
                },
                {
                    'name': 'Current Exp',
                    'value': `${currentExp}`,
                    'inline': true
                },
                {
                    'name': 'Current Level',
                    'value': `**${user.level ? user.level.level : 'None'}** (req. *${user.level ? user.level.requiredExp : 0} exp*)`,
                    'inline': true
                },
                {
                    'name': 'Next Level',
                    'value': `**${nextLevel ? nextLevel.level : 'none'}** (req. *${nextLevel ? nextLevel.requiredExp : 0} exp*)`,
                    'inline': true
                },
                {
                    'name': 'Exp To Next Level',
                    'value': `${nextLevel ? nextLevel.requiredExp - currentExp : 0}`,
                    'inline': true
                },
                {
                    'name': 'Exp To Overtake',
                    'value': `${nextPlaceUserExp - currentExp} (*${nextPlaceUserName || nextPlaceUser.user.id}*)`,
                    'inline': true
                }
            ]
        };

        discord.channel.send({ embed });
    }
    buildExp(channel: string): UserExp {
        const exp = new UserExp();
        exp.channel = channel;
        exp.timeStamp = moment().format('ddd MMM DD YYYY h:mm A');
        exp.amount = _.random(this.minExp, this.maxExp);
        return exp;
    }
    async gainExp(discord: Discord.Message, userId: string, channel: string) {
        let user = await this.getUser(userId);
        if (!user) {
            user = this.createUser(discord.guild.id, discord.guild.name, discord.guild.iconURL, discord.author.id, discord.author.username, discord.author.avatarURL);
            await this.users.save(user);
        }
        user.exps.push(this.buildExp(channel));
        await this.users.save(user);
        this.checkGainLevel(discord, user);
    }
    async checkGainLevel(discord: Discord.Message, user: PluginExpUser) {
        let currentExp = 0;
        let leveled = false;
        user.exps.forEach(exp => currentExp += exp.amount);

        const levels = await this.levels.find({ requiredExp: Between(1, currentExp) });
        if (!user.level) {
            leveled = true;
            user.level = levels[0];
            await this.users.save(user);
        } else {
            for (let i = 0; i < levels.length; i++) {
                const level = levels[i];
                if (level.id > user.level.id) {
                    leveled = true;
                    user.level = level;
                    await this.users.save(user);
                    break;
                }
            }
        }
        await this.users.save(user);

        if (leveled) {
            await this.gainLevel(discord, user);
            if (this.announceLevelUp) {
                if (typeof (this.announceLevelUp) === 'boolean') {
                    discord.channel.send(`Level 🆙 <@${user.user.id}> - You are now level ${user.level.level}`);
                } else {
                    const channel = discord.guild.channels.find('id', this.announceLevelUp.announceChannel) as Discord.TextChannel;
                    if (channel) {
                        channel.send(`Level 🆙 <@${user.user.id}> - You are now level ${user.level.level}`);
                    }
                }
            }
        }
    }
    async gainLevel(discord: Discord.Message, user: PluginExpUser) {
        const currentLevel = await this.levels.findOne({ id: user.level.id }, { relations: ['ranks'] });
        if (currentLevel) {
            currentLevel.ranks.forEach(r => {
                this.assignRole(discord, user.user.id, r.rankName);
            });
        }
    }
    async assignRole(discord: Discord.Message, userId: string, roleName: string) {
        try {
            const serverRole = discord.guild.roles.find('name', roleName);
            if (serverRole) {
                const guildUser = await discord.guild.fetchMember(userId);
                if (guildUser && !guildUser.roles.get(serverRole.id)) {
                    guildUser.addRole(serverRole);
                }
            }
        } catch (error) {
        }
    }
    parseParameters(messageContent: string): string[] {
        const params = messageContent.match(/ (?=\S)[^'\s]*(?:'[^\\']*(?:\\[\s\S][^\\']*)*'[^'\s]*)*/g) || [];
        return params.map(p => p.trim());
    }
    hydrateOptions(parameters: string[]): CommandLineOptions {
        return commandLineArgs(commandOptions, { argv: parameters, partial: true });
    }
    extractUserId(userId: string): string | undefined {
        const matches = userId.match(/(?<=@|!)(.*)(?=>)/gm);
        if (matches && matches.length === 1) {
            return matches[0].replace('!', '');
        }
        return undefined;
    }
}