package com.bitnix.dirtguilds.guild;

import com.bitnix.dirtguilds.DirtGuildsPlugin;
import com.bitnix.dirtguilds.data.DatabaseManager;
import org.bukkit.Bukkit;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.util.io.BukkitObjectInputStream;
import org.bukkit.util.io.BukkitObjectOutputStream;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.Base64;

public class GuildManager {

    private final DirtGuildsPlugin plugin;
    private final DatabaseManager databaseManager;

    private final Map<UUID, Guild> guildsById;
    private final Map<String, UUID> guildIdsByLowerName;
    private final Map<UUID, UUID> playerGuildMap;
    private final Map<UUID, List<GuildInvite>> invitesByTarget;
    private final Set<UUID> guildChatEnabled;

    public GuildManager(DirtGuildsPlugin plugin, DatabaseManager databaseManager) {
        this.plugin = plugin;
        this.databaseManager = databaseManager;
        this.guildsById = new ConcurrentHashMap<>();
        this.guildIdsByLowerName = new ConcurrentHashMap<>();
        this.playerGuildMap = new ConcurrentHashMap<>();
        this.invitesByTarget = new ConcurrentHashMap<>();
        this.guildChatEnabled = ConcurrentHashMap.newKeySet();
    }

    public void loadAll() {
        guildsById.clear();
        guildIdsByLowerName.clear();
        playerGuildMap.clear();
        invitesByTarget.clear();
        guildChatEnabled.clear();

        try (Connection connection = databaseManager.getConnection()) {
            Map<UUID, Map<UUID, String>> memberMap = new HashMap<>();
            Map<UUID, ItemStack[]> vaultMap = new HashMap<>();

            try (PreparedStatement statement = connection.prepareStatement("SELECT guild_id, player_uuid, rank_name FROM guild_members");
                 ResultSet resultSet = statement.executeQuery()) {
                while (resultSet.next()) {
                    UUID guildId = UUID.fromString(resultSet.getString("guild_id"));
                    UUID playerUuid = UUID.fromString(resultSet.getString("player_uuid"));
                    String rankName = resultSet.getString("rank_name");

                    memberMap.computeIfAbsent(guildId, ignored -> new HashMap<>()).put(playerUuid, rankName);
                    playerGuildMap.put(playerUuid, guildId);
                }
            }

            try (PreparedStatement statement = connection.prepareStatement("SELECT guild_id, slot_index, item_data FROM guild_vault_items");
                 ResultSet resultSet = statement.executeQuery()) {
                while (resultSet.next()) {
                    UUID guildId = UUID.fromString(resultSet.getString("guild_id"));
                    int slot = resultSet.getInt("slot_index");
                    String itemData = resultSet.getString("item_data");

                    ItemStack[] contents = vaultMap.computeIfAbsent(guildId, ignored -> new ItemStack[54]);
                    contents[slot] = deserializeItem(itemData);
                }
            }

            try (PreparedStatement statement = connection.prepareStatement("SELECT * FROM guilds");
                 ResultSet resultSet = statement.executeQuery()) {
                while (resultSet.next()) {
                    UUID id = UUID.fromString(resultSet.getString("id"));
                    String name = resultSet.getString("name");
                    String prefix = resultSet.getString("prefix");
                    UUID owner = UUID.fromString(resultSet.getString("owner"));
                    int level = resultSet.getInt("level");
                    double bankBalance = resultSet.getDouble("bank_balance");
                    boolean friendlyFire = resultSet.getBoolean("friendly_fire");

                    ItemStack[] vault = vaultMap.getOrDefault(id, new ItemStack[getVaultSizeForLevel(level)]);
                    Guild guild = new Guild(
                            id,
                            name,
                            prefix,
                            owner,
                            level,
                            bankBalance,
                            friendlyFire,
                            memberMap.getOrDefault(id, new HashMap<>()),
                            normalizeVault(vault, getVaultSizeForLevel(level))
                    );

                    guildsById.put(id, guild);
                    guildIdsByLowerName.put(name.toLowerCase(Locale.ROOT), id);

                    for (UUID member : guild.getMemberRanks().keySet()) {
                        playerGuildMap.put(member, id);
                    }
                }
            }
        } catch (Exception exception) {
            throw new RuntimeException("Failed to load guild data", exception);
        }
    }

