package me.despawningbone.discordbot.command;

import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import java.time.OffsetDateTime;
import java.time.temporal.ChronoUnit;
//import java.time.OffsetDateTime;
//import java.time.temporal.ChronoUnit;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;

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

import me.despawningbone.discordbot.DiscordBot;
import me.despawningbone.discordbot.command.CommandResult.CommandResultType;
import me.despawningbone.discordbot.utils.MiscUtils;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.TextChannel;
import net.dv8tion.jda.api.entities.User;

public class Command {  //no need to be abstract anymore  //make it final to ensure thread safety?
	protected String name;
	protected String desc;
	protected String usage;
	protected List<String> alias; //fixed-size in this case is negligible, as it will not be edited later on anyways
	protected List<String> remarks = null;
	protected boolean isDisabled;
	protected EnumSet<Permission> perms = EnumSet.noneOf(Permission.class);  //so that not configured commands would have a default of no perms needed, instead of null  //WILL NOT ALLOW DENY OVERRIDE FOR NOW
	protected int botUserLevel; //0 = everyone, 1 = BotMod, 2 = BotOwner; negative value = either BotUser or required perm
	protected List<String> examples;
	
	protected ExecuteImpl impl = null;
	private Command parent = null;
	
	private String cat; //not used; remove?   //actually gets used now
	
	private LinkedHashMap<String, Command> subCmds = new LinkedHashMap<>();   //preserve order
	public HashMap<String, Command> subCmdAliases = new HashMap<String, Command>();  //TODO temporary solution
	
	//DONE add examples; what about subcmds?
	
	//for overriding
	protected Command() {}
	
	//only used by nested commands, since all explicitly declared commands override execute(); hot creation of subcommands are possible, but permission setting via !desp settings is not supported
	public Command(String name, List<String> aliases, Command.ExecuteImpl exeFunc, String usage, List<String> examples, String desc, List<String> remarks, EnumSet<Permission> defaultPerms, int botUserLevel) {
		this.name = name;
		this.alias = aliases;
		this.impl = exeFunc;
		this.usage = usage;
		this.examples = examples;
		this.desc = desc;
		this.remarks = remarks;
		this.perms = defaultPerms;
		this.botUserLevel = botUserLevel;
	}
	
	public interface ExecuteImpl {   //DONE make this a seperate class to store the ever increasing params?	if so, protected constructor since it doesnt need to be instantiated elsewhere other than registerSubCommand here 	//implemented nested commands instead
		public CommandResult execute(TextChannel channel, User author, Message msg, String[] args);
	}
	
	private void setParent(Command parent) {  //private so only register function can access to avoid accidental use	
		this.parent = parent;
	}
	
	//for anonymous inner classes if needed
	public void registerSubCommand(Command subCmd) {
		subCmd.setParent(this);  //make parent only set on register since subcmds with wrong/no parents can result in finnicky bugs
		subCmds.put(subCmd.getName(), subCmd);
		if(subCmd.getAliases() != null) subCmd.getAliases().forEach(a -> subCmdAliases.put(a, subCmd));
	}
	
	public void registerSubCommand(String name, List<String> aliases, Command.ExecuteImpl exe, String usage, List<String> examples, String desc, List<String> remarks) {  //subcmd has its own examples?   //DONE subcmd permissions and botuserlevel  //probably not needed, as its gonna be binded to guild instead of cmds soon enough
		registerSubCommand(name, aliases, exe, usage, examples, desc, remarks, EnumSet.noneOf(Permission.class), 0);
	}
	
	public void registerSubCommand(String name, List<String> aliases, Command.ExecuteImpl exe, String usage, List<String> examples, String desc, List<String> remarks, EnumSet<Permission> defaultPerms, int botUserLevel) {
		Command subCmd = new Command(name, aliases, exe, usage, examples, desc, remarks, defaultPerms, botUserLevel);
		registerSubCommand(subCmd);
	}
	
	public boolean hasSubCommand() {
		return !subCmds.isEmpty();
	}

	public Set<String> getSubCommandNames() {
		return subCmds.keySet();
	}
	
	public Command getSubCommand(String name) {
		Command subCmd = subCmds.get(name);
		return subCmd == null ? subCmdAliases.get(name) : subCmd;
	}
	
	public Command getParent() {
		return parent;
	}
	
