import TurndownService from "turndown";
import interval from "interval-promise";
import { client, FORUMS_DOMAIN } from "../client";
import sha1 from "sha1";
import { TextChannel, Message, ReactionCollector, MessageReaction, User, Channel } from "discord.js";
import { Dict } from "../util";
import { getRepository, Not, IsNull } from "typeorm";
import { PostedForumNewsItem } from "@shared/db/entity/PostedForumsNewsItem";
import { KnownChannel } from "@shared/db/entity/KnownChannel";
import { PostVerifyMessage } from "@shared/db/entity/PostVerifyMessage";
import { render } from "../bbcode-parser/bbcode-js";
import { logger } from "src/logging";
import { Command, ICommandData, Plugin } from "src/model/plugin";

const PREVIEW_CHAR_LIMIT = 300;
const NEWS_POST_VERIFY_CHANNEL = "newsPostVerify";
const NEWS_FEED_CHANNEL = "newsFeed";
const RSS_UPDATE_INTERVAL_MIN = process.env.NODE_ENV == "dev" ? 60 : 5;
const NEWS_FORUM_ID = 49;

@Plugin
export class ForumsNewsChecker {

    verifyMessageIdToPost: Dict<string> = {}
    reactionCollectors: Dict<ReactionCollector> = {};
    verifyChannelId?: string;
    botUserId = 0;
    turndown = new TurndownService();

    constructor() {
        this.turndown.addRule("image", {
            filter: "img",
            replacement: () => ""
        });
        this.turndown.addRule("link", {
            filter: (node: HTMLElement) => node.nodeName === "A" && node.getAttribute("href") != null,
            replacement: (content: string, node: Node) => (node as HTMLElement).getAttribute("htef") ?? ""
        });
    }


    bbCodeToMarkdown(bbCode: string): string {
        const html = render(bbCode).replace(/\n/gm, "</br>");

        return this.turndown.turndown(html).trim();//.replace(/( {2}\n|\n\n){2,}/gm, "\n");
    }

    checkFeeds = async (): Promise<void> => {
        try {
            logger.info(`Checking feeds on ${new Date().toISOString()}`);
            const forumsNewsRepo = getRepository(PostedForumNewsItem);
            const postVerifyMessageRepo = getRepository(PostVerifyMessage);

            const forumThreads = await client.forum.getForumThreads(NEWS_FORUM_ID);

            for (const thread of [...forumThreads.threads, ...forumThreads.sticky]) {
                const firstPost = await client.forum.getPost(thread.first_post_id);

                const contents = this.bbCodeToMarkdown(firstPost.message);
                let itemObj = forumsNewsRepo.create({
                    id: thread.thread_id.toString(),
                    hash: sha1(firstPost.message),
                    verifyMessage: postVerifyMessageRepo.create({
                        author: thread.username,
                        link: `${FORUMS_DOMAIN}/index.php?threads/${thread.thread_id}/`,
                        title: thread.title,
                        text: `${contents.substr(0, Math.min(contents.length, PREVIEW_CHAR_LIMIT))}...`,
                        isNew: true
                    })
                });

                const postItem = await forumsNewsRepo.findOne({
                    where: { id: itemObj.id },
                    relations: ["verifyMessage"]
                });

                if (postItem) {

                    if (process.env.IGNORE_CHANGED_NEWS === "TRUE") {
                        await forumsNewsRepo.update({
                            id: postItem.id
                        }, {
                            hash: itemObj.hash
                        });
                        continue;
                    }

                    // Add message ID to mark for edit
                    if (postItem.hash != itemObj.hash) {
                        const newHash = itemObj.hash;
                        if (!postItem.verifyMessage)
                            postItem.verifyMessage = itemObj.verifyMessage;

                        itemObj = postItem;
                        if (itemObj.verifyMessage)
                            itemObj.verifyMessage.isNew = false;
                        itemObj.hash = newHash;
                    }
                    else
                        continue;
                }

                if (!this.verifyChannelId || firstPost.user_id == this.botUserId)
                    await this.sendNews(itemObj);
                else
                    await this.addVerifyMessage(itemObj);
            }
        } catch (err) {
            logger.error("Failed to check forums: %s", err);
        }
    }

