const path = require('path');
const find = require('findit');
const {Client,ClientOptions,GatewayIntentBits,Message, Partials, ActivityType,EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, PermissionsBitField, PermissionFlagsBits } = require('discord.js')
const roleRulesM = require('./models/autoRoleRule');
const feedM = require('./models/feeds');
const xmlparser = require('xml-js')
const GuildM = require('./models/guilds');
const Spawner = require('child_process');
const mongoose = require('mongoose');

module.exports.GatewayIntentBits = GatewayIntentBits;
module.exports.Partials = Partials;
module.exports.ActivityType = ActivityType;
module.exports.EmbedBuilder = EmbedBuilder;
module.exports.ActionRowBuilder = ActionRowBuilder;
module.exports.ButtonBuilder = ButtonBuilder;
module.exports.ButtonStyle = ButtonStyle;
module.exports.Message = Message;
module.exports.PermissionsBitField = PermissionsBitField;
module.exports.PermissionFlagsBits = PermissionFlagsBits;

//@ts-check
class CommandOptions 
{
    /**
     * 
     * @param {String} name 
     * @param {String[]} aliases 
     * @param {String} description 
     * @param {Boolean} needsAdmin 
     * @param {Boolean} hidden 
     */
    constructor (name, aliases, description, needsAdmin, hidden)
    {
        this.name = name;
        this.aliases = aliases;
        this.description = description;
        this.needsAdmin = needsAdmin;
        this.hidden = hidden;
    }
}
class command 
{
    /**
     * @param {_Client} client extends discord.js.Client
     * @param {CommandOptions} options extends Lib.Options
     */
    constructor(client, options)
    {
        this.name = options.name;
        this.aliases =  options.aliases;
        this.description = options.description;
        this.needsAdmin =options.needsAdmin;
        this.hidden = options.hidden;
        this.client = client;
    }
}module.exports.Command = command;


