package emu.grasscutter.game;

import dev.morphia.annotations.*;
import emu.grasscutter.database.DatabaseHelper;
import emu.grasscutter.utils.Crypto;
import emu.grasscutter.utils.Utils;

import static emu.grasscutter.config.Configuration.*;

import java.util.*;
import java.util.stream.Stream;

import org.bson.Document;

@Entity(value = "accounts", useDiscriminator = false)
public class Account {
    @Id private String id;

    @Indexed(options = @IndexOptions(unique = true))
    @Collation(locale = "simple", caseLevel = true)
    private String username;
    private String password; // Unused for now

    private int reservedPlayerId;
    private String email;

    private String token;
    private String sessionKey; // Session token for dispatch server
    private List<String> permissions;
    private Locale locale;

    private String banReason;
    private int banEndTime;
    private int banStartTime;
    private boolean isBanned;

    @Deprecated
    public Account() {
        this.permissions = new ArrayList<>();
        this.locale = LANGUAGE;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getToken() {
        return token;
    }

    public void setToken(String token) {
        this.token = token;
    }

    public int getReservedPlayerUid() {
        return this.reservedPlayerId;
    }

    public void setReservedPlayerUid(int playerId) {
        this.reservedPlayerId = playerId;
    }

    public String getEmail() {
        if (email != null && !email.isEmpty()) {
            return email;
        } else {
            return "";
        }
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getSessionKey() {
        return this.sessionKey;
    }

    public String generateSessionKey() {
        this.sessionKey = Utils.bytesToHex(Crypto.createSessionKey(32));
        this.save();
        return this.sessionKey;
    }

    public Locale getLocale() {
        return locale;
    }

    public void setLocale(Locale locale) {
        this.locale = locale;
    }

    public String getBanReason() {
        return banReason;
    }

    public void setBanReason(String banReason) {
        this.banReason = banReason;
    }

    public int getBanEndTime() {
        return banEndTime;
    }

    public void setBanEndTime(int banEndTime) {
        this.banEndTime = banEndTime;
    }

    public int getBanStartTime() {
        return banStartTime;
    }

    public void setBanStartTime(int banStartTime) {
        this.banStartTime = banStartTime;
    }

    public boolean isBanned() {
        if (banEndTime > 0 && banEndTime < System.currentTimeMillis() / 1000) {
            this.isBanned = false;
            this.banEndTime = 0;
            this.banStartTime = 0;
            this.banReason = null;
            save();
        }

        return isBanned;
    }

    public void setBanned(boolean isBanned) {
        this.isBanned = isBanned;
    }

    /**
     * The collection of a player's permissions.
     */
    public List<String> getPermissions() {
        return this.permissions;
    }

    public boolean addPermission(String permission) {
        if (this.permissions.contains(permission)) return false;
        this.permissions.add(permission); return true;
    }

    public static boolean permissionMatchesWildcard(String wildcard, String[] permissionParts) {
        String[] wildcardParts = wildcard.split("\\.");
        if (permissionParts.length < wildcardParts.length) {  // A longer wildcard can never match a shorter permission
            return false;
        }
        for (int i=0; i<wildcardParts.length; i++) {
            switch (wildcardParts[i]) {
                case "**":  // Recursing match
                    return true;
                case "*":  // Match only one layer
                    if (i >= (permissionParts.length-1)) {
                        return true;
                    }
                    break;
                default:  // This layer isn't a wildcard, it needs to match exactly
                    if (!wildcardParts[i].equals(permissionParts[i])) {
                        return false;
                    }
            }
        }
        // At this point the wildcard will have matched every layer, but if it is shorter then the permission then this is not a match at this point (no **).
        return (wildcardParts.length == permissionParts.length);
    }

    public boolean hasPermission(String permission) {
        if (permission.isEmpty()) return true;
        if (this.permissions.contains("*") && this.permissions.size() == 1) return true;

        // Add default permissions if it doesn't exist
        List<String> permissions = Stream.of(this.permissions, Arrays.asList(ACCOUNT.defaultPermissions))
                .flatMap(Collection::stream)
                .distinct().toList();

        if (permissions.contains(permission)) return true;

        String[] permissionParts = permission.split("\\.");
        for (String p : permissions) {
            if (p.startsWith("-") && permissionMatchesWildcard(p.substring(1), permissionParts)) return false;
            if (permissionMatchesWildcard(p, permissionParts)) return true;
        }

        return permissions.contains("*");
    }

    public boolean removePermission(String permission) {
        return this.permissions.remove(permission);
    }

    // TODO make unique
    public String generateLoginToken() {
        this.token = Utils.bytesToHex(Crypto.createSessionKey(32));
        this.save();
        return this.token;
    }

    public void save() {
        DatabaseHelper.saveAccount(this);
    }

    @PreLoad
    public void onLoad(Document document) {
        // Grant the superuser permissions to accounts created before the permissions update
        if (!document.containsKey("permissions")) {
            this.addPermission("*");
        }

        // Set account default language as server default language
        if (!document.containsKey("locale")) {
            this.locale = LANGUAGE;
        }
    }
}