    public Guild getGuildByPlayer(UUID playerUuid) {
        UUID guildId = playerGuildMap.get(playerUuid);
        return guildId == null ? null : guildsById.get(guildId);
    }

    public Guild getGuildByName(String name) {
        if (name == null) {
            return null;
        }

        UUID guildId = guildIdsByLowerName.get(name.toLowerCase(Locale.ROOT));
        return guildId == null ? null : guildsById.get(guildId);
    }

    public Guild getGuildById(UUID guildId) {
        return guildsById.get(guildId);
    }

    public boolean isInGuild(UUID playerUuid) {
        return playerGuildMap.containsKey(playerUuid);
    }

    public boolean guildNameExists(String name) {
        return guildIdsByLowerName.containsKey(name.toLowerCase(Locale.ROOT));
    }

    public Guild createGuild(UUID owner, String name) {
        UUID guildId = UUID.randomUUID();
        int defaultLevel = plugin.getConfig().getInt("settings.levels.default-level", 1);
        int vaultSize = getVaultSizeForLevel(defaultLevel);

        Guild guild = new Guild(
                guildId,
                name,
                name,
                owner,
                defaultLevel,
                0.0D,
                plugin.getConfig().getBoolean("settings.default-friendly-fire", false),
                new HashMap<>(),
                new ItemStack[vaultSize]
        );

        guildsById.put(guildId, guild);
        guildIdsByLowerName.put(name.toLowerCase(Locale.ROOT), guildId);
        playerGuildMap.put(owner, guildId);

        saveGuild(guild);
        saveMembers(guild);
        saveVault(guild);
        return guild;
    }

    public void deleteGuild(Guild guild) {
        guildsById.remove(guild.getId());
        guildIdsByLowerName.remove(guild.getName().toLowerCase(Locale.ROOT));

        for (UUID member : new HashSet<>(guild.getMemberRanks().keySet())) {
            playerGuildMap.remove(member);
            guildChatEnabled.remove(member);
        }

        invitesByTarget.values().forEach(list -> list.removeIf(invite -> invite.getGuildId().equals(guild.getId())));

        try (Connection connection = databaseManager.getConnection()) {
            try (PreparedStatement statement = connection.prepareStatement("DELETE FROM guild_members WHERE guild_id = ?")) {
                statement.setString(1, guild.getId().toString());
                statement.executeUpdate();
            }

            try (PreparedStatement statement = connection.prepareStatement("DELETE FROM guild_vault_items WHERE guild_id = ?")) {
                statement.setString(1, guild.getId().toString());
                statement.executeUpdate();
            }

            try (PreparedStatement statement = connection.prepareStatement("DELETE FROM guilds WHERE id = ?")) {
                statement.setString(1, guild.getId().toString());
                statement.executeUpdate();
            }
        } catch (SQLException exception) {
            throw new RuntimeException("Failed to delete guild", exception);
        }
    }

    public void renameGuild(Guild guild, String newName) {
        guildIdsByLowerName.remove(guild.getName().toLowerCase(Locale.ROOT));
        guild.setName(newName);
        guildIdsByLowerName.put(newName.toLowerCase(Locale.ROOT), guild.getId());
        saveGuild(guild);
    }

    public void setPrefix(Guild guild, String prefix) {
        guild.setPrefix(prefix);
        saveGuild(guild);
    }

    public void setFriendlyFire(Guild guild, boolean enabled) {
        guild.setFriendlyFire(enabled);
        saveGuild(guild);
    }

    public void addMember(Guild guild, UUID playerUuid) {
        guild.addMember(playerUuid);
        playerGuildMap.put(playerUuid, guild.getId());
        saveMembers(guild);
    }

    public void removeMember(Guild guild, UUID playerUuid) {
        guild.removeMember(playerUuid);
        playerGuildMap.remove(playerUuid);
        guildChatEnabled.remove(playerUuid);
        saveMembers(guild);
    }

    public void setMemberRank(Guild guild, UUID playerUuid, String rank) {
        guild.setRank(playerUuid, rank);
        saveMembers(guild);
    }

    public void transferOwnership(Guild guild, UUID newOwner) {
        guild.setOwner(newOwner);
        guild.setRank(newOwner, "OWNER");
        playerGuildMap.put(newOwner, guild.getId());
        saveGuild(guild);
        saveMembers(guild);
    }

