import { Message } from "discord.js";
import { getRepository } from "typeorm";
import { Guide, GuideType, GuideKeyword } from "@shared/db/entity/Guide";
import { Event, BotEventData, Command, ICommandData, Plugin } from "src/model/plugin";

@Plugin
export class GuideCommands {
    async matchGuide(keywords: string[]): Promise<Guide | null> {
        const a = await getRepository(Guide).query(
            `select guide.*
             from guide
             inner join (select gk."guideId", count("guideKeywordId") as gc
                        from guide_keywords_guide_keyword as gk
                        where
                        gk."guideKeywordId" in    (select id
                                                from guide_keyword
                                                where
                                                guide_keyword.keyword in (${keywords.map((v, i) => `$${i + 1}`).join(",")}))
                        group by gk."guideId") as gks
            on gks."guideId" = guide.id
            order by gc desc
            limit 1`,
            keywords
        ) as Guide[];
        if (a.length == 0)
            return null;
        return a[0];
    }

    async listGuides(msg: Message, guideType: string, message: string): Promise<void> {
        const repo = getRepository(Guide);

        const allGuides = await repo.createQueryBuilder("guide")
            .select(["guide.displayName"])
            .leftJoinAndSelect("guide.keywords", "keyword")
            .where("guide.type = :type", { type: guideType })
            .getMany();

        const MAX_GUIDES_PER_MSG = 30;
        let guides = `${msg.author.toString()} ${message}\n\`\`\``;
        let guideNum = 0;
        for (const guide of allGuides) {
            guides += `${guide.displayName} -- ${guide.keywords.map(k => k.keyword).join(" ")}\n`;
            guideNum++;

            if (guideNum == MAX_GUIDES_PER_MSG) {
                guides += "```";
                await msg.channel.send(guides);
                guides = "```\n";
                guideNum = 0;
            }
        }

        if (guideNum != 0) {
            guides += "```\n\nTo display the guides, ping me with one or more keywords, like `@NoctBot sybaris com`";
            await msg.channel.send(guides);
        }
    }

    @Event("directMention")
    async displayGuide(data: BotEventData, msg: Message, lowerCaseContent: string): Promise<void> {
        if (data.actionsDone)
            return;

        if (msg.attachments.size > 0 || lowerCaseContent.length == 0)
            return;

        const parts = lowerCaseContent.split(" ").map(s => s.trim()).filter(s => s.length != 0);

        const guide = await this.matchGuide(parts);

        if (guide) {
            msg.channel.send(guide.content);
            data.actionsDone = true;
        }
    }

    @Command({
        type: "mention",
        pattern: /^make (\w+)\s+name:(.+)\s*keywords:(.+)\s*contents:((.*[\n\r]*)+)$/i,
        auth: true,
        documentation: {
            description: "Creates a new guide of the specified type, the specified keywords and content.",
            example: "make <GUIDE TYPE> <NEWLINE>name: <NAME> <NEWLINE> keywords: <KEYWORDS> <NEWLINE> contents: <CONTENTS>"
        }
    })
    async makeGuide({ message, contents }: ICommandData): Promise<void> {
        const match = contents as RegExpMatchArray;
        const type = match[1].toLowerCase();
        const name = match[2].trim();
        const keywords = match[3].toLowerCase().split(" ").map(s => s.trim()).filter(s => s.length != 0);
        const msgContents = match[4].trim();

        if (msgContents.length == 0) {
            message.reply("the guide must have some content!");
            return;
        }
        
        if (!(<string[]>Object.values(GuideType)).includes(type)) {
            message.reply(`the type ${type} is not a valid guide type!`);
            return;
        }

        const repo = getRepository(GuideKeyword);
        const guideRepo = getRepository(Guide);

        const existingKeywords = await repo.find({
            where: [
                ...keywords.map(k => ({ keyword: k }))
            ]
        });

        const existingGuide = await this.matchGuide(keywords);

        const addGuide = async () => {
            const newKeywords = new Set<string>();
            const knownKeywords = new Set(existingKeywords.map(e => e.keyword));
            for (const word of keywords) {
                if (!knownKeywords.has(word))
                    newKeywords.add(word);
            }

            const addedKeywords = await repo.save([...newKeywords].map(k => repo.create({
                keyword: k
            })));

            await guideRepo.save(guideRepo.create({
                content: msgContents,
                displayName: name,
                keywords: [...existingKeywords, ...addedKeywords],
                type: type as GuideType
            }));
        };

        if (existingGuide) {
            const guideKeywordsCount = await repo
                .createQueryBuilder("keywords")
                .leftJoinAndSelect("keywords.relatedGuides", "guide")
                .where("guide.id = :id", { id: existingGuide.id })
                .getCount();

            if (guideKeywordsCount == existingKeywords.length)
                await guideRepo.update({ id: existingGuide.id }, {
                    displayName: name,
                    content: msgContents
                });
            else
                await addGuide();
        } else
            await addGuide();

        message.reply(
            `Added/updated "${name}" (keywords \`${keywords.join(" ")}\`)!`
        );
    }

    @Command({
        type: "mention",
        pattern: /^delete (\w+)\s+(.+)$/i,
        auth: true,
        documentation: {
            example: "delete <guidetype> <keywords>",
            description: "Deletes a guide with the specified keywords"
        }
    })
    async deleteGuide({ message, contents }: ICommandData): Promise<void> {
        const match = contents as RegExpMatchArray;
        const type = match[1];
        const keywords = match[2].toLowerCase().split(" ").map(s => s.trim()).filter(s => s.length != 0);

        if (!(<string[]>Object.values(GuideType)).includes(type)) {
            await message.reply(
                `The type ${type} is not a valid guide type!`
            );
            return;
        }

        const dedupedKeywords = [...new Set(keywords)];

        const repo = getRepository(GuideKeyword);
        const guideRepo = getRepository(Guide);
        const existingGuide = await this.matchGuide(keywords);

        if (existingGuide) {
            const guideKeywordsCount = await repo
                .createQueryBuilder("keywords")
                .leftJoinAndSelect("keywords.relatedGuides", "guide")
                .where("guide.id = :id", { id: existingGuide.id })
                .getCount();

            if (guideKeywordsCount == dedupedKeywords.length) {
                await guideRepo.delete({ id: existingGuide.id });
                await message.reply(`removed ${type} "${keywords.join(" ")}"!`);
                return;
            }
        }

        await message.reply(`no such ${type} with keywords \`${keywords.join(" ")}\`! Did you forget to specify all keywords?`);
    }

    @Command({
        type: "mention",
        pattern: "guides", 
        documentation: { 
            description: "Lists all guides and keywords that trigger them.", 
            example: "guides" 
        } 
    })
    async showGuides({ message }: ICommandData): Promise<void> {
        await this.listGuides(message, "guide", "Here are the guides I have:");
    }

    @Command({
        type: "mention",
        pattern: "memes", 
        documentation: {
            description: "Lists all memes and keywords that trigger them.", 
            example: "memes"
        } 
    })
    async showMemes({ message }: ICommandData): Promise<void> {
        await this.listGuides(message, "meme", "Here are some random memes I have:");
    }

    @Command({
        type: "mention",
        pattern: "misc", 
        documentation: {
            description: "Lists all additional keywords the bot reacts to.", 
            example: "misc"
        } 
    })
    async showMisc({ message }: ICommandData): Promise<void> {
        await this.listGuides(message, "misc", "These are some misc stuff I can also do:");
    }
}