package fr.luc.gamingcore.manager;

import fr.luc.gamingcore.Main;
import fr.luc.gamingcore.database.GuildRepository;
import fr.luc.gamingcore.economy.ChunkPriceCalculator;
import fr.luc.gamingcore.economy.EconomyManager;
import fr.luc.gamingcore.model.*;
import org.bukkit.Chunk;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.Player;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Pattern;

/**
 * Service central orchestrant l'intégralité de la logique métier du système de guilde.
 */
public class GuildManager {

    private static final Pattern GUILD_NAME_PATTERN = Pattern.compile("^[a-zA-Z0-9_]{3,16}$");

    private final Main plugin;
    private final GuildRepository repository;
    private final EconomyManager economyManager;
    private final ChunkPriceCalculator priceCalculator;
    private final InviteManager inviteManager;

    // Caches d'accès rapide
    private final Map<UUID, Guild> guildsById = new ConcurrentHashMap<>();
    private final Map<String, UUID> guildsByName = new ConcurrentHashMap<>();
    private final Map<UUID, UUID> playerGuildMap = new ConcurrentHashMap<>();
    private final Map<String, UUID> chunkGuildMap = new ConcurrentHashMap<>();

    /**
     * Constructeur du GuildManager.
     *
     * @param plugin         Instance du plugin principal.
     * @param repository     Référentiel de persistance des données.
     * @param economyManager Gestionnaire de l'économie Vault.
     */
    public GuildManager(Main plugin, GuildRepository repository, EconomyManager economyManager) {
        this.plugin = plugin;
        this.repository = repository;
        this.economyManager = economyManager;
        this.priceCalculator = new ChunkPriceCalculator();
        this.inviteManager = new InviteManager();
    }

    /**
     * Charge l'ensemble des données de la base en mémoire et reconstruit les index de recherche.
     */
    public void loadData() {
        guildsById.clear();
        guildsByName.clear();
        playerGuildMap.clear();
        chunkGuildMap.clear();

        Map<UUID, Guild> loaded = repository.loadAllGuilds();
        for (Guild guild : loaded.values()) {
            indexGuild(guild);
        }

        plugin.getLogger().info("Charges " + guildsById.size() + " guildes (" + playerGuildMap.size() + " membres, " + chunkGuildMap.size() + " claims).");
    }

    /**
     * Arrête proprement le gestionnaire et libère les ressources.
     */
    public void shutdown() {
        guildsById.clear();
        guildsByName.clear();
        playerGuildMap.clear();
        chunkGuildMap.clear();
        inviteManager.cleanExpired();
    }

    // ==========================================
    // MÉTHODES DE CRÉATION & DISSOLUTION
    // ==========================================