    public void deposit(Guild guild, double amount) {
        guild.setBankBalance(guild.getBankBalance() + amount);
        saveGuild(guild);
    }

    public boolean withdraw(Guild guild, double amount) {
        if (guild.getBankBalance() < amount) {
            return false;
        }

        guild.setBankBalance(guild.getBankBalance() - amount);
        saveGuild(guild);
        return true;
    }

    public boolean upgradeLevel(Guild guild) {
        int nextLevel = guild.getLevel() + 1;
        ConfigurationSection levelSection = plugin.getConfig().getConfigurationSection("settings.levels.levels." + nextLevel);
        if (levelSection == null) {
            return false;
        }

        double cost = levelSection.getDouble("cost", 0.0D);
        if (guild.getBankBalance() < cost) {
            return false;
        }

        guild.setBankBalance(guild.getBankBalance() - cost);
        guild.setLevel(nextLevel);
        guild.setVaultContents(normalizeVault(guild.getVaultContents(), getVaultSizeForLevel(nextLevel)));
        saveGuild(guild);
        saveVault(guild);
        return true;
    }

    public int getMaxConfiguredLevel() {
        ConfigurationSection section = plugin.getConfig().getConfigurationSection("settings.levels.levels");
        if (section == null) {
            return 1;
        }

        int max = 1;
        for (String key : section.getKeys(false)) {
            try {
                max = Math.max(max, Integer.parseInt(key));
            } catch (NumberFormatException ignored) {
            }
        }
        return max;
    }

    public int getVaultSizeForLevel(int level) {
        int configured = plugin.getConfig().getInt("settings.levels.levels." + level + ".vault-slots",
                plugin.getConfig().getInt("settings.vault.default-size", 9));

        configured = Math.max(9, configured);
        configured = Math.min(plugin.getConfig().getInt("settings.vault.max-size", 54), configured);

        int remainder = configured % 9;
        if (remainder != 0) {
            configured += (9 - remainder);
        }

        return Math.max(9, Math.min(54, configured));
    }

    public double getNextLevelCost(Guild guild) {
        return plugin.getConfig().getDouble("settings.levels.levels." + (guild.getLevel() + 1) + ".cost", -1.0D);
    }

    public void createInvite(UUID target, UUID inviter, Guild guild) {
        long expiresAt = System.currentTimeMillis() + (plugin.getConfig().getLong("settings.invite-expire-seconds", 120L) * 1000L);
        GuildInvite invite = new GuildInvite(guild.getId(), target, inviter, expiresAt);
        invitesByTarget.computeIfAbsent(target, ignored -> new ArrayList<>()).removeIf(existing -> existing.getGuildId().equals(guild.getId()));
        invitesByTarget.computeIfAbsent(target, ignored -> new ArrayList<>()).add(invite);
    }

    public GuildInvite getInvite(UUID target, UUID guildId) {
        List<GuildInvite> invites = invitesByTarget.get(target);
        if (invites == null) {
            return null;
        }

        invites.removeIf(GuildInvite::isExpired);

        for (GuildInvite invite : invites) {
            if (invite.getGuildId().equals(guildId)) {
                return invite;
            }
        }

        return null;
    }

    public void removeInvite(UUID target, UUID guildId) {
        List<GuildInvite> invites = invitesByTarget.get(target);
        if (invites == null) {
            return;
        }

        invites.removeIf(invite -> invite.getGuildId().equals(guildId));
        if (invites.isEmpty()) {
            invitesByTarget.remove(target);
        }
    }

    public boolean isGuildChatEnabled(UUID playerUuid) {
        return guildChatEnabled.contains(playerUuid);
    }

    public void setGuildChatEnabled(UUID playerUuid, boolean enabled) {
        if (enabled) {
            guildChatEnabled.add(playerUuid);
        } else {
            guildChatEnabled.remove(playerUuid);
        }
    }

    public void saveVaultFromInventory(Guild guild, Inventory inventory) {
        ItemStack[] contents = new ItemStack[inventory.getSize()];
        for (int i = 0; i < inventory.getSize(); i++) {
            contents[i] = inventory.getItem(i);
        }
        guild.setVaultContents(contents);
        saveVault(guild);
    }

