import { BRIDGED_MESSAGES, BRIDGE_CONFIG, logger } from "..";
import { client } from "./client";
import { AUTUMN_URL, client as revoltClient } from "../revolt/client";
import axios from 'axios';
import { ulid } from "ulid";
import GenericEmbed from "../types/GenericEmbed";
import FormData from 'form-data';
import { discordFetchUser, revoltFetchMessage } from "../util";
import { TextChannel } from "discord.js";
import { smartReplace } from "smart-replace";
import { metrics } from "../metrics";

const MAX_BRIDGED_FILE_SIZE = 8_000_000; // 8 MB
const RE_MENTION_USER = /<@!*[0-9]+>/g;
const RE_MENTION_CHANNEL = /<#[0-9]+>/g;
const RE_EMOJI = /<(a?)?:\w+:\d{18}?>/g;
const RE_TENOR = /^https:\/\/tenor.com\/view\/[^\s]+$/g;
const RE_TENOR_META = /<meta class="dynamic" property="og:url" content="[^\s]+">/g

client.on('messageDelete', async message => {
    try {
        logger.debug(`[D] Discord: ${message.id}`);

        const [ bridgeCfg, bridgedMsg ] = await Promise.all([
            BRIDGE_CONFIG.findOne({ discord: message.channelId }),
            BRIDGED_MESSAGES.findOne({ "discord.messageId": message.id }),
        ]);

        if (!bridgedMsg?.revolt) return logger.debug(`Discord: Message has not been bridged; ignoring deletion`);
        if (!bridgeCfg?.revolt) return logger.debug(`Discord: No Revolt channel associated`);

        const targetMsg = await revoltFetchMessage(bridgedMsg.revolt.messageId, revoltClient.channels.get(bridgeCfg.revolt));
        if (!targetMsg) return logger.debug(`Discord: Could not fetch message from Revolt`);

        await targetMsg.delete();
        metrics.messages.inc({ source: 'discord', type: 'delete' });
    } catch(e) {
        console.error(e);
    }
});

client.on('messageUpdate', async (oldMsg, newMsg) => {
    if (oldMsg.content && newMsg.content == oldMsg.content) return; // Let's not worry about embeds here for now

    try {
        logger.debug(`[E] Discord: ${newMsg.content}`);

        const [ bridgeCfg, bridgedMsg ] = await Promise.all([
            BRIDGE_CONFIG.findOne({ discord: newMsg.channel.id }),
            BRIDGED_MESSAGES.findOne({ "discord.messageId": newMsg.id }),
        ]);

        if (!bridgedMsg) return logger.debug(`Discord: Message has not been bridged; ignoring edit`);
        if (!bridgeCfg?.revolt) return logger.debug(`Discord: No Revolt channel associated`);
        if (newMsg.webhookId && newMsg.webhookId == bridgeCfg.discordWebhook?.id) {
            return logger.debug(`Discord: Message was sent by bridge; ignoring edit`);
        }

        const targetMsg = await revoltFetchMessage(bridgedMsg.revolt.messageId, revoltClient.channels.get(bridgeCfg.revolt));
        if (!targetMsg) return logger.debug(`Discord: Could not fetch message from Revolt`);

        await targetMsg.edit({ content: newMsg.content ? await renderMessageBody(newMsg.content) : undefined });
        metrics.messages.inc({ source: 'discord', type: 'edit' });
    } catch(e) {
        console.error(e);
    }
});