    /**
     * Crée une nouvelle guilde pour un joueur avec prélèvement financier et claim obligatoire du premier chunk.
     *
     * @param creator      Le joueur fondateur.
     * @param name         Nom de la guilde (3 à 16 caractères alphanumériques).
     * @param tag          Tag court optionnel (ex: "LEG").
     * @param creationCost Coût financier requis pour la création.
     * @return GuildResult indiquant le succès ou la raison de l'échec.
     */
    public GuildResult createGuild(Player creator, String name, String tag, double creationCost) {
        if (creator == null) {
            return GuildResult.failure("Joueur invalide.");
        }

        // 1. Vérifier si le joueur est déjà dans une guilde
        if (isInGuild(creator.getUniqueId())) {
            return GuildResult.failure("Vous appartenez déjà à une guilde.");
        }

        // 2. Valider le format du nom de la guilde
        if (name == null || !GUILD_NAME_PATTERN.matcher(name).matches()) {
            return GuildResult.failure("Le nom de guilde doit comporter entre 3 et 16 caractères alphanumériques.");
        }

        // 3. Vérifier l'unicité du nom
        if (getGuildByName(name) != null) {
            return GuildResult.failure("Une guilde portant le nom '" + name + "' existe déjà.");
        }

        // 4. Vérifier si le chunk actuel est disponible
        Location loc = creator.getLocation();
        Chunk chunk = loc.getChunk();
        String worldName = loc.getWorld().getName();
        int chunkX = chunk.getX();
        int chunkZ = chunk.getZ();

        if (isChunkClaimed(worldName, chunkX, chunkZ)) {
            return GuildResult.failure("Le chunk où vous vous trouvez est déjà revendiqué par une autre guilde.");
        }

        // 5. Vérifier et prélever l'argent du joueur
        if (creationCost > 0) {
            if (!economyManager.has(creator, creationCost)) {
                return GuildResult.failure("Vous n'avez pas assez d'argent pour fonder une guilde (Requis: " + economyManager.format(creationCost) + ").");
            }
            boolean success = economyManager.withdraw(creator, creationCost);
            if (!success) {
                return GuildResult.failure("Échec de la transaction financière lors de la création de la guilde.");
            }
        }

        // 6. Instancier la nouvelle guilde
        UUID guildId = UUID.randomUUID();
        Guild guild = new Guild(guildId, name, tag, "Guilde fondée par " + creator.getName(), 0.0, System.currentTimeMillis());

        // 7. Configurer les rangs par défaut
        GuildRank leaderRank = new GuildRank("leader", guildId, "Chef", 100, false, EnumSet.allOf(GuildPermission.class));
        GuildRank officerRank = new GuildRank("officer", guildId, "Officier", 50, false, EnumSet.of(
                GuildPermission.CLAIM, GuildPermission.UNCLAIM, GuildPermission.INVITE, GuildPermission.KICK,
                GuildPermission.SET_HOME, GuildPermission.HOME, GuildPermission.BANK_DEPOSIT, GuildPermission.BANK_WITHDRAW,
                GuildPermission.BUILD, GuildPermission.BREAK, GuildPermission.INTERACT, GuildPermission.CONTAINER
        ));
        GuildRank memberRank = new GuildRank("member", guildId, "Membre", 1, true, EnumSet.of(
                GuildPermission.HOME, GuildPermission.BANK_DEPOSIT, GuildPermission.BUILD,
                GuildPermission.BREAK, GuildPermission.INTERACT, GuildPermission.CONTAINER
        ));

        guild.addRank(leaderRank);
        guild.addRank(officerRank);
        guild.addRank(memberRank);

        // 8. Ajouter le fondateur comme chef
        GuildMember founder = new GuildMember(creator.getUniqueId(), guildId, leaderRank.getId(), System.currentTimeMillis(), System.currentTimeMillis(), 0.0);
        guild.addMember(founder);

        // 9. Claimer le 1er chunk et définir le home
        GuildClaim initialClaim = new GuildClaim(0, guildId, worldName, chunkX, chunkZ, System.currentTimeMillis());
        guild.addClaim(initialClaim);
        guild.setHome(loc);

        // 10. Indexer et persister en BDD
        indexGuild(guild);
        repository.saveGuildAsync(guild);
        repository.saveRankAsync(leaderRank);
        repository.saveRankAsync(officerRank);
        repository.saveRankAsync(memberRank);
        repository.saveMemberAsync(founder);
        repository.saveClaimAsync(initialClaim);

        return GuildResult.success("La guilde <white><bold>" + name + "</bold></white> a été créée avec succès !");
    }

    /**
     * Dissout définitivement une guilde existante. Seul le chef de guilde peut initier cette action.
     *
     * @param actor Le joueur demandant la suppression.
     * @param guild La guilde à dissoudre.
     * @return GuildResult.
     */
    public GuildResult disbandGuild(Player actor, Guild guild) {
        if (actor == null || guild == null) {
            return GuildResult.failure("Paramètres invalides.");
        }

        // Vérifier si l'acteur est bien le chef de la guilde
        GuildMember actorMember = guild.getMember(actor.getUniqueId());
        if (actorMember == null) {
            return GuildResult.failure("Vous ne faites pas partie de cette guilde.");
        }

        GuildRank actorRank = guild.getRank(actorMember.getRankId());
        GuildRank leaderRank = guild.getLeaderRank();
        if (actorRank == null || leaderRank == null || !actorRank.equals(leaderRank)) {
            return GuildResult.failure("Seul le Chef de la guilde peut dissoudre la guilde.");
        }

        // Désindexer la guilde
        unindexGuild(guild);

        // Supprimer de la base de données
        repository.deleteGuildAsync(guild.getId());

        return GuildResult.success("La guilde <white>" + guild.getName() + "</white> a été dissoute.");
    }