	public enum BotUserLevel {  //DONE? implement this DONE test this
		DEFAULT,
		BOT_MOD,
		BOT_OWNER
	}
	
	public String getCategory() {  //is null if it is a subcommand
		return cat;
	}
	
	public String getName() {
		return name;
	}
	
	public String getDesc() {
		return desc;
	}
	
	public String getUsage() {
		return usage;
	}
	
	public List<String> getAliases() {
		return alias;
	}
	
	public List<String> getRemarks() {
		return remarks;
	}
	
	public EnumSet<Permission> getDefaultPerms() {  //DONE make this channel dependent?
		return perms;
	}
	
	public int getRequiredBotUserLevel() {  
		return botUserLevel;
	}
	
	public List<String> getExamples() {
		return examples;
	}
	
	public boolean isDisabled() {  //FIXED think of what to do with disabledGuild; it makes the command class not thread safe; store in DB instead, and be channel based?
		return isDisabled;
	}
	
	//even when multiple thread references to one specific command, it should be thread safe because it doesnt modify anything in the execute stage; and even the hashmap is somewhat immutable as it will never be changed after finished loading
	//i dont think i need to even volatile that hashmap
	//the only thing i might need to do to make it thread safe is to make the hashmap a concurrenthashmap
	public CommandResult execute(TextChannel channel, User author, Message msg, String[] args) {  //if you want a main command to work with sub commands, just super() this and then write the main command stuff
		OffsetDateTime timesent = OffsetDateTime.now();
		try(Connection con = DiscordBot.db.getConnection(); Statement s = con.createStatement()) {  //not the best, but i cant share connection through threads
			String prefix = MiscUtils.getPrefix(s, channel.getGuild().getId());
			if(hasSubCommand()) {
				if(args.length > 0) {
					Command subCmd = getSubCommand(args[0].toLowerCase());
					if(subCmd != null) {
						int botUserLevel = subCmd.getRequiredBotUserLevel(); Command temp = subCmd;
						while(temp.getParent() != null && botUserLevel == 0) {
							temp = temp.getParent();
							botUserLevel = temp.getRequiredBotUserLevel();  //if not set get parent's 
						}

						String perms = subCmd.hasSubCommand() ? null : MiscUtils.getMissingPerms(MiscUtils.getActivePerms(s, channel, subCmd), botUserLevel, channel.getGuild().getMember(author), channel);  //yet again pass to handler
						if(perms == null) {
							OffsetDateTime timeReceived = OffsetDateTime.now();
							long ms = Math.abs(timesent.until(timeReceived, ChronoUnit.MILLIS));
							System.out.println("subcmd parse Time taken: " + ms + "ms");
							return subCmd.execute(channel, author, msg, Arrays.copyOfRange(args, 1, args.length));							
						} else if(perms.equals("DISABLED") || this.isDisabled) {
							msg.addReaction("❎").queue();
							return new CommandResult(CommandResultType.DISABLED);
						} else {
							return new CommandResult(CommandResultType.NOPERMS, perms);
						}
							
					} else {
						if(impl == null) return new CommandResult(CommandResultType.INVALIDARGS, "Unknown sub command! Check `" + prefix + "help " + this.getName() + "` for more info.");				
					}
				} else {  //only run if no execute definition, if not let it override default messages
					if(impl == null) return new CommandResult(CommandResultType.INVALIDARGS, "Please specify a subcommand. You can view them with `" + prefix + "help " + this.name + "`.");
				}			
			}
			
			return impl.execute(channel, author, msg, args); //fallback if no subcommand found; useful for defining custom messages or default command for a set of subcommands 
		} catch (SQLException e) {
			return new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(e));
		}
	}
	
	public void executeAsync(TextChannel channel, User author, Message msg, String[] args, Consumer<CommandResult> success) {
		OffsetDateTime timesent = OffsetDateTime.now();
		CompletableFuture.supplyAsync(() -> execute(channel, author, msg, args))
		.exceptionally(ex -> new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(ex.getCause())))  //getCause should always work as ex is a CompletionException
		.thenAccept(success).thenAccept(r -> {  //DONE thenAccept parses the CommandResult
			OffsetDateTime timeReceived = OffsetDateTime.now();
			long ms = Math.abs(timesent.until(timeReceived, ChronoUnit.MILLIS));
			System.out.println("Time taken: " + ms + "ms");
		});
	}
	
}