class _Client extends Client
{
    /**
     * 
     * @param {ClientOptions} options 
     */
    commands = new Map();
    constructor(options, mongoDBURI)
    {
        super(options);
        let finder = find(path.resolve('commands'));
        finder.on('file', file=>
        {
            let command = require(file);
            if(typeof command === 'function')
            {
                let c = new command(this);
                
                this.commands.set(c.name,{
                    needsAdmin: c.needsAdmin?c.needsAdmin:false,
                    command: command,
                });
                if(c.aliases)
                {
                    for(var i =0; i<c.aliases.length+1; i++)
                    {
                        this.commands.set(c.aliases[i],{
                            needsAdmin: c.needsAdmin,
                            command: command,
                        });
                    }
                }
            }
        })
        
        finder.on('end', ()=>
        {
            this.enableCommands();
        })
        connectToDB();
        function connectToDB()
        {
            try {
                mongoose.connect(mongoDBURI);
            } catch (err) {
                console.log('Server: Error: There was an Error with the connection to the database, attempting to restart it.', err)
                connectToDB();
            }
        }
        mongoose.Promise = global.Promise;
        this.setGuilds();
        this.RoleSetter();
        this.rustCommits = new rustCommits(this);
        this.freegames = new FreeGames(this);
        this.strikes = new Strikes(this);
        this.on('disconnect', (event) => {
            console.log(`Client disconnected: ${event.reason}`);
        });
        this.on('reconnecting', () => {
            console.log('Client is reconnecting...');
        });
        this.YTFeed();
        this.music();
        setInterval(() => {
            this.YTFeed()
            this.music();
        }, 60*60*1000);
    }
    setGuilds()
    {
        this.on('ready', ()=>
        {

            this.guilds.cache.forEach(guild=>
            {
                GuildM.find({gID:guild.id})
                .then(
                    (guild_,err)=>
                    {
                        if(err || guild_.length==0)
                        {
                            var nGuild = GuildM();
                            nGuild.name = guild.name;
                            nGuild.memberCount = guild.members.cache.size;
                            nGuild.gID = guild.id;
                            nGuild.allowInvites = false;
                            nGuild.strikes = false;
                            nGuild.music = false;
                            nGuild.save(err=>
                                {
                                    if(err) console.log('Server: Adding non existing guilds to the DB. Error ocurred:', err)
                                });
                        }
                    }
                )
            })
        })
        this.on('guildCreate', guild=> 
        {
            var guildModel = new GuildM();
            guildModel.name = guild.name;
            guildModel.memberCount = guild.members.cache.size;
            guildModel.gID = guild.id;
            guildModel.allowInvites = false;
            guildModel.strikes = false;
            guildModel.music = false;
            guildModel.save()
        });
        this.on('guildDelete', guild => 
        {
            GuildM.find({gID:guild.id}).then(guild=>
                {
                    if(guild.length==0) return sendMessage(false, 'ID not on the list.')
                    GuildM.findOneAndRemove(guild.id, (err)=>
                    {
                        if(err) console.log('Server: Deleting guild from DB: There was an error:', err)
                    })
                })
        });


    }
    /**
     * Register a group of commands under the folder commands
     * @param {String} name - Name of the group.
     * @param {String} folderName - Name of the folder.
     */
    async enableCommands()
    {
        this.on("messageCreate", message=>
        {
            this.checkForInvites(message);
            if(message.content.startsWith(process.env.prefix)) //Test prefix t!
            {
                let commandName=message.content.split('!')[1].split(' ')[0];
                let args = message.content.split(' ');
                args = args.slice(1);
                args = args.filter(x => x !== '')
                if(this.commands.get(commandName))
                {
                    let needsAdmin = this.commands.get(commandName).needsAdmin;
                    let isAdmin =false;
                    try 
                    {
                        isAdmin = this.guilds.cache.get(message.guild.id).members.cache.get(message.author.id).permissions.has(PermissionFlagsBits.Administrator);
                    } catch (error) 
                    {
                        console.log('Lib: Command: Permission Verification: Failed')
                    }
                    let command = this.commands.get(commandName).command
                    if((needsAdmin && isAdmin) || !needsAdmin) return   new command(this).run(message, args);
                    if(needsAdmin && !isAdmin) return new ErrorMessage(this).send(ErrorType.Permissions, message);
                }
                new ErrorMessage(this).send(ErrorType.NotOnTheList,message)
            }
        })
    }
    /**
     * 
     * @param {String} name - Command Name
     * @returns {command}
     */
    async RoleSetter ()
    {
        this.on('messageReactionAdd', (reaction, user)=>
            {
                console.log('Lib: RoleSetter: Reaction: Added', reaction._emoji.name);
                (async ()=>
                {
                    const rule = await roleRulesM.findOne({mID:reaction.message.id,roleEmoji:reaction._emoji.id}).then(rule =>{return rule}).catch(err=>{return null})
                    if(rule)
                    {
                        console.log(`RoleAssignment: Guild:${this.guilds.cache.get(rule.gID)}: Adding role ${rule.roleName} given to ${user.username}`)
                        this.channels.cache.get(reaction.message.channelId).members.get(user.id).roles.add(rule.roleID)
                    }
                })()
            })
            this.on('messageReactionRemove', (reaction, user)=>
            {
                (async ()=>
                {
                    const rule = await roleRulesM.findOne({mID:reaction.message.id,roleEmoji:reaction._emoji.id}).then(rule =>{return rule}).catch(err=>{return null})
                    if(rule)
                    {
                        console.log(`RoleAssignment: Guild:${this.guilds.cache.get(rule.gID)}: Removing ${user.username}'s ${rule.roleName} role. `)
                        this.channels.cache.get(reaction.message.channelId).members.get(user.id).roles.remove(rule.roleID)
                    }
                })()
            })
    }
    async YTFeed()
    {
        const feeds = await feedM.find().then(feeds=>{return feeds})
        if(feeds.length<1) return 
        for(var feed of feeds)
        {
            (async (feed)=>{
                if(!feed.YTChannelId) return
                if(feed.YTChannelId=='false') return
                var res = await fetch(`https://www.youtube.com/feeds/videos.xml?channel_id=${feed.YTChannelId}`)
                                .then(handleResponse)
                                .then(handleData)
                                .catch(handleError);
                                function handleResponse(response) 
                                {
                                return response.text()
                                }
                                function handleError(error) 
                                {
                                return error;
                                }
                                function handleData(data) 
                                {
                                    data = xmlparser.xml2json(data,
                                                    {
                                                        compact: true,
                                                        space: 4
                                                    });
                                    data = JSON.parse(data);
                                    return {
                                                link:data.feed.entry[0].link._attributes.href,
                                                published:data.feed.entry[0].published._text,
                                                link:data.feed.entry[0].link._attributes.href,
                                                image:data.feed.entry[0].link._attributes.href.split('=')[1],
                                                title:data.feed.entry[0].title._text,
                                                author:
                                                {
                                                    name:data.feed.entry[0].author.name._text,
                                                    url:data.feed.entry[0].author.uri._text
                                                }        
                                            }
                                } 
                if(!res) return false;
                const channel = this.channels.cache.get(feed.ChannelId);
                if(!channel) return false;
                var ytChannelName = await fetch('https://www.youtube.com/feeds/videos.xml?channel_id='+feed.YTChannelId,
                                        {
                                            method: "GET",
                                            mode: "cors", 
                                        })
                                        .then(response=>
                                            {
                                                if(response.ok) return response.text();
                                            })
                                        .then(data=>
                                            {
                                                data = xmlparser.xml2json(data,
                                                    {
                                                        compact: true,
                                                        space: 4
                                                    });
                                                    data = JSON.parse(data);
                                                return data.feed.author.name._text
                                            })
                                        .catch(error=>
                                            {
                                                return error;
                                            });
                console.log('Lib: YTFeed: Channel:',ytChannelName,' | Discord Channel:', channel.name, ' | DCChannelId:',channel.id)
                var aux = res;
                const lastSentMessage= await channel.messages.fetch().then(res=>
                    {
                        for(var item of res)
                        {
                            if(item[1].embeds)
                            {
                                if(item[1].embeds[0])
                                {
                                    var fields = item[1].embeds[0].fields.filter(x=>x.name === 'PublishedTimeStamp');
                                    var isFeed = fields.length>0;
                                    if(!aux.author) return null;
                                    if(isFeed && item[1].embeds[0].title == aux.author.name) return item[1].embeds[0];
                                }
                            }
                        }
                    })
                if(!lastSentMessage) return sendMessage(res, feed, channel);
                const lastSentMessagePublished = lastSentMessage.fields.filter(x=>x.name === 'PublishedTimeStamp')[0].value;
                // console.log(lastSentMessagePublished != res.published , 'lastSentPublished',lastSentMessagePublished,'published', res.published)
                if(!lastSentMessagePublished) sendMessage(res, feed, channel)
                else if(lastSentMessagePublished != res.published) sendMessage(res, feed, channel);
            })(feed);
        }
        function sendMessage(res, feed, channel)
        {
            if(!res?.author?.name) return 
            console.log('Lib: YTFeed: SendMessage: Res: Author: Name:', res?.author?.name)
            const embed = new EmbedBuilder()
            embed.setFooter({text:'Rem-chan on ', iconURL:"https://i.imgur.com/g6FSNhL.png"})
            embed.setAuthor({name:"Rem-chan", iconURL:"https://i.imgur.com/g6FSNhL.png",url:'https://rem.wordfights.com/addtodiscord'}); 
            embed.setColor(0x110809);
            embed.setTimestamp();
            embed.setURL(res?.author?.url)
            embed.setTitle(res?.author?.name);
            embed.setImage(`https://i3.ytimg.com/vi/${res?.image}/maxresdefault.jpg`)
            embed.setDescription(feed?.CostumMessage);
            embed.addFields({name:res?.title, value:' '},
                            {name:'PublishedTimeStamp', value:res?.published},
                            {name:'Link:', value:res?.link});
            channel.send({embeds:[embed]});
        }
    }
    async checkForInvites(message)
    {
        let content = message.content;
        let guild = message.guildId;
        if(!content?.includes('discord.gg')) return 

        await GuildM.find({gID:guild})
        .then((g, err)=>
        {
            if(err) return;
            if(!g.allowInvites) 
            {
                message.delete();
            }
            return;
        });
    }
    async music()
    {
        const ServersWithMusicOn = await this.serversWithMusicOn();
        const ISGONNABEOFFFORAWHILE = true
        if(!ServersWithMusicOn && !ISGONNABEOFFFORAWHILE) return 
        for(var server of ServersWithMusicOn)
        {
            if(!Childs[server.name])
            {
                console.log('Index: Starting Music Worker:', server.name,`(${server.gID})`)
                Childs[server.name] = Spawner.fork('./musicWorker.js',[server.gID, server.name]);
            }
            else
            {       //Health check
            (function (server)
            {
                    Childs[server.name].on('exit', ()=>
                    {
                        console.log('Child DIED, spawning another.')
                        Childs[server.name] = Spawner.fork('./musicWorker.js',[server.gID, server.name]);
                    })
                })(server);
            }

        } 
    }
    async serversWithMusicOn()
    {
        return await GuildM.find({music:true}).then(g=>{return g})
    }

}module.exports.Client = _Client;
const ErrorType = 
{
    Permissions: "Permissions",
    Arguments: "Arguments",
    NoArguments: "No Arguments Given",
    NotOnTheList: "Unknown Command",
    OldMessages: "Can't delete this messages.",
    FeatureInnactive: "This feature is not active."

}
module.exports.ErrorType = ErrorType;

