package me.despawningbone.discordbot.command.admin;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.List;
import java.util.ArrayList;
import java.util.stream.Collectors;

import org.apache.commons.lang3.exception.ExceptionUtils;

import me.despawningbone.discordbot.DiscordBot;
import me.despawningbone.discordbot.command.Command;
import me.despawningbone.discordbot.command.CommandResult;
import me.despawningbone.discordbot.command.CommandResult.CommandResultType;
import me.despawningbone.discordbot.utils.MiscUtils;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.Permission;

public class Settings extends Command {
	
	public static final int DISABLED = 0x80000;   //even if i use 0x80000000 and it overflows it should still work   //DONE 0x200 is a new permission; JDA 3 will probably not recognize it so i have to circumvent over it manually  //changed to 0x80000
	private static final List<String> disableExempt = Arrays.asList("admin", "admin.settings", "admin.settings.perms"); 
	
	public Settings() {
		this.alias = Arrays.asList("setting", "set");
		this.desc = "Change the settings of the bot for this guild!";
		this.usage = "<subcommands>";
		this.botUserLevel = -BotUserLevel.BOT_OWNER.ordinal();
		this.perms = EnumSet.of(Permission.MANAGE_SERVER);
		//PATCHED? SQL INJECTION PRONE (eg node: games/**/WHERE1--.",*," - returns numberformatexception;  games/**/WHERE1--.osu",*,"osu returns N/A; can even drop tables)
		registerSubCommand("perms", Arrays.asList("permissions", "perm", "permission"), (channel, user, msg, words) -> {  //DONE disable commands; use negative?  //TODO list all edited permission nodes, clear command?
			int opType = 3;
			try {
				if(words[0].equalsIgnoreCase("list")) {
					if(words.length != 2) throw new ArrayIndexOutOfBoundsException();  //invalid argument count
					opType = 0;
				} else if(words[0].equalsIgnoreCase("wipe") || words[0].equalsIgnoreCase("wipeall")){
					opType = 1;
				} else {
					if(words.length != 3) throw new ArrayIndexOutOfBoundsException();  //invalid argument count
					opType = words[0].equalsIgnoreCase("guild") ? 2 : 3;
				}
			} catch(ArrayIndexOutOfBoundsException e) {
				return new CommandResult(CommandResultType.INVALIDARGS, "Invalid argument count specified.");
			}
			if(!words[1].matches("[a-zA-Z0-9.]+")) return new CommandResult(CommandResultType.INVALIDARGS, "Invalid node specified.");  //prevent SQL injection; might be restricting though, but then again all command names are alphanumeric
			try(Connection con = DiscordBot.db.getConnection(); Statement s = con.createStatement()){
				String node = words[1].contains(".") ? words[1].substring(words[1].indexOf(".") + 1).toLowerCase() : "_GLOBAL_";
				String cat = node.equals("_GLOBAL_") ? words[1].toLowerCase() : words[1].substring(0, words[1].indexOf(".")).toLowerCase();
				ResultSet rs = null;
				if(opType == 3) try {
					if(channel.getGuild().getTextChannelById(words[0]) == null) throw new NumberFormatException();
				} catch(NumberFormatException e) {
					if(words[0].startsWith("#")) {
						if(!msg.getMentionedChannels().isEmpty()) {
							words[0] = msg.getMentionedChannels().get(0).getId(); //assuming its to order from left to right; but tbh if they try to trick the command with 2 channels it wont work anyways with the range check and node check
						}
					} else { 
						return new CommandResult(CommandResultType.INVALIDARGS, "Invalid channel specified.");	
					}
				}
				try {
					rs = s.executeQuery("SELECT \"" + node + "\" FROM perms_" + cat + " WHERE id = " + channel.getGuild().getId() + ";");  //hopefully this will be pretty fast	
				} catch(SQLException x) {
					return new CommandResult(CommandResultType.INVALIDARGS, "Invalid node specified.");
				}
				
				EnumSet<Permission> def = null;
				if(node.contains(".")) {  //path must be valid coz or else SQL wouldve thrown an exception
					String[] split = node.split("\\.");
					Command subCmd = DiscordBot.commands.get(split[0]);
					for(int i = 1; i < split.length; i++) {
						subCmd = subCmd.getSubCommand(split[i]);
					}
					def = subCmd.getDefaultPerms();
				} else {
					def = DiscordBot.commands.containsKey(node) ? DiscordBot.commands.get(node).getDefaultPerms() : EnumSet.noneOf(Permission.class);
				}
				
				if(opType == 0) {  //list perms
					EmbedBuilder eb = new EmbedBuilder();
					eb.setTitle("Permissions for node: " + words[1]);
					
					eb.appendDescription("*Default: " + (def.isEmpty() ? "N/A" : def.stream().map(p -> p.name()).collect(Collectors.joining(", "))) + "*\n\n");
					if(rs.next()) {
						String sOrig = rs.getString(1);
						if(sOrig.equals(node)) return new CommandResult(CommandResultType.INVALIDARGS, "Invalid node specified.");   //SQL returns the actual string if the table doesnt have the column
						for(String line : sOrig.split("\n")) {
							String[] split = line.split(":");
							int index = 0;
							if(split.length < 2) {  //channels would have colons
								eb.appendDescription("**Global permissions:** \n");
							} else {
								eb.appendDescription("<#" + split[0] + ">:\n");  //TODO low priority: old permissions set for deleted channels will still be there
								index = 1;
							}
							long orig = Long.parseLong(split[index]);
							if(orig != 0) {
								long deny = (int) (orig >> 32), allow = (int) orig;
								if(MiscUtils.hasDisabled(deny)) eb.appendDescription("**-DISABLED**\n");
								for(Permission p : Permission.getPermissions(deny)) eb.appendDescription("-" + p.name() + "\n");
								if(MiscUtils.hasDisabled(allow)) eb.appendDescription("**DISABLED**\n");
								for(Permission p : Permission.getPermissions(allow)) eb.appendDescription(p.name() + "\n");								
							} else {
								eb.appendDescription("N/A\n");
							}
							eb.appendDescription("\n");
						}
					} else {  //edit perms
						eb.appendDescription("Global permissions: \n");
						if(!def.isEmpty()) for(Permission p : def) eb.appendDescription(p.name() + "\n");
						else eb.appendDescription("N/A\n");
					}
					eb.setFooter("To see the permissions in effect, do the help command in an affected channel!", null);
					channel.sendMessageEmbeds(eb.build()).queue();
					return new CommandResult(CommandResultType.SUCCESS);
				} else if(opType == 1) {  //wipe
					String actualNode = words[1].toLowerCase();
					if(words[0].equalsIgnoreCase("wipeall")) {
						s.execute("DELETE FROM perms_" + cat + " WHERE id = " + channel.getGuild().getId() + ";");
						actualNode = cat;
					} else {
						s.execute("UPDATE perms_" + cat + " SET \"" + node + "\" = '" + ((0L << 32) | (Permission.getRaw(def) & 0xffffffffL)) + "\n'");
					}
					channel.sendMessage("Successfully reset `" + actualNode + "` to default permissions.").queue();
					return new CommandResult(CommandResultType.SUCCESS);
				} else {
					boolean allows = words[2].indexOf("-") == -1;
					Permission perm = null;
					try {
						String p = allows ? words[2].toUpperCase() : words[2].substring(1).toUpperCase();
						if(!p.equals("DISABLED")) perm = Permission.valueOf(p);
						if(perm == null && disableExempt.contains(words[1].toLowerCase())) return new CommandResult(CommandResultType.INVALIDARGS, "You cannot disable this node.");  //temporary patch 
					} catch(IllegalArgumentException e) {
						return new CommandResult(CommandResultType.INVALIDARGS, "Invalid permission specified.");
					}
					String[] lines = {""};
					String type = "";
					if(rs.next()) {  //considerations: rs.next(); guild vs channel; allows; add or remove;
						lines = rs.getString(1).split("\n");
						if(lines[0].equals(node)) return new CommandResult(CommandResultType.INVALIDARGS, "Invalid node specified.");  //SQL returns the actual string if the table doesnt have the column
						int i = 0;
						long orig = 0;
						if(opType == 2) {
							orig = Long.parseLong(lines[0]);
						} else {
							try {
								for(i = 0; i <= lines.length; i++) {
									if(lines[i].startsWith(words[0] + ":")) {
										orig = Long.parseLong(lines[i].split(":")[1]);
										break;
									}
								}
							} catch(ArrayIndexOutOfBoundsException e) {
								lines = Arrays.copyOf(lines, lines.length + 1);  //to extend it; should be the same method as using arraylist
								//orig = Permission.getRaw(def);  //use default   //actually should already have imposed global default if rs has next; adding default to this will only give channel the global perm, which makes no sense 
							}
						}
						String[] split = imposePerms(words[0].equalsIgnoreCase("guild") ? null : words[0], orig, perm, allows);
						type = split[0]; lines[i] = split[1];  //at this stage theres only 2 options: guild and a valid channel id
					} else {
						if(opType == 2) {
							String[] split = imposePerms(null, Permission.getRaw(def), perm, allows);  //initiate guild by imposing perm to default; since allow is in the lower 32 bit, i can just put def as the base
							type = split[0]; lines[0] = split[1];
						} else {
							lines[0] = String.valueOf(Permission.getRaw(def));  //initiate guild with the default permission of the command
							lines = Arrays.copyOf(lines, lines.length + 1);
							String[] split = imposePerms(words[0], 0, perm, allows);  //impose with perm base = 0
							type = split[0]; lines[1] = split[1];
						}
					}
					String fin = String.join("\n", lines) + "\n";  //last line must have \n 
					
					s.execute("INSERT INTO perms_" + cat + "(id, \"" + node + "\") VALUES (" + channel.getGuild().getId() + ", '" + fin + "') ON CONFLICT(id) DO UPDATE SET \"" + node + "\" = '" + fin + "';");  //no need blank update check, because it will never be the same
					channel.sendMessage("Successfully " + type + " " + (words[0].equalsIgnoreCase("guild") ? "guild-wide" : "channel-specific (<#" + words[0] + ">)") + " permission `" + (allows ? "" : "-") + (perm == null ? "DISABLED" : perm.name()) + "` for `" + words[1].toLowerCase() + "`.").queue();
					return new CommandResult(CommandResultType.SUCCESS);
				}	
			} catch(SQLException e) {
				return new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(e));
			}
			
		}, "<channel|guild|list|wipe|wipeall> <command node> [-][permission]", Arrays.asList("list admin", "list games.osu.pp", "wipeall admin", "wipe games.osu", "419464253574086656 games MESSAGE_EMBED_LINKS", "guild games.osu -MESSAGE_EMBED_LINKS", "#general music VOICE_CONNECT", "guild admin ADMINISTRATOR"),
				"Edit default perms required for commands for this guild!", Arrays.asList(
				"Adding a `-` sign before a permission overrides the parent permission set,",
				"Where `cat(guild > channel) > cmd(guild > channel) > subcmd(guild > channel)` (parent > child, where subcmd channel is of highest priority).",
				"*For example, specifying `admin.settings.prefix` with `-MANAGE_SERVER` will override the requirement of `admin.settings`, and allow those without `MANAGE_SERVER` to use `admin.settings.prefix`*.\n",
				"The permission nodes are as such: `<cat>[.cmd][.subcmd]`.",
				"For permission names, please refer to https://ci.dv8tion.net/job/JDA/javadoc/net/dv8tion/jda/core/Permission.html,",
				"Along with a special permission `DISABLED` for disabling the node.",
				"Specifying the permission twice will remove it.\n", 
				"Use `wipe` if you want to reset the permissions for the node, and `wipeall` for resetting all perms in the whole category.",
				"`list` and `wipe`/`wipeall` only accepts the first 2 parameters."), EnumSet.of(Permission.ADMINISTRATOR), -BotUserLevel.BOT_OWNER.ordinal());
		