    public Collection<Guild> getAllGuilds() {
        return Collections.unmodifiableCollection(guildsById.values());
    }

    private void saveGuild(Guild guild) {
        try (Connection connection = databaseManager.getConnection();
             PreparedStatement statement = connection.prepareStatement("""
                     INSERT INTO guilds (id, name, prefix, owner, level, bank_balance, friendly_fire)
                     VALUES (?, ?, ?, ?, ?, ?, ?)
                     ON CONFLICT(id) DO UPDATE SET
                     name = excluded.name,
                     prefix = excluded.prefix,
                     owner = excluded.owner,
                     level = excluded.level,
                     bank_balance = excluded.bank_balance,
                     friendly_fire = excluded.friendly_fire
                     """)) {

            statement.setString(1, guild.getId().toString());
            statement.setString(2, guild.getName());
            statement.setString(3, guild.getPrefix());
            statement.setString(4, guild.getOwner().toString());
            statement.setInt(5, guild.getLevel());
            statement.setDouble(6, guild.getBankBalance());
            statement.setBoolean(7, guild.isFriendlyFire());
            statement.executeUpdate();
        } catch (SQLException exception) {
            throw new RuntimeException("Failed to save guild", exception);
        }
    }

    private void saveMembers(Guild guild) {
        try (Connection connection = databaseManager.getConnection()) {
            try (PreparedStatement delete = connection.prepareStatement("DELETE FROM guild_members WHERE guild_id = ?")) {
                delete.setString(1, guild.getId().toString());
                delete.executeUpdate();
            }

            try (PreparedStatement insert = connection.prepareStatement("INSERT INTO guild_members (guild_id, player_uuid, rank_name) VALUES (?, ?, ?)")) {
                for (Map.Entry<UUID, String> entry : guild.getMemberRanks().entrySet()) {
                    insert.setString(1, guild.getId().toString());
                    insert.setString(2, entry.getKey().toString());
                    insert.setString(3, entry.getValue());
                    insert.addBatch();
                }
                insert.executeBatch();
            }
        } catch (SQLException exception) {
            throw new RuntimeException("Failed to save guild members", exception);
        }
    }

    private void saveVault(Guild guild) {
        try (Connection connection = databaseManager.getConnection()) {
            try (PreparedStatement delete = connection.prepareStatement("DELETE FROM guild_vault_items WHERE guild_id = ?")) {
                delete.setString(1, guild.getId().toString());
                delete.executeUpdate();
            }

            try (PreparedStatement insert = connection.prepareStatement("INSERT INTO guild_vault_items (guild_id, slot_index, item_data) VALUES (?, ?, ?)")) {
                ItemStack[] contents = guild.getVaultContents();
                for (int i = 0; i < contents.length; i++) {
                    ItemStack item = contents[i];
                    if (item == null) {
                        continue;
                    }

                    insert.setString(1, guild.getId().toString());
                    insert.setInt(2, i);
                    insert.setString(3, serializeItem(item));
                    insert.addBatch();
                }
                insert.executeBatch();
            }
        } catch (Exception exception) {
            throw new RuntimeException("Failed to save guild vault", exception);
        }
    }

    private String serializeItem(ItemStack item) throws Exception {
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        BukkitObjectOutputStream dataOutput = new BukkitObjectOutputStream(outputStream);
        dataOutput.writeObject(item);
        dataOutput.close();
        return Base64.getEncoder().encodeToString(outputStream.toByteArray());
    }

    private ItemStack deserializeItem(String data) throws Exception {
        byte[] bytes = Base64.getDecoder().decode(data);
        BukkitObjectInputStream dataInput = new BukkitObjectInputStream(new ByteArrayInputStream(bytes));
        ItemStack item = (ItemStack) dataInput.readObject();
        dataInput.close();
        return item;
    }

    private ItemStack[] normalizeVault(ItemStack[] original, int newSize) {
        ItemStack[] normalized = new ItemStack[newSize];
        if (original != null) {
            System.arraycopy(original, 0, normalized, 0, Math.min(original.length, newSize));
        }
        return normalized;
    }

    public void shutdown() {
        invitesByTarget.clear();
        guildChatEnabled.clear();
        guildsById.clear();
        guildIdsByLowerName.clear();
        playerGuildMap.clear();
    }
}