class Channel 
{
    /**
     * 
     * @param {String} channelID
     */
    constructor(channelID)
    {
        this.channel = channelID;
    }

}

module.exports.Channel = Channel;
/**
 * 
 * @param {Number} length 
 * @returns {String}
 */
function  Random(length) 
{
    length?length:length=5;
    var result           = '';
    var characters       = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
    var charactersLength = characters.length;
    for ( var i = 0; i < length; i++ ) {
        result += characters.charAt(Math.floor(Math.random() * charactersLength));
    }
    return result;
}module.exports.Random = Random;

class ErrorMessage 
{
    /**
     * @param {_Client} client 
     */
    constructor(client)
    {
        this.client = client;
    }
    /**
     * 
     * @param {ErrorType} errorType 
     * @param {Message} message 
     * @param {...String} extraMessages 
     */
   
    async send(errorType,message, extraMessages)
    {
        if (!message.channel?.id) return;
        const embed = new EmbedBuilder()
        .setColor(0x4d0000)
        .setAuthor({name:"Rem-chan", iconURL:"https://i.imgur.com/g6FSNhL.png",url:'https://rem.wordfights.com/addtodiscord'})
        .setTimestamp()
        .setFooter({text:'Rem-chan on ', iconURL:"https://i.imgur.com/g6FSNhL.png"})
        .addFields({name: 'Error:', value: 'something'})
        .addFields({name: 'Try:', value: 'something'});
        
        switch (errorType) {
            case ErrorType.Arguments:
                embed.data.fields[0].value =ErrorType.Arguments;
                embed.data.fields[1].value = 'Verify the arguments provided.'
                break;
            case ErrorType.NoArguments:
                embed.data.fields[0].value =ErrorType.NoArguments;
                embed.data.fields[1].value = 'Provide the required arguments for this command.'
                break;
            case ErrorType.Permissions:
                embed.data.fields[0].value =ErrorType.Permissions;
                embed.data.fields[1].value = 'Ask this servers administrator to use the command.'
                break;
            case ErrorType.NotOnTheList:
                embed.data.fields[0].value =ErrorType.NotOnTheList;
                embed.data.fields[1].value = 'Use !help for the list of available.'
                break;
            case ErrorType.OldMessages:
                embed.data.fields[0].value =ErrorType.OldMessages;
                break;
            case ErrorType.FeatureInnactive:
                embed.data.fields[0].value =ErrorType.FeatureInnactive;
                embed.data.fields[1].value = 'If you are an admin you can activate the function on the dashboard. (rem.wordfights.com)'
                break;
            default:
                break;
        }
        if(extraMessages)
        {
            for(var i = 0; i<extraMessages.length; i++)
            {
                embed.addFields({name:'Tip:', value:extraMessages[i]});
            }
        }
        const randomID =  Random();
        const row = new ActionRowBuilder()
        .addComponents(
                new ButtonBuilder()
                .setCustomId(randomID)
                .setLabel('Remove this.')
                .setStyle(ButtonStyle.Primary)
                .setEmoji('❌')
        );
        const Message = await this.client.channels.cache.get(message.channel.id).send({ephemeral: true, embeds: [embed], components: [row] }); 
    
        const filter = i => i.customId === randomID;

        const collector = message.channel.createMessageComponentCollector({ filter, time: 60000 });

        collector.on('collect', async m => 
        {
            message.delete().then(()=>m.message.delete())
            .catch(()=>m.message.delete());
        });
        collector.on('end', async ()=>
        {
            Message.edit({ components: [] });
        })

    }

}module.exports.ErrorMessage = ErrorMessage;