    // ==========================================
    // GESTION DES MEMBRES & RANGS
    // ==========================================

    /**
     * Intègre un joueur dans une guilde en lui attribuant le rang par défaut.
     *
     * @param guild  La guilde à rejoindre.
     * @param player Le nouveau membre.
     * @return GuildResult.
     */
    public GuildResult addMember(Guild guild, Player player) {
        if (guild == null || player == null) {
            return GuildResult.failure("Paramètres invalides.");
        }

        if (isInGuild(player.getUniqueId())) {
            return GuildResult.failure("Le joueur appartient déjà à une guilde.");
        }

        GuildRank defaultRank = guild.getDefaultRank();
        if (defaultRank == null) {
            return GuildResult.failure("La guilde ne possède aucun rang par défaut.");
        }

        GuildMember member = new GuildMember(player.getUniqueId(), guild.getId(), defaultRank.getId(), System.currentTimeMillis(), System.currentTimeMillis(), 0.0);
        guild.addMember(member);
        playerGuildMap.put(player.getUniqueId(), guild.getId());
        repository.saveMemberAsync(member);

        return GuildResult.success("Le joueur <white>" + player.getName() + "</white> a rejoint la guilde !");
    }

    /**
     * Expulse un membre d'une guilde avec vérification de la hiérarchie des rangs.
     *
     * @param actor      Le joueur initiateur de l'expulsion.
     * @param guild      La guilde concernée.
     * @param targetUuid L'UUID du membre à expulser.
     * @return GuildResult.
     */
    public GuildResult kickMember(Player actor, Guild guild, UUID targetUuid) {
        if (actor == null || guild == null || targetUuid == null) {
            return GuildResult.failure("Paramètres invalides.");
        }

        if (actor.getUniqueId().equals(targetUuid)) {
            return GuildResult.failure("Vous ne pouvez pas vous expulser vous-même. Utilisez /guild leave.");
        }

        GuildRank actorRank = guild.getMemberRank(actor.getUniqueId());
        if (actorRank == null || !actorRank.hasPermission(GuildPermission.KICK)) {
            return GuildResult.failure("Vous n'avez pas la permission d'expulser des membres.");
        }

        GuildMember targetMember = guild.getMember(targetUuid);
        if (targetMember == null) {
            return GuildResult.failure("Ce joueur n'est pas membre de la guilde.");
        }

        GuildRank targetRank = guild.getRank(targetMember.getRankId());
        if (targetRank != null && targetRank.getPriority() >= actorRank.getPriority()) {
            return GuildResult.failure("Vous ne pouvez pas expulser un membre ayant un rang supérieur ou égal au vôtre.");
        }

        guild.removeMember(targetUuid);
        playerGuildMap.remove(targetUuid);
        repository.deleteMemberAsync(targetUuid);

        return GuildResult.success("Le membre a été expulsé de la guilde.");
    }