    async initPendingReactors(): Promise<void> {
        if (!this.verifyChannelId)
            return;

        const verifyChannel = client.bot.channels.resolve(this.verifyChannelId);

        if (!verifyChannel)
            return;

        const repo = getRepository(PostedForumNewsItem);
        const verifyMessageRepo = getRepository(PostVerifyMessage);

        const pendingVerifyMessages = await repo.find({
            where: { verifyMessage: Not(IsNull()) },
            select: ["id"],
            relations: ["verifyMessage"]
        });

        for (const msg of pendingVerifyMessages) {
            // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
            const m = await this.tryFetchMessage(verifyChannel, msg.verifyMessage!.messageId);

            if (!m) {
                await repo.update({ id: msg.id }, { verifyMessage: undefined });
                // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
                await verifyMessageRepo.delete(msg.verifyMessage!);
                continue;
            }

            const collector = m.createReactionCollector(this.isVerifyReaction, { maxEmojis: 1 });
            collector.on("collect", this.collectorFor(collector));
            this.reactionCollectors[m.id] = collector;
            this.verifyMessageIdToPost[m.id] = msg.id;
        }
    }

    async addVerifyMessage(item: PostedForumNewsItem): Promise<void> {
        if (!this.verifyChannelId)
            return;
        if (!item.verifyMessage)
            throw new Error("No verify message! This shouldn't happen!");

        const verifyChannel = client.bot.channels.resolve(this.verifyChannelId) as TextChannel;

        if (!verifyChannel) {
            logger.warn(`Skipping adding item ${item.id} because no verify channel is set up!`);
            return;
        }

        const verifyMessageRepo = getRepository(PostVerifyMessage);
        const forumsNewsRepo = getRepository(PostedForumNewsItem);

        if (item.verifyMessage.messageId) {
            const oldMessage = await this.tryFetchMessage(verifyChannel, item.verifyMessage.messageId);
            if (oldMessage)
                await oldMessage.delete();
        }

        const newMessage = await verifyChannel.send(this.toVerifyString(item.id, item.verifyMessage)) as Message;
        item.verifyMessage.messageId = newMessage.id;

        await newMessage.react("✅");
        await newMessage.react("❌");

        const collector = newMessage.createReactionCollector(this.isVerifyReaction, { maxEmojis: 1 });
        collector.on("collect", this.collectorFor(collector));
        this.reactionCollectors[newMessage.id] = collector;
        this.verifyMessageIdToPost[newMessage.id] = item.id;

        item.verifyMessage = await verifyMessageRepo.save(item.verifyMessage);
        await forumsNewsRepo.save(item);
    }

    collectorFor = (collector: ReactionCollector) =>
        async (reaction: MessageReaction): Promise<void> => {
            const verifyMessageRepo = getRepository(PostVerifyMessage);
            const postRepo = getRepository(PostedForumNewsItem);
    
            const m = reaction.message;
            collector.stop();
            delete this.reactionCollectors[m.id];
            const postId = this.verifyMessageIdToPost[m.id];
    
            const post = await postRepo.findOne({
                where: { id: postId },
                relations: ["verifyMessage"]
            });
    
            if (!post) 
                throw new Error("Post not found!");
    
            await postRepo.update({ id: post.id }, { verifyMessage: undefined });
            if (post.verifyMessage)
                await verifyMessageRepo.delete({ id: post.verifyMessage.id });
            await reaction.message.delete();
    
            if (reaction.emoji.name == "✅")
                this.sendNews(post);
    
            delete this.verifyMessageIdToPost[m.id];
        };

