package tc.oc.commons.bukkit.nick;

import java.util.Objects;
import java.util.stream.Stream;
import javax.annotation.Nullable;
import javax.inject.Inject;
import javax.inject.Singleton;

import com.google.common.base.Function;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.sk89q.minecraft.util.commands.Command;
import com.sk89q.minecraft.util.commands.CommandContext;
import com.sk89q.minecraft.util.commands.CommandException;
import com.sk89q.minecraft.util.commands.CommandPermissions;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.chat.BaseComponent;
import net.md_5.bungee.api.chat.TranslatableComponent;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.permissions.Permission;
import org.bukkit.permissions.PermissionAttachment;
import org.bukkit.permissions.PermissionDefault;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.PluginManager;
import tc.oc.api.bukkit.users.OnlinePlayers;
import tc.oc.api.docs.PlayerId;
import tc.oc.api.docs.User;
import tc.oc.api.docs.virtual.UserDoc;
import tc.oc.api.exceptions.UnprocessableEntity;
import tc.oc.minecraft.scheduler.SyncExecutor;
import tc.oc.api.users.UserService;
import tc.oc.commons.bukkit.chat.Audiences;
import tc.oc.commons.bukkit.chat.NameStyle;
import tc.oc.commons.bukkit.chat.PlayerComponent;
import tc.oc.commons.bukkit.chat.WarningComponent;
import tc.oc.commons.bukkit.commands.CommandUtils;
import tc.oc.commons.bukkit.commands.UserFinder;
import tc.oc.commons.bukkit.event.UserLoginEvent;
import tc.oc.commons.bukkit.localization.Translations;
import tc.oc.commons.core.chat.Audience;
import tc.oc.commons.core.chat.Component;
import tc.oc.commons.core.commands.CommandFutureCallback;
import tc.oc.commons.core.commands.Commands;
import tc.oc.commons.core.commands.ComponentCommandException;

@Singleton
public class NicknameCommands implements Listener, Commands {

    public static final String PERMISSION = "setting.nick";
    public static final String PERMISSION_SET = PERMISSION + ".set";
    public static final String PERMISSION_GET = PERMISSION + ".get";
    public static final String PERMISSION_IMMEDIATE = PERMISSION + ".immediate";
    public static final String PERMISSION_ANY = PERMISSION + ".any";
    public static final String PERMISSION_ANY_SET = PERMISSION_ANY + ".set";
    public static final String PERMISSION_ANY_GET = PERMISSION_ANY + ".get";

    private final NicknameConfiguration config;
    private final SyncExecutor syncExecutor;
    private final UserService userService;
    private final Audiences audiences;
    private final IdentityProvider identities;
    private final OnlinePlayers onlinePlayers;
    private final UserFinder userFinder;
    private final PluginManager pluginManager;
    private final Plugin plugin;

    @Inject NicknameCommands(NicknameConfiguration config,
                             SyncExecutor syncExecutor,
                             UserService userService,
                             Audiences audiences,
                             IdentityProvider identities,
                             OnlinePlayers onlinePlayers,
                             UserFinder userFinder,
                             PluginManager pluginManager,
                             Plugin plugin) {
        this.config = config;
        this.syncExecutor = syncExecutor;
        this.userService = userService;
        this.audiences = audiences;
        this.identities = identities;
        this.onlinePlayers = onlinePlayers;
        this.userFinder = userFinder;
        this.pluginManager = pluginManager;
        this.plugin = plugin;
    }

    @Override
    public void enable() {
        final PermissionAttachment attachment = Bukkit.getConsoleSender().addAttachment(plugin);
        Stream.of(
            PERMISSION,
            PERMISSION_GET,
            PERMISSION_SET,
            PERMISSION_ANY,
            PERMISSION_ANY_GET,
            PERMISSION_ANY_SET,
            PERMISSION_IMMEDIATE
        ).forEach(name -> {
            final Permission permission = new Permission(name, PermissionDefault.FALSE);
            pluginManager.addPermission(permission);
            attachment.setPermission(permission, true);
        });
    }

    private static boolean isSelf(CommandSender sender, @Nullable String username) {
        return username == null || username.equals(sender.getName());
    }