client.on('messageCreate', async message => {
    try {
        logger.debug(`[M] Discord: ${message.content}`);
        const [ bridgeCfg, bridgedReply ] = await Promise.all([
            BRIDGE_CONFIG.findOne({ discord: message.channelId }),
            (message.reference?.messageId
                ? BRIDGED_MESSAGES.findOne({ "discord.messageId": message.reference.messageId })
                : undefined
            ),
        ]);

        if (message.webhookId && bridgeCfg?.discordWebhook?.id == message.webhookId) {
            return logger.debug(`Discord: Message has already been bridged; ignoring`);
        }
        if (!bridgeCfg?.revolt) return logger.debug(`Discord: No Revolt channel associated`);

        const channel = revoltClient.channels.get(bridgeCfg.revolt);
        if (!channel) return logger.debug(`Discord: Cannot find associated channel`);

        if (!(channel.havePermission('SendMessage'))) {
            return logger.debug(`Discord: Lacking SendMessage permission; refusing to send`);
        }

        for (const perm of [ 'SendEmbeds', 'UploadFiles', 'Masquerade' ]) {
            if (!(channel.havePermission(perm as any))) {
                // todo: maybe don't spam this on every message?
                await channel.sendMessage(`Missing permission: I don't have the \`${perm}\` permission `
                    + `which is required to bridge a message sent by \`${message.author.tag}\` on Discord.`);
                return logger.debug(`Discord: Lacking ${perm} permission; refusing to send`);
            }
        }

        // Setting a known nonce allows us to ignore bridged
        // messages while still letting other AutoMod messages pass.
        const nonce = ulid();

        await BRIDGED_MESSAGES.update(
            { "discord.messageId": message.id },
            {
                $setOnInsert: {
                    origin: 'discord',
                    discord: {
                        messageId: message.id,
                    },
                },
                $set: {
                    'revolt.nonce': nonce,
                    channels: {
                        discord: message.channelId,
                        revolt: bridgeCfg.revolt,
                    }
                }
            },
            { upsert: true }
        );

        const autumnUrls: string[] = [];

        // todo: upload all attachments at once instead of sequentially
        for (const a of message.attachments) {
            try {
                if (a[1].size > MAX_BRIDGED_FILE_SIZE) {
                    logger.debug(`Skipping attachment ${a[0]} ${a[1].name}: Size ${a[1].size} > max (${MAX_BRIDGED_FILE_SIZE})`);
                    continue;
                }

                logger.debug(`Downloading attachment ${a[0]} ${a[1].name} (Size ${a[1].size})`);

                const formData = new FormData();
                const file = await axios.get(a[1].url, { responseType: 'arraybuffer' });

                logger.debug(`Downloading attachment ${a[0]} finished, uploading to autumn`);

                formData.append(
                    a[0],
                    file.data,
                    {
                        filename: a[1].name || a[0],
                        contentType: a[1].contentType || undefined
                    }
                );

                const res = await axios.post(
                    `${AUTUMN_URL}/attachments`, formData, { headers: formData.getHeaders() }
                );

                logger.debug(`Uploading attachment ${a[0]} finished`);

                autumnUrls.push(res.data.id);
            } catch(e) { console.error(e) }
        }

        const sendBridgeMessage = async (reply?: string) => {
            const payload = {
                content: await renderMessageBody(message.content),
                //attachments: [],
                //embeds: [],
                nonce: nonce,
                replies: reply ? [ { id: reply, mention: !!message.mentions.repliedUser } ] : undefined,
                masquerade: {
                    name: message.author.username,
                    avatar: message.author.displayAvatarURL({ size: 128 }),
                },
                embeds: message.embeds.length
                    ? message.embeds.map(e => new GenericEmbed(e).toRevolt())
                    : undefined,
                attachments: autumnUrls.length ? autumnUrls : undefined,
            };

            await axios.post(
                `${revoltClient.apiURL}/channels/${channel._id}/messages`,
                payload,
                {
                    headers: {
                        'x-bot-token': process.env['REVOLT_TOKEN']!
                    }
                }
            )
            .then(async res => {
                await BRIDGED_MESSAGES.update(
                    { "discord.messageId": message.id },
                    {
                        $set: { "revolt.messageId": res.data._id },
                    }
                );

                metrics.messages.inc({ source: 'discord', type: 'create' });
            })
            .catch(async e => {
                console.error(`Failed to send message: ${e}`);
                if (reply) {
                    console.info('Reytring without reply');
                    await sendBridgeMessage(undefined);
                }
            });
        }

        await sendBridgeMessage(bridgedReply?.revolt?.messageId);
    } catch(e) {
        console.error(e);
    }
});

// Replaces @mentions and #channel mentions
async function renderMessageBody(message: string): Promise<string> {

    // Replace Tenor URLs so they render properly.
    // We have to download the page first, then extract
    // the `c.tenor.com` URL from the meta tags.
    // Could query autumn but that's too much effort and I already wrote this.
    if (RE_TENOR.test(message)) {
        try {
            logger.debug('Replacing tenor URL');

            const res = await axios.get(
                message,
                {
                    headers: {
                        'User-Agent': 'AutoMod/1.0; https://github.com/janderedev/automod',
                    }
                }
            );

            const metaTag = RE_TENOR_META.exec(res.data as string)?.[0];
            if (metaTag) {
                return metaTag
                    .replace('<meta class="dynamic" property="og:url" content="', '')
                    .replace('">', '');
            }
        } catch(e) { logger.warn(`Replacing tenor URL failed: ${e}`) }
    }

    // @mentions
    message = await smartReplace(message, RE_MENTION_USER, async (match: string) => {
        const id = match.replace('<@!', '').replace('<@', '').replace('>', '');
        const user = await discordFetchUser(id);
        return `@${user?.username || id}`;
    }, { cacheMatchResults: true, maxMatches: 10 });

    // #channels
    message = await smartReplace(message, RE_MENTION_CHANNEL, async (match: string) => {
        const id = match.replace('<#', '').replace('>', '');
        const channel = client.channels.cache.get(id);
        const bridgeCfg = channel ? await BRIDGE_CONFIG.findOne({ discord: channel.id }) : undefined;
        const revoltChannel = bridgeCfg?.revolt
            ? revoltClient.channels.get(bridgeCfg.revolt)
            : undefined;

        return revoltChannel ? `<#${revoltChannel._id}>` : `#${(channel as TextChannel)?.name || id}`;
    }, { cacheMatchResults: true, maxMatches: 10 });

    // :emojis:
    message = await smartReplace(message, RE_EMOJI, async (match: string) => {
        return match
            .replace(/<(a?)?:/, ':\u200b') // We don't want to accidentally send an unrelated emoji, so we add a zero width space here
            .replace(/(:\d{18}?>)/, ':');
    }, { cacheMatchResults: true });

    return message;
}