    /**
     * Permet à un membre de quitter volontairement sa guilde.
     *
     * @param player Le joueur souhaitant partir.
     * @param guild  La guilde quittée.
     * @return GuildResult.
     */
    public GuildResult leaveGuild(Player player, Guild guild) {
        if (player == null || guild == null) {
            return GuildResult.failure("Paramètres invalides.");
        }

        GuildMember member = guild.getMember(player.getUniqueId());
        if (member == null) {
            return GuildResult.failure("Vous n'êtes pas membre de cette guilde.");
        }

        GuildRank leaderRank = guild.getLeaderRank();
        if (leaderRank != null && member.getRankId().equalsIgnoreCase(leaderRank.getId())) {
            return GuildResult.failure("En tant que Chef, vous ne pouvez pas quitter la guilde. Transférez le lead ou dissolvez la guilde.");
        }

        guild.removeMember(player.getUniqueId());
        playerGuildMap.remove(player.getUniqueId());
        repository.deleteMemberAsync(player.getUniqueId());

        return GuildResult.success("Vous avez quitté la guilde.");
    }

    /**
     * Modifie le rang d'un membre avec contrôle de priorité hiérarchique.
     *
     * @param actor      Le joueur initiateur.
     * @param guild      La guilde concernée.
     * @param targetUuid Le membre cible.
     * @param newRankId  L'identifiant du nouveau rang.
     * @return GuildResult.
     */
    public GuildResult setMemberRank(Player actor, Guild guild, UUID targetUuid, String newRankId) {
        if (actor == null || guild == null || targetUuid == null || newRankId == null) {
            return GuildResult.failure("Paramètres invalides.");
        }

        GuildRank actorRank = guild.getMemberRank(actor.getUniqueId());
        if (actorRank == null) {
            return GuildResult.failure("Vous ne faites pas partie de cette guilde.");
        }

        GuildMember targetMember = guild.getMember(targetUuid);
        if (targetMember == null) {
            return GuildResult.failure("Le joueur cible n'est pas dans la guilde.");
        }

        GuildRank newRank = guild.getRank(newRankId);
        if (newRank == null) {
            return GuildResult.failure("Le rang spécifié n'existe pas.");
        }

        GuildRank targetCurrentRank = guild.getRank(targetMember.getRankId());
        boolean isPromote = targetCurrentRank == null || newRank.getPriority() > targetCurrentRank.getPriority();

        // Vérification des permissions
        if (isPromote && !actorRank.hasPermission(GuildPermission.PROMOTE)) {
            return GuildResult.failure("Vous n'avez pas la permission de promouvoir des membres.");
        }
        if (!isPromote && !actorRank.hasPermission(GuildPermission.DEMOTE)) {
            return GuildResult.failure("Vous n'avez pas la permission de rétrograder des membres.");
        }

        // Contrôle hiérarchique : l'acteur ne peut pas affecter un rang supérieur ou égal au sien
        GuildRank leaderRank = guild.getLeaderRank();
        boolean isLeader = actorRank.equals(leaderRank);
        if (!isLeader && newRank.getPriority() >= actorRank.getPriority()) {
            return GuildResult.failure("Vous ne pouvez pas attribuer un rang supérieur ou égal au vôtre.");
        }

        targetMember.setRankId(newRank.getId());
        repository.saveMemberAsync(targetMember);

        return GuildResult.success("Le rang du membre a été mis à jour vers <white>" + newRank.getName() + "</white>.");
    }

    // ==========================================
    // GESTION DES CLAIMS (TERRITOIRES)
    // ==========================================