/**
 * Anilist Client -> Search Functions
 */
class AnimeInfo
{
    /**
     * 
     * @param {String} title 
     * @param {String} status 
     * @param {String} url
     * @param {String} episodes 
     * @param {String} trailer 
     * @param {String} description 
     * @param {String} coverImage 
     */
    constructor(title, status, episodes, url , trailer, description, coverImage)
    {
        this.title = title; 
        this.status = status;
        this.url = url
        this.episodes=episodes;
        this.trailer = trailer;
        this.description = description;
        this.coverImage = coverImage;
    }
}
class CharInfo
{
    /**
     * 
     * @param {String} name 
     * @param {String} gender 
     * @param {String} image 
     * @param {String} url
     * @param {String} description 
     */
    constructor(name, gender, image, description)
    {
        this.name = name; 
        this.gender = gender;
        this.url = url
        this.image=image;
        this.description = description;
    }
}
class MangaInfo
{
    /**
     * 
     * @param {String} title 
     * @param {String} status 
     * @param {String} url
     * @param {String} description 
     * @param {String} coverImage 
     */
    constructor(title, status, description, coverImage)
    {
        this.title = title; 
        this.status = status;
        this.url = url
        this.description = description;
        this.coverImage = coverImage;  
    }
}
class Options
{
    /**
     * 
     * @param {String} method 
     * @param {String} headers 
     * @param {String} body 
     */
    constructor(method, headers, body)
    {

        this.method = method;
        this.headers = headers;
        this.body = body;  
    }
}

class AnilistCli {
    /**
     * 
     * @param {Options} options 
     * @returns {Info}
     */
    async getInfo(options)
    {
        return await fetch('https://graphql.Anilist.co', options).then(handleResponse)
                     .then(handleData)
                     .catch(handleError);
          function handleResponse(response) 
          {
              return response.json().then(function (json) 
              {
                  return response.ok ? json : Promise.reject(json);
              });
          }
          function handleData(data)
          {
            return data
          }
          function handleError(error) {
              return error;
          }
    }
    /**
     * 
     * @param {String} ss - Search Query
     * @returns {AnimeInfo} 
     */
    async searchAnime(ss)
    {
        var variables = {
            ss,
            "page": 0,
            "perPage": 4
        };
        var query = `query ($ss: String, $page: Int, $perPage: Int) {
            Page(page: $page, perPage: $perPage) {
              media(search: $ss, type: ANIME) {
                id
                title {
                  romaji
                  english
                }
                coverImage {
                    medium
                }
              }
            }
          }
          `
          var options = {
              method: 'POST',
              headers: {
                  'Content-Type': 'application/json',
                  'Accept': 'application/json', 
              },
              body: JSON.stringify({
                  query,
                  variables
              })
          };
        var data =  await this.getInfo(options);

        data = data.data.Page.media
        if(!data[0]) return 'Error'
        var aux = [];
        for(var anime of data)
        {
            aux.push({
                title:anime.title,
                id:anime.id,
                coverImage:anime.coverImage.medium
            });
        }
            return aux;
    }   
    async getAnimeInfo(id)
    {
        var variables = {
            id
        };
        var query = `query ($id: Int) {
            Media(id:$id) {
              id
              idMal
              title {
                romaji
                english
                native
              }
              siteUrl
              status
              episodes
              season
              seasonYear
              trailer {
                site
                id
              }
              description(asHtml:false)
              coverImage {
                large
              }
            }
          }`;
        var options = {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'Accept': 'application/json', 
            },
            body: JSON.stringify({
                query,
                variables
            })
        };
        var data =  await this.getInfo(options);
        data = data.data;
        return {
            title:data.Media.title,
            malLink:'https://myanimelist.net/anime/'+data.Media.idMal,
            status:data.Media.status,
            url:data.Media.siteUrl,
            episodes:data.Media.episodes,
            trailer:data.Media.trailer,
            description:data.Media.description,
            coverImage:data.Media.coverImage.large,
            season: (data.Media.season?data.Media.season:'') + ' ' + (data.Media.seasonYear?data.Media.seasonYear:'')
        }        
    }
    /**
     * 
     * @param {String} ss - Search Query
     * @returns {CharInfo}
     */
    async searchChar(ss)
    {
        var variables = {
            ss,
            "page": 0,
            "perPage": 4
        };
        var query = `query ($ss: String, $page: Int, $perPage: Int) {
            Page(page: $page, perPage: $perPage) {
              characters(search: $ss, sort: SEARCH_MATCH) {
                id
                name {
                  full
                  native
                }
                siteUrl
                gender
                image {
                  medium
                }
              }
            }
          }
          `;
        var options = {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'Accept': 'application/json',
            },
            body: JSON.stringify({
                query,
                variables
            })
        };
        var data =  await this.getInfo(options);
        var chars = data.data.Page.characters
        var aux = [];
        if(!chars[0]) return 'Error'
        for(var char of chars)
        {
            aux.push({
                id:char.id,
                name:char.name,
                gender:char.gender,
                url:char.siteUrl,
                image:char.image.medium
            })
        }
        return aux;
    }
    async getCharInfo(id)
    {
        
        var variables = {
            id
        };
        var query = `query ($id: Int) {
            Character(id: $id, ) {
              id
              name {
                full
                native
              }
              gender
              age
              siteUrl
              image {
                medium
              }
              description(asHtml:false)
            }
          }
          `;
        var options = {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'Accept': 'application/json',
            },
            body: JSON.stringify({
                query,
                variables
            })
        };
        var data =  await this.getInfo(options);
        data = data.data;
        return {
            name:data.Character.name,
            gender:data.Character.gender,
            url:data.Character.siteUrl,
            image:data.Character.image.medium,
            description:data.Character.description?data.Character.description:' ',
            age:data.Character.age,
        }

        
    }
    /**
     * 
     * @param {String} ss - Search Query
     * @returns {MangaInfo}
     */
    async searchManga(ss)
    {
        var variables = 
        {
            ss,
            "page": 0,
            "perPage": 4
        }
        var query = `query ($ss: String, $page: Int, $perPage: Int) {
            Page(page: $page, perPage: $perPage) {
              media(search: $ss, type: MANGA) {
                id
                title {
                  romaji
                  english
                }
                coverImage {
                  medium
                }
              }
            }
          }`
        var options = {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'Accept': 'application/json',
            },
            body: JSON.stringify({
                query,
                variables
            })
        };
        var data =  await this.getInfo(options);
        var mangas = data.data.Page.media;
        var aux = [];
        if(!mangas[0]) return 'Error'
        for(var manga of mangas)
        {
            aux.push({
                id:manga.id,
                title:manga.title,
                image:manga.coverImage.medium
            })
        }
        return aux;
        
    }   
    async getMangaInfo(id)
    {  
        var variables = {
            id
        };
        var query = `query ($id: Int) {
            Media(id: $id) {
              title {
                romaji
                english
                native
              }
              siteUrl
              status
              description(asHtml: false)
              coverImage {
                large
              }
            }
          }
          `;
        var options = {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'Accept': 'application/json',
            },
            body: JSON.stringify({
                query,
                variables
            })
        };
        var data =  await this.getInfo(options);
        data = data.data;
        return {
            title:data.Media.title,
            status:data.Media.status,
            url:data.Media.siteUrl,
            description:data.Media.description,
            coverImage:data.Media.coverImage.large,
        }     
    }

}module.exports.Anilist = AnilistCli;

