package com.earth2me.essentials;

import com.earth2me.essentials.config.ConfigurateUtil;
import com.earth2me.essentials.config.EssentialsUserConfiguration;
import com.earth2me.essentials.config.entities.CommandCooldown;
import com.earth2me.essentials.config.entities.LazyLocation;
import com.earth2me.essentials.config.holders.UserConfigHolder;
import com.earth2me.essentials.userstorage.ModernUserMap;
import com.earth2me.essentials.utils.NumberUtil;
import com.earth2me.essentials.utils.StringUtil;
import com.google.common.base.Charsets;
import net.ess3.api.IEssentials;
import net.ess3.api.MaxMoneyException;
import net.ess3.api.TranslatableException;
import net.essentialsx.api.v2.services.mail.MailMessage;
import org.bukkit.ChatColor;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;

import java.io.File;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.logging.Level;
import java.util.regex.Pattern;

public abstract class UserData extends PlayerExtension implements IConf {
    protected final transient Essentials ess;
    private final EssentialsUserConfiguration config;
    private UserConfigHolder holder;
    private BigDecimal money;

    protected UserData(final Player base, final IEssentials ess) {
        super(base);
        this.ess = (Essentials) ess;
        final File folder = new File(ess.getDataFolder(), "userdata");
        if (!folder.exists() && !folder.mkdirs()) {
            throw new RuntimeException("Unable to create userdata folder!");
        }

        config = new EssentialsUserConfiguration(base.getName(), base.getUniqueId(), new File(folder, base.getUniqueId() + ".yml"));
        config.setSaveHook(() -> {
            config.setRootHolder(UserConfigHolder.class, holder);
        });
        reloadConfig();

        if (config.getUsername() == null) {
            config.setUsername(getLastAccountName());
        }
    }

    public final void reset() {
        config.blockingSave();
        if (!config.getFile().delete()) {
            ess.getLogger().warning("Unable to delete data file for " + config.getFile().getName());
        }
        if (config.getUsername() != null) {
            final ModernUserMap users = (ModernUserMap) ess.getUsers();
            users.invalidate(config.getUuid());
            if (isNPC()) {
                final String name = ess.getSettings().isSafeUsermap() ? StringUtil.safeString(config.getUsername()) : config.getUsername();
                users.invalidate(UUID.nameUUIDFromBytes(("NPC:" + name).getBytes(Charsets.UTF_8)));
            }
        }
    }

    public final void cleanup() {
        config.blockingSave();
    }

    @Override
    public final void reloadConfig() {
        config.load();
        try {
            holder = config.getRootNode().get(UserConfigHolder.class);
        } catch (Throwable e) {
            ess.getLogger().log(Level.SEVERE, "Error while reading user config: " + config.getFile().getName(), e);
            throw new RuntimeException(e);
        }
        money = _getMoney();
    }

    private BigDecimal _getMoney() {
        BigDecimal result = ess.getSettings().getStartingBalance();
        final BigDecimal maxMoney = ess.getSettings().getMaxMoney();
        final BigDecimal minMoney = ess.getSettings().getMinMoney();

        // NPC banks are not actual player banks, as such they do not have player starting balance.
        if (isNPC()) {
            result = BigDecimal.ZERO;
        }

        if (holder.money() != null) {
            result = holder.money();
        }
        if (result.compareTo(maxMoney) > 0) {
            result = maxMoney;
        }
        if (result.compareTo(minMoney) < 0) {
            result = minMoney;
        }
        holder.money(result);

        return holder.money();
    }

    public BigDecimal getMoney() {
        return money;
    }

    public void setMoney(final BigDecimal value, final boolean throwError) throws MaxMoneyException {
        final BigDecimal maxMoney = ess.getSettings().getMaxMoney();
        final BigDecimal minMoney = ess.getSettings().getMinMoney();
        if (value.compareTo(maxMoney) > 0) {
            if (throwError) {
                throw new MaxMoneyException();
            }
            money = maxMoney;
        } else {
            money = value;
        }
        if (money.compareTo(minMoney) < 0) {
            money = minMoney;
        }
        holder.money(money);
        stopTransaction();
    }