    private void assertWritePerms(CommandSender sender, boolean self, boolean immediate) throws CommandException {
        if(self) {
            CommandUtils.assertPermission(sender, PERMISSION_SET);
        } else {
            CommandUtils.assertPermission(sender, PERMISSION_ANY_SET);
        }

        if(immediate) {
            CommandUtils.assertPermission(sender, PERMISSION_IMMEDIATE);
        }
    }

    @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
    public void sendNickReminderOnLogin(UserLoginEvent event) {
        if(event.getUser().nickname() != null) {
            if(event.getPlayer().hasPermission(PERMISSION_SET)) {
                audiences.get(event.getPlayer()).sendMessage(new TranslatableComponent(
                    "nick.joinReminder",
                    new Component("/nick", ChatColor.GOLD),
                    new Component("/nick clear", ChatColor.GOLD)
                ));
            } else {
                set(event.getUser(), null, true);
            }
        }
    }

    @EventHandler(priority = EventPriority.MONITOR)
    public void sendNickChangeMessage(PlayerIdentityChangeEvent event) {
        if(event.getNewIdentity().getNickname() == null) {
            audiences.get(event.getPlayer()).sendMessage(new TranslatableComponent("command.nick.clearSelf.immediate"));
        } else {
            audiences.get(event.getPlayer()).sendMessage(new TranslatableComponent("command.nick.setSelf.immediate", highlight(event.getNewIdentity().getNickname())));
        }
    }

    @Command(aliases = {"nick" },
             usage = "list | show [player] | set <nickname> [player] | clear [player] | <nickname> [player]",
             desc = "Show, set, or clear a nickname for yourself or another player. " +
                    "Changes will take effect the next time the player " +
                    "connects to the server. The -i option makes the change " +
                    "visible immediately.",
             flags = "i",
             min = 0,
             max = 3)
    @CommandPermissions(PERMISSION)
    public void nick(final CommandContext args, final CommandSender sender) throws CommandException {
        if(!config.enabled()) {
            throw new CommandException(Translations.get().t("command.nick.notEnabled", sender));
        }

        final boolean immediate = args.hasFlag('i');

        if(args.argsLength() == 0) {
            show(sender, null);
        } else {
            final String arg = args.getString(0);
            switch(arg) {
                case "list":
                    list(sender);
                    break;

                case "show":
                    show(sender, args.getString(1, null));
                    break;

                case "set":
                    if(args.argsLength() < 2) CommandUtils.notEnoughArguments(sender);
                    set(sender, args.getString(1), args.getString(2, null), immediate);
                    break;

                case "clear":
                    set(sender, null, args.getString(1, null), immediate);
                    break;

                default:
                    set(sender, arg, args.getString(1, null), immediate);
                    break;
            }
        }
    }

    public void list(final CommandSender sender) throws CommandException {
        CommandUtils.assertPermission(sender, PERMISSION_ANY_GET);

        final Audience audience = audiences.get(sender);
        boolean some = false;

        for(Player player : onlinePlayers.all()) {
            final Identity identity = identities.currentIdentity(player);
            if(identity.getNickname() != null) {
                some = true;
                audience.sendMessage(new PlayerComponent(identity, NameStyle.VERBOSE));
            }
        }

        if(!some) {
            audience.sendMessage(new TranslatableComponent("command.nick.noActiveNicks"));
        }
    }