const channelM = require('./models/channels');

class rustCommits
{
    /**
     * 
     * @param {_Client} client 
     */
    constructor(client)
    {
        this.client = client;
        this.update();
        setInterval(() => {
            this.update();
        }, 3*60*1000);
        this.channels;
    }
    async update ()
    {
        const channels = await channelM.find({for:'rust'}).then(channels=>{return channels});
        this.channels = await this.resolveChannels(channels);
        const Res = await fetch("https://commits.facepunch.com/?format=json").then(handleResponse)
                   .then(handleData)
                   .catch(handleError);
        function handleResponse(response) 
        {
            return response.json().then(function (json) 
            {
                return response.ok ? json : Promise.reject(json);
            });
        }
        function handleError(error) {
            return error;
        }
        function handleData(data) {
            data.results.splice(30, 20);
            return data.results.filter(x=>x.repo.search(/rust/i)!=-1)
        }
        if(!Res.length)
        {
            //console.log('Framework: Rustcommits: Res', Res);
            return this.update();
        }
        for(var j = 0; j<this.channels.length; j++)
        {
            if(!this.channels[j].lastid) this.channels[j].lastid = 0;
            const Pos = Res.map(e => e.id).indexOf(this.channels[j].lastid);
            Res.splice(Pos, Res.length-Pos);
            for(var i = Res.length-1; i>0; i--)
            {
                if(Res[i].id > this.channels[j].lastid)
                {
                    send(
                    {
                        Author:Res[i].user.name,
                        Avatar:Res[i].user.avatar,
                        Time:Res[i].created.split("T")[1]+ " of "+ Res[i].created.split("T")[0],
                        Content:Res[i].message,
                        ID:Res[i].id,
                        Repo:Res[i].repo,
                        Branch:Res[i].branch
                    }, this.channels[j].cID, this.client);
                }
            }
        }
        /**
         * 
         * @param {RustCommit} commit 
         * @param {Channel} channel 
         */
        function send(commit, channel, client)
        {
            client.channels.cache.get(channel).send({embeds:[
                new EmbedBuilder()
                .setColor(0xc23811)
                .setTitle(commit.Time)
                .setURL('https://commits.facepunch.com')
                .setAuthor({name:commit.Author, iconURL:commit.Avatar==''?"https://i.imgur.com/g6FSNhL.png":commit.Avatar,url:`https://commits.facepunch.com/${commit.Author.split(' ').join('')}`})
                .setDescription(commit.Content)
                .addFields(
                    { name:`${commit.Repo}`, value:`[${commit.Repo}/${commit.Branch}](https://commits.facepunch.com/r/${commit.Repo}/${commit.Branch.split(' ')? commit.Branch.split(' ').join('%20'):commit.Branch} 'Branch Link')`},
                    { name: 'ID', value: commit.ID.toString() },
                    )
                .setTimestamp()
                .setFooter({text:'Rem-chan on ', iconURL:"https://i.imgur.com/g6FSNhL.png"})
            ]});
        }
        
    }
     /**
     * 
     * @param {channelM} channels 
     */
      async resolveChannels(channels)
      {
          for await(var channel of channels )
          {
              if(this.client.channels.cache.get(channel.cID))
              {
                  const Channel = this.client.channels.cache.get(channel.cID)
                  const lastid = await Channel.messages.fetch({limit:1}).then(message=>
                      {
                            if(!message) return 0
                            const [content] = message.values();
                            if(!content||!content.embeds||!content.embeds.length>0) return 0;
                            return parseInt(content.embeds[0].fields[1].value)
                      })
                      channel['lastid'] = lastid;  
              }
          }
          return channels;
      }
}
class RustCommit 
{
    /**
     * 
     * @param {String} Author 
     * @param {String} Avatar - IMAGE URL
     * @param {String} Time 
     * @param {String} Content 
     * @param {Number} ID 
     * @param {String} Repo 
     * @param {String} Branch 
     */
    constructor(Author, Avatar, Time, Content, ID, Repo, Branch)
    {
        this.Author = Author
        this.Avatar = Avatar
        this.Time = Time
        this.Content = Content
        this.ID = ID
        this.Repo = Repo
        this.Branch = Branch
    }
}
class FreeGames
{
    /**
     * 
     * @param {_Client} client 
     */
    constructor(client)
    {
        this.client = client;
        this.update()
        setTimeout(() => {
            this.update();
        }, 3*60*1000);
    }
    async update()
    {
        const channels = await channelM.find({for:'freegames'}).then(channels=>{return channels});
        this.channels = await this.resolveChannels(channels);
        
        const Res = await fetch('https://www.gamerpower.com/api/giveaways').then(handleResponse)
            .then(handleData)
            .catch(handleError);
        function handleResponse(response) 
        {
            return response.json().then(function (json) 
            {
                    return response.ok ? json : Promise.reject(json);
            });
        }
        function handleError(error) {
            return error;
        }
        function handleData(data) {
            data.splice(10, data.length-10);
            return data
        }
        //if(!Res.length) console.log('FrameWork -> FreeGames: Res:',Res);
        if(!Res.length) return setTimeout(() => 
        {
            this.update();
        }, 60*1000); 
        for(var j = 0; j<this.channels.length; j++)
        { 
            const Pos = Res.map(e => e.id).indexOf(this.channels[j].lastid);
            Res.splice(Pos, Res.length-Pos)

            for(var i = Res.length-1; i>0; i--)
            {
                if(Res[i].id > this.channels[j].lastid)
                {
                    send(
                    {
                        ID:Res[i].id, //
                        Title:Res[i].title, //
                        Type:Res[i].type, //
                        Thumb:Res[i].thumbnail, //
                        Image:Res[i].image, //
                        Description:Res[i].description, //
                        Instructions:Res[i].instructions,//
                        URL:Res[i].open_giveaway_url,//
                        Platform:Res[i].platforms,//
                        EndDate:Res[i].end_date,
                        
                    }, this.channels[j].cID, this.client);
                }
            }
        }
        /**
         * 
         * @param {FreeGameModel} game 
         * @param {Channel} channel 
         */
        function send(game, channel, client)
        {
            client.channels.cache.get(channel).send({embeds:[
                new EmbedBuilder()
                .setColor(0x2d9134)
                .setURL(game.URL)
                .setTitle(game.Title)
	            .setAuthor({name:"Rem-chan", iconURL:"https://i.imgur.com/g6FSNhL.png",url:'https://rem.wordfights.com/addtodiscord'})
                .setDescription(game.Description)
                .setImage(game.Image)
                .setThumbnail(game.Thumb)
                .addFields(
                    { name: 'Platforms', value: game.Platform},
                    { name: 'Type of offer:', value: game.Type},
                    { name:'Instructions:', value:game.Instructions},
                    { name:`End date:`, value:`${game.EndDate}`},
                    )
                .setTimestamp()
                .setFooter({text:`(${game.ID}) Rem-chan on `, iconURL:"https://i.imgur.com/g6FSNhL.png"})
            ]});
        }
    }
    /**
     * 
     * @param {channelM} channels 
     */
    async resolveChannels(channels)
    {
        for await(var channel of channels )
        {
            if(this.client.channels.cache.get(channel.cID))
            {
                const Channel = this.client.channels.cache.get(channel.cID)
                const lastid = await Channel.messages.fetch({limit:1}).then(message=>
                    {
                        if(!message) return 0
                        const [content] = message.values();
                        if(!content||!content.embeds||!content.embeds.length>0) return 0;
                        return parseInt(content.embeds[0].footer.text.split('(')[1].split(')')[0])
                    })
                    channel.lastid = lastid;  
            }
        }
        return channels;
    }
}