    private String getHomeName(String search) {
        if (NumberUtil.isInt(search)) {
            try {
                search = getHomes().get(Integer.parseInt(search) - 1);
            } catch (final NumberFormatException | IndexOutOfBoundsException ignored) {
            }
        }
        return search;
    }

    public Location getHome(final String name) {
        final String search = getHomeName(name);
        final LazyLocation loc = holder.homes().get(search);
        return loc != null ? loc.location() : null;
    }

    public boolean hasValidHomes() {
        for (final LazyLocation loc : holder.homes().values()) {
            if (loc != null && loc.location() != null) {
                return true;
            }
        }
        return false;
    }

    public Location getHome(final Location world) {
        if (getHomes().isEmpty()) {
            return null;
        }
        for (final String home : getHomes()) {
            final Location loc = holder.homes().get(home).location();
            if (loc != null && world.getWorld() == loc.getWorld()) {
                return loc;
            }

        }
        return holder.homes().get(getHomes().get(0)).location();
    }

    public List<String> getHomes() {
        return new ArrayList<>(holder.homes().keySet());
    }

    public void setHome(String name, final Location loc) {
        //Invalid names will corrupt the yaml
        name = StringUtil.safeString(name);
        holder.homes().put(name, LazyLocation.fromLocation(loc));
        config.save();
    }

    public void delHome(final String name) throws Exception {
        String search = getHomeName(name);
        if (!holder.homes().containsKey(search)) {
            search = StringUtil.safeString(search);
        }
        if (holder.homes().containsKey(search)) {
            holder.homes().remove(search);
            config.save();
        } else {
            throw new TranslatableException("invalidHome", search);
        }
    }

    public void renameHome(final String name, final String newName) throws Exception {
        final LazyLocation location = holder.homes().remove(name);
        if (location != null) {
            holder.homes().put(StringUtil.safeString(newName), location);
            config.save();
        } else {
            throw new TranslatableException("invalidHome", name);
        }
    }

    public boolean hasHome() {
        return !holder.homes().isEmpty();
    }

    public boolean hasHome(final String name) {
        return holder.homes().containsKey(name);
    }

    public String getNickname() {
        return holder.nickname();
    }

    public void setNickname(final String nick) {
        holder.nickname(nick);
        config.save();
    }

    public Set<Material> getUnlimited() {
        return holder.unlimited();
    }

    public boolean hasUnlimited(final ItemStack stack) {
        return holder.unlimited().contains(stack.getType());
    }

    public void setUnlimited(final ItemStack stack, final boolean state) {
        final boolean wasUpdated;
        if (state) {
            wasUpdated = holder.unlimited().add(stack.getType());
        } else {
            wasUpdated = holder.unlimited().remove(stack.getType());
        }

        if (wasUpdated) {
            config.save();
        }
    }

    public void clearAllPowertools() {
        holder.powertools().clear();
        config.save();
    }

    public List<String> getPowertool(final ItemStack stack) {
        return getPowertool(stack.getType());
    }

    public List<String> getPowertool(final Material material) {
        return holder.powertools().get(material.name().toLowerCase(Locale.ENGLISH));
    }

    public void setPowertool(final ItemStack stack, final List<String> commandList) {
        if (commandList == null || commandList.isEmpty()) {
            holder.powertools().remove(stack.getType().name().toLowerCase(Locale.ENGLISH));
        } else {
            holder.powertools().put(stack.getType().name().toLowerCase(Locale.ENGLISH), commandList);
        }
        config.save();
    }

    public boolean hasPowerTools() {
        return !holder.powertools().isEmpty();
    }

    public Location getLastLocation() {
        final LazyLocation lastLocation = holder.lastLocation();
        return lastLocation != null ? lastLocation.location() : null;
    }

    public void setLastLocation(final Location loc) {
        if (loc == null || loc.getWorld() == null) {
            return;
        }
        holder.lastLocation(loc);
        config.save();
    }

    public Location getLogoutLocation() {
        final LazyLocation logoutLocation = holder.logoutLocation();
        return logoutLocation != null ? logoutLocation.location() : null;
    }

    public void setLogoutLocation(final Location loc) {
        if (loc == null || loc.getWorld() == null) {
            return;
        }
        holder.logoutLocation(loc);
        config.save();
    }