    /**
     * Revendique un chunk territorial pour une guilde en débitant son coût depuis la banque de guilde.
     *
     * @param player Le joueur initiant le claim.
     * @param guild  La guilde pour laquelle claimer.
     * @param world  Le monde Minecraft.
     * @param chunkX La coordonnée X du chunk.
     * @param chunkZ La coordonnée Z du chunk.
     * @return GuildResult.
     */
    public GuildResult claimChunk(Player player, Guild guild, World world, int chunkX, int chunkZ) {
        if (player == null || guild == null || world == null) {
            return GuildResult.failure("Paramètres invalides.");
        }

        String worldName = world.getName();

        // 1. Vérifier la permission
        if (!guild.hasPermission(player.getUniqueId(), GuildPermission.CLAIM)) {
            return GuildResult.failure("Votre rang ne vous autorise pas à revendiquer de nouveaux territoires.");
        }

        // 2. Vérifier si le chunk est déjà claimé
        if (isChunkClaimed(worldName, chunkX, chunkZ)) {
            return GuildResult.failure("Ce chunk est déjà revendiqué.");
        }

        // 3. Calculer le prix du chunk avec la formule spatio-temporelle
        double price = priceCalculator.calculateNextChunkPrice(guild);

        // 4. Vérifier si la banque de guilde possède les fonds
        if (!guild.hasBalance(price)) {
            return GuildResult.failure("La banque de guilde n'a pas assez d'argent (Requis: " + economyManager.format(price) + ", Solde: " + economyManager.format(guild.getBalance()) + ").");
        }

        // 5. Débiter la banque et enregistrer le claim
        guild.withdraw(price);
        GuildClaim claim = new GuildClaim(0, guild.getId(), worldName, chunkX, chunkZ, System.currentTimeMillis());
        guild.addClaim(claim);
        chunkGuildMap.put(getChunkKey(worldName, chunkX, chunkZ), guild.getId());

        // 6. Sauvegarder en BDD
        repository.saveGuildAsync(guild);
        repository.saveClaimAsync(claim);

        return GuildResult.success("Chunk revendiqué avec succès pour <yellow>" + economyManager.format(price) + "</yellow> ! (Total: " + guild.getClaimCount() + " chunks)");
    }

    /**
     * Abandonne un chunk possédé par la guilde.
     *
     * @param player Le joueur initiateur.
     * @param guild  La guilde propriétaire.
     * @param world  Le monde Minecraft.
     * @param chunkX La coordonnée X du chunk.
     * @param chunkZ La coordonnée Z du chunk.
     * @return GuildResult.
     */
    public GuildResult unclaimChunk(Player player, Guild guild, World world, int chunkX, int chunkZ) {
        if (player == null || guild == null || world == null) {
            return GuildResult.failure("Paramètres invalides.");
        }

        String worldName = world.getName();

        // 1. Vérifier la permission
        if (!guild.hasPermission(player.getUniqueId(), GuildPermission.UNCLAIM)) {
            return GuildResult.failure("Votre rang ne vous autorise pas à abandonner des territoires.");
        }

        // 2. Vérifier si le chunk appartient à la guilde
        if (!guild.isClaimed(worldName, chunkX, chunkZ)) {
            return GuildResult.failure("Ce chunk n'appartient pas à votre guilde.");
        }

        // 3. Empêcher d'avoir 0 chunk si c'est le dernier
        if (guild.getClaimCount() <= 1) {
            return GuildResult.failure("Vous ne pouvez pas abandonner le dernier chunk de votre guilde.");
        }

        // 4. Supprimer le claim
        guild.removeClaim(worldName, chunkX, chunkZ);
        chunkGuildMap.remove(getChunkKey(worldName, chunkX, chunkZ));
        repository.deleteClaimAsync(worldName, chunkX, chunkZ);

        return GuildResult.success("Le chunk a été libéré.");
    }

    // ==========================================
    // GESTION DE LA BANQUE
    // ==========================================

    /**
     * Dépose de l'argent depuis le compte personnel d'un joueur vers la banque de guilde.
     *
     * @param player Le joueur déposant.
     * @param guild  La guilde bénéficiaire.
     * @param amount Le montant à déposer.
     * @return GuildResult.
     */
    public GuildResult depositMoney(Player player, Guild guild, double amount) {
        if (player == null || guild == null || amount <= 0) {
            return GuildResult.failure("Montant invalide.");
        }

        if (!guild.hasPermission(player.getUniqueId(), GuildPermission.BANK_DEPOSIT)) {
            return GuildResult.failure("Votre rang ne vous autorise pas à déposer dans la banque.");
        }

        if (!economyManager.has(player, amount)) {
            return GuildResult.failure("Vous n'avez pas assez d'argent sur votre compte personnel.");
        }

        boolean withdrawn = economyManager.withdraw(player, amount);
        if (!withdrawn) {
            return GuildResult.failure("Échec de la transaction bancaire.");
        }

        guild.deposit(amount);
        GuildMember member = guild.getMember(player.getUniqueId());
        if (member != null) {
            member.addContribution(amount);
            repository.saveMemberAsync(member);
        }
        repository.saveGuildAsync(guild);

        return GuildResult.success("Vous avez déposé <green>" + economyManager.format(amount) + "</green> dans la banque de guilde ! (Nouveau solde: " + economyManager.format(guild.getBalance()) + ")");
    }