class FreeGameModel
{
    /** 
     * @param {String} ID 
     * @param {String} Title 
     * @param {String} Thumb 
     * @param {String} Type
     * @param {String} Image 
     * @param {String} Description 
     * @param {String} Instructions 
     * @param {String} URL 
     * @param {String} Platform 
     * @param {Date} EndDate 
     */
    constructor(ID, Title, Thumb,Type, Image, Description, Instructions, URL, Platform, EndDate)
    {
        this.ID = ID;
        this.Title = Title;
        this.Type = Type;
        this.Thumb = Thumb;
        this.Image = Image;
        this.Description = Description;
        this.Instructions = Instructions;
        this.URL = URL;
        this.Platform = Platform;
        this.EndDate = EndDate;
    }
}
const strikesM = require('./models/strikes');
const guildsM = require('./models/guilds');
class Strikes
{
    constructor(client)
    {
        this.client = client;
        this.check();
         setInterval(() => {
             this.check();
         }, 5*60*1000);
    }
    async getAreStrikesActive(guildId)
    {
        const guild = await guildsM.find({gID:guildId}).then(g=>{return g[0]});
        return guild.strikes;
    }
    async check()
    {

        /**
         * TODO: Grab validated strikes.
         * TODO: Check if guild has strikes active.
         * TODO: Handle player if strikes have reached the threshold
         * 
         */
        const Strikes = await strikesM.find().then(s=>{return s})
        if(Strikes.length==0) return console.log('Lib: Striker: No Strikes')
        const guilds = await guildsM.find({strikes:true}).then(g=>{return g})
                                                         .then(guilds => guilds.map(guild => guild.gID));;
        if(guilds.length==0) return console.log('Lib: Striker: No Guilds')
        const validStrikes = await strikesM.find({validated:true}).then(res=>{return res}); 
        const strikesByGuildID = new Map();
        validStrikes.forEach(strike => 
        {
            const { strokedID, guildID } = strike;
            
            // If the guildID key doesn't exist in strikesByGuildID, create it
            if (!strikesByGuildID.has(guildID)) strikesByGuildID.set(guildID, new Map());

            const strikesByStrokedID = strikesByGuildID.get(guildID);

            // If the strokedID key doesn't exist in the strikesByStrokedID map, create it
            if (!strikesByStrokedID.has(strokedID)) strikesByStrokedID.set(strokedID, [strike]);
            else strikesByStrokedID.get(strokedID).push(strike);
        });
        const guildsWith3Strikes = new Map();

        strikesByGuildID.forEach((strikesByStrokedID, guildID) => {
            // Filter strikes by each strokedID to find users with at least three strikes
            const usersWith3Strikes = [];
            strikesByStrokedID.forEach((strikes, strokedID) => {
                if (strikes.length >= 3) {
                    usersWith3Strikes.push({ user: strokedID, strikes });
                }
            });
            
            // If there are users with at least three strikes, add them to the map
            if (usersWith3Strikes.length > 0) {
                guildsWith3Strikes.set(guildID, usersWith3Strikes);
            }
        });
        const guildsWith3StrikesFiltered = new Map([...guildsWith3Strikes].filter(([guildID]) => guilds.includes(guildID)));
        guildsWith3StrikesFiltered.forEach((guild, guildid)=>
        {
            guild.forEach((user)=>
            {
                this.handleStroked(user,guildid);
            })
        })
        
    }
    handleStroked(user, guild)
    {    
        const member = this.client.guilds.cache.get(guild).members.cache.get(user.user);
        if (!member) return console.log('Lib:Strikes: Handle Struck: Member not valid', member)
        const reason = `Strike 1: ${user.strikes[0].reason}; Strike 2: ${user.strikes[1].reason}; Strike 3: ${user.strikes[2].reason}`;
        member.kick(reason)
              .then(() => console.log(`Successfully kicked ${member.user.username}`))
              .catch(err => console.log(`Unable to kick ${member.user.username}: ${err}`));
        strikesM.deleteMany({ guildID: guild, strokedID: user.user }, (err, res) => {
            if (!err) return console.log('Lib: Strikes: Deleted: From User:', member.user.username, 'on Guild:', member.guild.name)
          });
    } 
}
module.exports.Strikes = Strikes;