    public long getLastTeleportTimestamp() {
        return holder.timestamps().lastTeleport();
    }

    public void setLastTeleportTimestamp(final long time) {
        holder.timestamps().lastTeleport(time);
        config.save();
    }

    public long getLastHealTimestamp() {
        return holder.timestamps().lastHeal();
    }

    public void setLastHealTimestamp(final long time) {
        holder.timestamps().lastHeal(time);
        config.save();
    }

    public String getJail() {
        return holder.jail();
    }

    public void setJail(final String jail) {
        holder.jail(jail);
        config.save();
    }

    /**
     * @deprecated Mails are no longer just strings, this method is therefore misleading.
     */
    @Deprecated
    public List<String> getMails() {
        final List<String> list = new ArrayList<>();
        if (getMailAmount() != 0) {
            for (MailMessage mail : getMailMessages()) {
                // I hate this code btw
                list.add(mail.isLegacy() ? mail.getMessage() : ChatColor.GOLD + "[" + ChatColor.RESET + mail.getSenderUsername() + ChatColor.GOLD + "] " + ChatColor.RESET + mail.getMessage());
            }
        }
        return list;
    }

    /**
     * @deprecated This method does not support the new mail system and will fail at runtime.
     */
    @Deprecated
    public void setMails(List<String> mails) {
        throw new UnsupportedOperationException("UserData#setMails(List<String>) is deprecated and can no longer be used. Please tell the plugin author to update this!");
    }

    public int getMailAmount() {
        return holder.mail() == null ? 0 : holder.mail().size();
    }

    public int getUnreadMailAmount() {
        if (holder.mail() == null || holder.mail().isEmpty()) {
            return 0;
        }

        int unread = 0;
        for (MailMessage element : holder.mail()) {
            if (!element.isRead()) {
                unread++;
            }
        }
        return unread;
    }

    /**
     * @deprecated This method does not support the new mail system and should not be used.
     */
    @Deprecated
    abstract void addMail(final String mail);

    public ArrayList<MailMessage> getMailMessages() {
        return new ArrayList<>(holder.mail());
    }

    public void setMailList(ArrayList<MailMessage> messages) {
        holder.mail(messages);
        config.save();
    }

    public boolean isTeleportEnabled() {
        return holder.teleportEnabled();
    }

    public void setTeleportEnabled(final boolean set) {
        holder.teleportEnabled(set);
        config.save();
    }

    public boolean isAutoTeleportEnabled() {
        return holder.teleportAuto();
    }

    public void setAutoTeleportEnabled(final boolean set) {
        holder.teleportAuto(set);
        config.save();
    }

    @Deprecated
    public void setIgnoredPlayers(final List<String> players) {
        final List<UUID> uuids = new ArrayList<>();
        for (final String player : players) {
            final User user = ess.getOfflineUser(player);
            if (user == null) {
                return;
            }
            uuids.add(user.getBase().getUniqueId());
        }
        setIgnoredPlayerUUIDs(uuids);
    }

    public void setIgnoredPlayerUUIDs(final List<UUID> players) {
        holder.ignore(players);
        config.save();
    }

    @Deprecated
    public boolean isIgnoredPlayer(final String userName) {
        final IUser user = ess.getUser(userName);
        if (user == null || !user.getBase().isOnline()) {
            return false;
        }
        return isIgnoredPlayer(user);
    }

    public boolean isIgnoredPlayer(final IUser user) {
        return holder.ignore().contains(user.getBase().getUniqueId()) && !user.isIgnoreExempt();
    }

    public List<UUID> _getIgnoredPlayers() {
        return holder.ignore();
    }

    public void setIgnoredPlayer(final IUser user, final boolean set) {
        final UUID uuid = user.getBase().getUniqueId();
        if (set) {
            if (!holder.ignore().contains(uuid)) {
                holder.ignore().add(uuid);
            }
        } else {
            holder.ignore().remove(uuid);
        }
        config.save();
    }

    public boolean isGodModeEnabled() {
        return holder.godMode();
    }

    public void setGodModeEnabled(final boolean set) {
        holder.godMode(set);
        config.save();
    }