		registerSubCommand("prefix", Arrays.asList("pre"), (channel, user, msg, words) -> {  //dont allow prefixes with markdown for now
			String prefix;
			if(words.length < 1) {
				prefix = DiscordBot.prefix;
				channel.sendMessage("No prefix entered. Using default prefix...").queue();
			} else {
				prefix = msg.getContentStripped().substring(msg.getContentDisplay().lastIndexOf(String.join(" ", words))).replaceAll("\\\\", "");  //if prefix/shortcut contains markdown it breaks
			}
			if(prefix.length() > 30) return new CommandResult(CommandResultType.INVALIDARGS, "Please do not enter prefixes that are too long.");  //arbitrarily set; can change anytime 
			try (Connection con = DiscordBot.db.getConnection()) {
				PreparedStatement s = con.prepareStatement("INSERT INTO settings(id, prefix) VALUES (" + channel.getGuild().getId() + ", ?) ON CONFLICT(id) DO UPDATE SET prefix = ? WHERE prefix <> ?");
				s.setString(1, prefix); s.setString(2, prefix); s.setString(3, prefix);  //using prep statement to prevent injection
				int update = s.executeUpdate();
				s.close();
				if(update != 0) {  //should ever only be 1 or 0
					channel.sendMessage("Successfully changed prefix to `" + prefix + "`. Do " + prefix + "help to see the new command syntaxes.").queue();
				} else {
					return new CommandResult(CommandResultType.INVALIDARGS, "You are setting the same prefix!");
				}
				return new CommandResult(CommandResultType.SUCCESS);
			} catch(SQLException e) {
				return new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(e));
			}
		}, "[new prefix]", Arrays.asList("`!desp `", "!", "-"),
				"Change the prefix for this guild!", Arrays.asList("*You can also include spaces with the use of `code block`s.*", "Leave blank to use the default (`" + DiscordBot.prefix + "`)."));
			
	}
	
	private String[] imposePerms(String cId, long base, Permission perm, boolean allows) {
		long allow = (int) base;
		long deny = base >> 32;
		String type; long newRaw = allows ? allow : deny;
		if(perm == null) {  //only disabled is null; since Permission doesnt have DISABLED mapped, i have to do it manually
			type = MiscUtils.hasDisabled(newRaw) ? "removed" : "added";
			if(type.equals("removed")) {
				newRaw &= ~DISABLED; 
			} else {
				newRaw |= DISABLED;
			}
		} else { 
			List<Permission> perms = new ArrayList<>(Permission.getPermissions(newRaw));
			if(perms.contains(perm)) {
				perms.remove(perm);
				type = "removed";
			} else {
				perms.add(perm);
				type = "added";
			} 
			newRaw = Permission.getRaw(perms) | (MiscUtils.hasDisabled(newRaw) ? DISABLED : 0);  //add back as perms remove it
		}
		if(allows) allow = newRaw; else deny = newRaw;
		return new String[]{type, (cId == null ? "" : cId + ":") + ((deny << 32) | (allow & 0xffffffffL))};
	}
}