exports.DiscordAPI = class DiscordAPI
{
    constructor (cookie, bot)
    {
        this.authorization = this.AuthStringMaker(cookie);
        this.bot = bot;
        this.userId = '';
    }
    async getUser ()
    {
            return await fetch('https://discord.com/api/users/@me', 
            {
                headers: {
                    authorization: this.authorization,
                },
            })
            .then(result=>result.json())
            .then(response => 
            {
                return response;
            })
    }
    async getUserId()
    {
        return this.userId = await this.getUser().then(user=>{return user.id});
    }
    async getUserGuilds()
    {

        return await fetch('https://discord.com/api/users/@me/guilds',
                    {
                        headers: {
                            authorization: this.authorization,
                        },
                    })
                    .then(result => result.json())
                    .then(response => 
                    {
                        
                        if(response.message) return response.message
                        return response.filter(x=>x.owner==true);
                    })
    }
    async getGuildsFull()
    {
        var userGuilds = await this.getUserGuilds()
        var promises = [];
        for(var guild of userGuilds) {
            promises.push(new Promise((resolve, reject) => {
                (async (guild)=>
                {
                    
                    var aux = await this.getSpecificGuildData(guild.id);
                    var roleRules = aux ? await this.getRoleRules(guild.id):null;
                    var guildTextChannels = aux ? aux.TextChannels:null;
                    var emojis = aux ? aux.emojis:null;
                    var roles  = aux ? aux.roles:null;
                    const dataObject =  
                    {
                        id:guild.id,
                        music:aux.guild?aux.guild.music:false,
                        strikes:aux.guild?aux.guild.strikes:false,
                        allowInvites:aux?.guild?.allowInvites,
                        name:guild.name,
                        icon:guild.icon?`https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}.webp`:'https://cdn3.iconfinder.com/data/icons/popular-services-brands-vol-2/512/discord-512.png',
                        features:guild.features,
                        hasRem:aux.extra?true:false,
                        memberCount:aux.extra?aux.extra.memberCount:null,
                        roleRules,
                        guildTextChannels,
                        emojis,
                        roles
                    };
                    resolve(dataObject);
    
                })(guild)
            }));
        }
        const guilds = await Promise.all(promises).then((values) => 
        {
            return values
        });
        return guilds;
    }
    async getSpecificGuildData(guildID)
    {
        var extra = this.bot.guilds.cache.get(guildID)
        var guild = await GuildM.find({gID:guildID})
            .then((guild,err)=>
            {
                if(err || guild.length==0) return;
                return {guild};
            });
        var TextChannels = [];
        var emojis = [];
        var roles = [];
        if(extra)
        {
            for(var channel of extra.channels.cache)
            {
                if(channel[1].type==0)
                {
                    TextChannels.push(
                        {
                            'id':channel[0], 
                            'name':channel[1].name
                        });
                }
            }
            for(var emoji of extra.emojis.cache)
            {
                emojis.push(
                    {
                        'id':emoji[0], 
                        'name':emoji[1].name
                    });
            }
            for(var role of extra.roles.cache)
            {
                roles.push(
                    {
                        'id':role[0], 
                        'name':role[1].name
                    });
            }
        }
        try{
            guild= guild.guild[0];
        }
        catch (err)
        {
            console.log('There was an error fetching the guild', err)
        }
        return {guild, extra, TextChannels, emojis, roles};
    }

    async  getRoleRules(guildid)
    {
        return await roleRulesM.find({gID:guildid}).then((rules, err)=>
        {
            if(err || rules.length==0) return 
            return rules
        })
    }
    AuthStringMaker(cookieString)
    {
        const splitCookie = cookieString.split('; ');
        const tokenType = splitCookie[0].split('=')[1];
        const accessToken = splitCookie[1].split('=')[1];
        return `${tokenType} ${accessToken}`
    }
    /**
     * 
     * @param {String} id GuildID 
     * @returns 
     */
    async getSpecificGuildDataForDash(id)
    {
        const aux = await this.getSpecificGuildData(id);
        const roleRules = aux ? await this.getRoleRules(id):null;
        const guildTextChannels = aux ? aux.TextChannels:null;
        const emojis = aux ? aux.emojis:null;
        const roles  = aux ? aux.roles:null;
        return  {
            id,
            music:aux.guild?aux.guild.music:false,
            strikes:aux.guild?aux.guild.strikes:false,
            allowInvites:aux?.guild?.allowInvites,
            name:aux.guild.name,
            icon:aux.icon?`https://cdn.discordapp.com/icons/${aux.id}/${aux.icon}.webp`:'https://cdn3.iconfinder.com/data/icons/popular-services-brands-vol-2/512/discord-512.png',
            hasRem:aux.extra?true:false,
            memberCount:aux.extra?aux.extra.memberCount:null,
            roleRules,
            guildTextChannels,
            emojis,
            roles
        };
    }
    
}
/**
 * 
 * @param {String|String} id - Discord Guild Id (GuildId) | Mongo Record ID (ruleID)
 * @returns {Array<Object>|String} payload
 */