    public boolean getMuted() {
        return holder.muted();
    }

    public boolean isMuted() {
        return getMuted();
    }

    public void setMuted(final boolean set) {
        holder.muted(set);
        config.save();
    }

    public String getMuteReason() {
        return holder.muteReason();
    }

    public void setMuteReason(final String reason) {
        holder.muteReason(reason);
        config.save();
    }

    public boolean hasMuteReason() {
        return holder.muteReason() != null;
    }

    public long getMuteTimeout() {
        return holder.timestamps().mute();
    }

    public void setMuteTimeout(final long time) {
        holder.timestamps().mute(time);
        config.save();
    }

    public boolean isJailed() {
        return holder.jailed();
    }

    public void setJailed(final boolean set) {
        holder.jailed(set);
        config.save();
    }

    public boolean toggleJailed() {
        final boolean ret = !isJailed();
        setJailed(ret);
        return ret;
    }

    public long getJailTimeout() {
        return holder.timestamps().jail();
    }

    public void setJailTimeout(final long time) {
        holder.timestamps().jail(time);
        config.save();
    }

    public long getOnlineJailedTime() {
        return holder.timestamps().onlineJail();
    }

    public void setOnlineJailedTime(long onlineJailed) {
        holder.timestamps().onlineJail(onlineJailed);
        config.save();
    }

    public long getLastLogin() {
        return holder.timestamps().login();
    }

    public void setLastLogin(final long time) {
        holder.timestamps().login(time);
        if (base.getAddress() != null && base.getAddress().getAddress() != null) {
            holder.ipAddress(base.getAddress().getAddress().getHostAddress());
        }
        config.save();
    }

    public long getLastLogout() {
        return holder.timestamps().logout();
    }

    public void setLastLogout(final long time) {
        holder.timestamps().logout(time);
        config.save();
    }

    public String getLastLoginAddress() {
        return holder.ipAddress();
    }

    public boolean isAfk() {
        return holder.afk();
    }

    public void _setAfk(final boolean set) {
        holder.afk(set);
        config.save();
    }

    public String getGeoLocation() {
        return holder.geolocation();
    }

    public void setGeoLocation(final String geolocation) {
        holder.geolocation(geolocation);
        config.save();
    }

    public boolean isSocialSpyEnabled() {
        return holder.socialSpy();
    }

    public void setSocialSpyEnabled(final boolean status) {
        holder.socialSpy(status);
        config.save();
    }

    public boolean isNPC() {
        return holder.npc();
    }

    public void setNPC(final boolean set) {
        holder.npc(set);
        config.save();
    }

    public String getLastAccountName() {
        return holder.lastAccountName();
    }

    public void setLastAccountName(final String lastAccountName) {
        if (getLastAccountName() != null && !getLastAccountName().equals(lastAccountName)) {
            final List<String> usernames = holder.pastUsernames();
            usernames.add(0, getLastAccountName());
            holder.pastUsernames(usernames);
        }
        holder.lastAccountName(lastAccountName);
        config.save();
    }

    public boolean arePowerToolsEnabled() {
        return holder.powerToolsEnabled();
    }

    public void setPowerToolsEnabled(final boolean set) {
        holder.powerToolsEnabled(set);
        config.save();
    }

    public boolean togglePowerToolsEnabled() {
        final boolean ret = !arePowerToolsEnabled();
        setPowerToolsEnabled(ret);
        return ret;
    }

    public long getKitTimestamp(String name) {
        name = name.replace('.', '_').replace('/', '_').toLowerCase(Locale.ENGLISH);
        if (holder.timestamps().kits() != null && holder.timestamps().kits().containsKey(name)) {
            return holder.timestamps().kits().get(name);
        }
        return 0L;
    }

    public void setKitTimestamp(String name, final long time) {
        name = name.replace('.', '_').replace('/', '_').toLowerCase(Locale.ENGLISH);
        holder.timestamps().kits().put(name, time);
        config.save();
    }

    public List<CommandCooldown> getCooldownsList() {
        return holder.timestamps().commandCooldowns();
    }