    public void show(final CommandSender sender, final @Nullable String username) throws CommandException {
        final boolean self = isSelf(sender, username);
        final Audience audience = audiences.get(sender);

        syncExecutor.callback(
            userFinder.findUser(sender, username, UserFinder.Scope.ALL, UserFinder.Default.SENDER),
            CommandFutureCallback.onSuccess(sender, result -> {
                final Identity identity = identities.currentIdentity(result.user);

                if(self || identity.isFriend(sender)) {
                    CommandUtils.assertPermission(sender, PERMISSION_GET);
                } else {
                    CommandUtils.assertPermission(sender, PERMISSION_ANY_GET);
                }

                final String currentNick = identity.getNickname();
                final String pendingNick = result.user.nickname();

                final String who = self ? "Self" : "Other";
                final PlayerComponent name = self ? null : new PlayerComponent(identity, NameStyle.FANCY);

                TranslatableComponent message = null;

                if(currentNick != null) {
                    message = new TranslatableComponent("command.nick.set" + who + ".immediate", highlight(currentNick));
                } else if(pendingNick == null) {
                    message = new TranslatableComponent("command.nick.clear" + who + ".immediate");
                }

                if(message != null) {
                    if(name != null) message.addWith(name);
                    audience.sendMessage(message);
                }

                if(!Objects.equals(currentNick, pendingNick)) {
                    if(pendingNick != null) {
                        message = new TranslatableComponent("command.nick.set" + who + ".queued", highlight(pendingNick));
                    } else {
                        message = new TranslatableComponent("command.nick.clear" + who + ".queued");
                    }

                    if(name != null) message.addWith(name);
                    audience.sendMessage(message);
                }
            })
        );
    }

    private static Component highlight(String nickname) {
        return new Component(nickname, ChatColor.AQUA);
    }

    public @Nullable BaseComponent invalidReason(@Nullable String nickname) {
        if(nickname == null) return null;

        if(!nickname.matches("^[A-Za-z0-9_]+$")) {
            return new TranslatableComponent("command.nick.invalidCharacters");
        }
        if(nickname.length() > 16) {
            return new TranslatableComponent("command.nick.tooLong");
        }
        if(nickname.length() < 4) {
            return new TranslatableComponent("command.nick.tooShort");
        }
        if(onlinePlayers.find(nickname) != null) {
            return new TranslatableComponent("command.nick.nickTaken", highlight(nickname));
        }
        return null;
    }

    private void validate(@Nullable String nickname) throws CommandException {
        final BaseComponent reason = invalidReason(nickname);
        if(reason != null) {
            throw new ComponentCommandException(reason);
        }
    }

    public void set(final CommandSender sender, final @Nullable String nickname, final @Nullable String username, final boolean immediate) throws CommandException {
        final boolean self = isSelf(sender, username);

        // Don't need perms to clear your own nickname, only to set it
        if(!(self && nickname == null)) {
            assertWritePerms(sender, self, immediate);
        }

        final Audience audience = audiences.get(sender);

        if(nickname != null) {
            validate(nickname);
            audience.sendMessage(new TranslatableComponent("command.nick.checkingNickname", highlight(nickname)));
        }

        // TODO: a way to do this with only one API call instead of two
        syncExecutor.callback(
            userFinder.findUser(sender, username, UserFinder.Scope.ALL, UserFinder.Default.SENDER),
            CommandFutureCallback.onSuccess(sender, response -> {
                syncExecutor.callback(
                    set(response.user, nickname, immediate),
                    CommandFutureCallback.<User>onSuccess(sender, user -> {
                        if(!(self && immediate)) {
                            final String key = "command.nick." +
                                               (nickname == null ? "clear" : "set") +
                                               (self ? "Self" : "Other") +
                                               (immediate ? ".immediate" : ".queued");

                            final TranslatableComponent message = new TranslatableComponent(key);
                            if(nickname != null) message.addWith(highlight(nickname));
                            if(!self) message.addWith(new PlayerComponent(identities.createIdentity(user, null), NameStyle.FANCY));

                            audience.sendMessage(message);
                        }
                    }).onFailure(UnprocessableEntity.class, ex -> {
                        // Assume any validation error is a username collision
                        audience.sendMessage(new WarningComponent("command.nick.nickTaken", highlight(nickname)));
                    })
                );
            })
        );
    }

    public ListenableFuture<User> set(PlayerId playerId, @Nullable String nickname, boolean immediate) {
        final BaseComponent reason = invalidReason(nickname);
        if(reason != null) {
            return Futures.immediateFailedFuture(new ComponentCommandException(reason));
        }

        return Futures.transform(
            userService.update(playerId, (UserDoc.Nickname) () -> nickname),
            (Function<User, User>) user -> {
                if(immediate) {
                    final Player player = onlinePlayers.find(user);
                    if(player != null) {
                        identities.changeIdentity(player, nickname);
                    }
                }
                return user;
            },
            syncExecutor
        );
    }
}