exports.getRules = async function getRules(id)
{
    if(!id) return 'Error: No Guild or Rule Id '
    if(mongoose.Types.ObjectId.isValid(id)) return await roleRulesM.find({_id:id}).then(rule=>{return rule[0]})
    return  await roleRulesM.find({gID:id})
                    .then(rules =>{return rules})
                    .catch(()=>{return null})
}
/**
 * @param {Array.<string, string,string>} args - [Discord Guildid,Discord channelId, YT channelURL]
 * @param {_Client} bot 
 * @returns 
 */
exports.confirmArgsForYTFeed = async function confirmArgs(args, bot)
{
    var channelId = args[1];
    var channelURL = args[2];

    async function getChannelId(url)
    {
    if(url.split('@').length<1) return 
    return await fetch(url)
            .then(handleResponse)
            .then(handleData)
            .catch(handleError);
            function handleResponse(response) 
            {
                if(response.ok) return response.text();
            }
            function handleError(error) 
            {
                return {error:'Bad youtube channel link.'};
            }
            function handleData(data) 
            {         
                return data ? data.split('https://www.youtube.com/feeds/videos.xml?channel_id=')[1].split('"')[0] : false;
            }
    }

    const YTChannelId = await getChannelId(channelURL);
    if(YTChannelId.error) return {error:YTChannelId.error}
    const discordChannel = await bot.channels.fetch(channelId).then(channel=>{return channel }).catch(()=>{return null});
    if(!discordChannel) return {error:'Provided discord channel id does not exist.'}   
    var name = await fetch(`https://www.youtube.com/feeds/videos.xml?channel_id=${YTChannelId}`)
                        .then(response =>
                            {
                                return response.text()
                            })
                        .then(data=>
                            {

                                data = xmlparser.xml2json(data,
                                                {
                                                    compact: true,
                                                    space: 4
                                                });
                                data = JSON.parse(data);
                                return data.feed.author.name._text
                            })
                        .catch(error=>{return error;});   
    return {YTChannelId, channelId, channelName:name}
}