    async sendNews(item: PostedForumNewsItem): Promise<void> {
        const channelRepo = getRepository(KnownChannel);
        const newsPostRepo = getRepository(PostedForumNewsItem);

        const outChannel = await channelRepo.findOne({
            where: { channelType: NEWS_FEED_CHANNEL }
        });

        if (!outChannel)
            return;

        const sentMessage = await this.postNewsItem(outChannel.channelId, item);

        item.postedMessageId = sentMessage?.id;
        item.verifyMessage = undefined;

        await newsPostRepo.save(item);
    }

    isVerifyReaction = (reaction: MessageReaction, user: User): boolean => {
        return (reaction.emoji.name == "✅" || reaction.emoji.name == "❌") && !user.bot;
    }

    async tryFetchMessage(channel?: Channel, messageId?: string): Promise<Message | null> {
        if(!channel || !messageId)
            return null;
        try {
            if (!(channel instanceof TextChannel))
                return null;
            return await channel.messages.fetch(messageId);
        } catch (error) {
            return null;
        }
    }

    get shouldVerify(): boolean {
        return this.verifyChannelId != undefined;
    }

    async postNewsItem(channel: string, item: PostedForumNewsItem): Promise<Message | null> {
        if (!item.verifyMessage)
            throw new Error("No message to send!");

        const newsMessage = this.toNewsString(item.verifyMessage);
        const ch = client.bot.channels.resolve(channel);

        if (!(ch instanceof TextChannel))
            return null;

        if (item.postedMessageId) {
            const message = await this.tryFetchMessage(ch, item.postedMessageId);
            if (message)
                return await message.edit(newsMessage);
            else
                return await ch.send(newsMessage) as Message;
        }
        else
            return await ch.send(newsMessage) as Message;
    }

    toNewsString(item: PostVerifyMessage): string {
        return `**${item.title}**
Posted by ${item.author}
${item.link}
    
${item.text}`;
    }

    toVerifyString(postId: string, item: PostVerifyMessage): string {
        return `[${item.isNew ? "🆕 ADD" : "✏️ EDIT"}]
Post ID: **${postId}**
        
${this.toNewsString(item)}
                
React with ✅ (approve) or ❌ (deny).`;
    }

    @Command({
        type: "mention",
        pattern: /^edit (\d+)\s+((.*[\n\r]*)+)$/i
    })
    async editPreview({ message, contents }: ICommandData): Promise<void> {
        if (message.channel.id != this.verifyChannelId)
            return;
        
        const match = contents as RegExpMatchArray;
        const id = match[1];
        const newContents = match[2].trim();
        const repo = getRepository(PostedForumNewsItem);
        const verifyRepo = getRepository(PostVerifyMessage);

        const post = await repo.findOne({
            where: { id: id },
            relations: ["verifyMessage"]
        });

        if (!post || !post.verifyMessage) {
            message.reply(`no unapproved news items with id ${id}!`);
            return;
        }

        const editMsg = await this.tryFetchMessage(client.bot.channels.resolve(this.verifyChannelId) ?? undefined, post.verifyMessage.messageId);

        if (!editMsg) {
            message.reply(`no verify message found for ${id}! This is a bug: report to horse.`);
            return;
        }

        post.verifyMessage.text = newContents;

        await verifyRepo.save(post.verifyMessage);
        await editMsg.edit(this.toVerifyString(post.id, post.verifyMessage));
        await message.delete();
    }

    async start(): Promise<void> {
        const repo = getRepository(KnownChannel);

        const verifyChannel = await repo.findOne({
            channelType: NEWS_POST_VERIFY_CHANNEL
        });

        if (!verifyChannel)
            return;

        this.verifyChannelId = verifyChannel.channelId;

        const user = await client.forum.getMe();
        this.botUserId = user.user_id;

        await this.initPendingReactors();
        interval(this.checkFeeds, RSS_UPDATE_INTERVAL_MIN * 60 * 1000);
    }
}