    /**
     * Retire de l'argent de la banque de guilde vers le compte d'un joueur.
     *
     * @param player Le joueur bénéficiaire.
     * @param guild  La guilde débitée.
     * @param amount Le montant à retirer.
     * @return GuildResult.
     */
    public GuildResult withdrawMoney(Player player, Guild guild, double amount) {
        if (player == null || guild == null || amount <= 0) {
            return GuildResult.failure("Montant invalide.");
        }

        if (!guild.hasPermission(player.getUniqueId(), GuildPermission.BANK_WITHDRAW)) {
            return GuildResult.failure("Votre rang ne vous autorise pas à retirer de l'argent de la banque.");
        }

        if (!guild.hasBalance(amount)) {
            return GuildResult.failure("Le solde de la banque de guilde est insuffisant (Solde actuel: " + economyManager.format(guild.getBalance()) + ").");
        }

        boolean withdrawnFromGuild = guild.withdraw(amount);
        if (!withdrawnFromGuild) {
            return GuildResult.failure("Impossible de débiter la banque de guilde.");
        }

        economyManager.deposit(player, amount);
        repository.saveGuildAsync(guild);

        return GuildResult.success("Vous avez retiré <green>" + economyManager.format(amount) + "</green> de la banque de guilde.");
    }

    // ==========================================
    // GETTERS & LOOKUPS RAPIDES
    // ==========================================

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

    public Guild getGuildByName(String name) {
        if (name == null) return null;
        UUID id = guildsByName.get(name.toLowerCase());
        return id != null ? guildsById.get(id) : null;
    }

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

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

    public Guild getGuildAt(String world, int chunkX, int chunkZ) {
        UUID guildId = chunkGuildMap.get(getChunkKey(world, chunkX, chunkZ));
        return guildId != null ? guildsById.get(guildId) : null;
    }

    public boolean isChunkClaimed(String world, int chunkX, int chunkZ) {
        return chunkGuildMap.containsKey(getChunkKey(world, chunkX, chunkZ));
    }

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

    public ChunkPriceCalculator getPriceCalculator() {
        return priceCalculator;
    }

    public EconomyManager getEconomyManager() {
        return economyManager;
    }

    public InviteManager getInviteManager() {
        return inviteManager;
    }

    // ==========================================
    // MÉTHODES INTERNES D'INDEXATION
    // ==========================================

    private void indexGuild(Guild guild) {
        guildsById.put(guild.getId(), guild);
        guildsByName.put(guild.getName().toLowerCase(), guild.getId());
        for (GuildMember member : guild.getMembers()) {
            playerGuildMap.put(member.getPlayerUuid(), guild.getId());
        }
        for (GuildClaim claim : guild.getClaims()) {
            chunkGuildMap.put(getChunkKey(claim.getWorldName(), claim.getChunkX(), claim.getChunkZ()), guild.getId());
        }
    }

    private void unindexGuild(Guild guild) {
        guildsById.remove(guild.getId());
        guildsByName.remove(guild.getName().toLowerCase());
        for (GuildMember member : guild.getMembers()) {
            playerGuildMap.remove(member.getPlayerUuid());
        }
        for (GuildClaim claim : guild.getClaims()) {
            chunkGuildMap.remove(getChunkKey(claim.getWorldName(), claim.getChunkX(), claim.getChunkZ()));
        }
    }

    private String getChunkKey(String world, int x, int z) {
        return (world != null ? world.toLowerCase() : "world") + ":" + x + "," + z;
    }
}