    public Map<Pattern, Long> getCommandCooldowns() {
        final Map<Pattern, Long> map = new HashMap<>();
        for (final CommandCooldown c : getCooldownsList()) {
            if (c == null || c.isIncomplete()) {
                // stupid solution to stupid problem
                continue;
            }
            map.put(c.pattern(), c.value());
        }
        return map;
    }

    public Date getCommandCooldownExpiry(final String label) {
        for (CommandCooldown cooldown : getCooldownsList()) {
            if (cooldown == null || cooldown.isIncomplete()) {
                // stupid solution to stupid problem
                continue;
            }
            if (cooldown.pattern().matcher(label).matches()) {
                return new Date(cooldown.value());
            }
        }
        return null;
    }

    public void addCommandCooldown(final Pattern pattern, final Date expiresAt, final boolean save) {
        final CommandCooldown cooldown = new CommandCooldown();
        cooldown.pattern(pattern);
        cooldown.value(expiresAt.getTime());
        if (cooldown.isIncomplete()) {
            return;
        }
        holder.timestamps().commandCooldowns().add(cooldown);
        if (save) {
            save();
        }
    }

    public boolean clearCommandCooldown(final Pattern pattern) {
        if (holder.timestamps().commandCooldowns().isEmpty()) {
            return false; // false for no modification
        }

        if (getCooldownsList().removeIf(cooldown -> cooldown != null && !cooldown.isIncomplete() && cooldown.pattern().equals(pattern))) {
            save();
            return true;
        }
        return false;
    }

    public boolean isAcceptingPay() {
        return holder.acceptingPay();
    }

    public void setAcceptingPay(final boolean acceptingPay) {
        holder.acceptingPay(acceptingPay);
        save();
    }

    public boolean isPromptingPayConfirm() {
        return holder.confirmPay() != null ? holder.confirmPay() : ess.getSettings().isConfirmCommandEnabledByDefault("pay");
    }

    public void setPromptingPayConfirm(final boolean prompt) {
        holder.confirmPay(prompt);
        save();
    }

    public boolean isPromptingClearConfirm() {
        return holder.confirmClear() != null ? holder.confirmClear() : ess.getSettings().isConfirmCommandEnabledByDefault("clearinventory");
    }

    public void setPromptingClearConfirm(final boolean prompt) {
        holder.confirmClear(prompt);
        save();
    }

    public boolean isLastMessageReplyRecipient() {
        return holder.lastMessageReplyRecipient() != null ? holder.lastMessageReplyRecipient() : ess.getSettings().isLastMessageReplyRecipient();
    }

    public void setLastMessageReplyRecipient(final boolean enabled) {
        holder.lastMessageReplyRecipient(enabled);
        save();
    }

    public boolean isBaltopExcludeCache() {
        return holder.baltopExempt();
    }

    public void setBaltopExemptCache(boolean baltopExempt) {
        holder.baltopExempt(baltopExempt);
        config.save();
    }

    public List<String> getPastUsernames() {
        return holder.pastUsernames();
    }

    public void addPastUsername(String username) {
        final List<String> usernames = holder.pastUsernames();
        usernames.add(0, username);
        holder.pastUsernames(usernames);
        config.save();
    }

    public boolean isShouting() {
        if (holder.shouting() == null) {
            holder.shouting(ess.getSettings().isShoutDefault());
        }
        return holder.shouting();
    }

    public void setShouting(boolean shouting) {
        holder.shouting(shouting);
        config.save();
    }

    public UUID getConfigUUID() {
        return config.getUuid();
    }

    public void save() {
        config.save();
    }

    public void startTransaction() {
        config.startTransaction();
    }

    public void stopTransaction() {
        config.stopTransaction();
    }

    public void setConfigProperty(String node, Object object) {
        setConfigPropertyRaw("info." + node, object);
    }

    public void setConfigPropertyRaw(String node, Object object) {
        config.setRaw(node, object);
        config.save();
    }

    public Set<String> getConfigKeys() {
        return ConfigurateUtil.getKeys(config.getSection("info"));
    }

    public Map<String, Object> getConfigMap() {
        return ConfigurateUtil.getRawMap(config.getSection("info"));
    }

    public Map<String, Object> getConfigMap(final String node) {
        return ConfigurateUtil.getRawMap(config.getSection("info." + node));
    }
}
