package com.tiedup.remake.entities.ai.kidnapper;

import com.tiedup.remake.core.SettingsAccessor;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.dialogue.EntityDialogueManager;
import com.tiedup.remake.dialogue.EntityDialogueManager.DialogueCategory;
import com.tiedup.remake.entities.EntityKidnapper;
import com.tiedup.remake.state.IRestrainable;
import com.tiedup.remake.util.MessageDispatcher;
import com.tiedup.remake.util.tasks.ItemTask;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import net.minecraft.ChatFormatting;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.ai.goal.Goal;
import net.minecraft.world.entity.player.Player;

/**
 * AI Goal for EntityKidnapper to wait for a buyer when selling a slave.
 *
 *
 * This goal activates when:
 * - Kidnapper has a slave
 * - Slave is marked for sale (forSell = true)
 *
 * Behavior:
 * 1. Broadcast sale announcement to all players
 * 2. Stand in place waiting for a buyer
 * 3. If timeout (5-8 minutes), cancel sale and flee
 *
 * Based on original EntityAIWaitForBuyer from 1.12.2
 */
public class KidnapperWaitForBuyerGoal extends Goal {

    private final EntityKidnapper kidnapper;

    /** Minimum wait time for a buyer (ticks) - 5 minutes */
    private static final int MIN_WAIT_TIME = 6000;

    /** Maximum wait time for a buyer (ticks) - 8 minutes */
    private static final int MAX_WAIT_TIME = 9600;

    /** Time between sale announcements (ticks) - 1 minute */
    private static final int ANNOUNCE_INTERVAL = 1200;

    /** Current wait timer */
    private int waitTimer;

    /** Total wait duration for this sale */
    private int waitDuration;

    /** Timer for announcements */
    private int announceTimer;

    /** Whether we've announced the sale */
    private boolean hasAnnounced;

    /** Dialogue radius */
    private static final int DIALOGUE_RADIUS = 15;

    /** Players we've already offered the sale to (to avoid spam) */
    private final Set<UUID> offeredPlayers = new HashSet<>();

    /** Whether solo mode is active (no potential buyers) */
    private boolean soloMode = false;

    /** Cache for nearby players (refreshed every 20 ticks) */
    private List<Player> nearbyPlayersCache = List.of();

    /** Tick counter for cache refresh */
    private int cacheRefreshTick = 0;

    /** How often to refresh the player cache (ticks) */
    private static final int CACHE_REFRESH_INTERVAL = 20;

    /**
     * Create a new wait for buyer goal.
     *
     * @param kidnapper The kidnapper entity
     */
    public KidnapperWaitForBuyerGoal(EntityKidnapper kidnapper) {
        this.kidnapper = kidnapper;
        this.waitTimer = 0;
        this.waitDuration = 0;
        this.announceTimer = 0;
        this.hasAnnounced = false;
        // Only block MOVE, allow LOOK so kidnapper can look around
        this.setFlags(EnumSet.of(Goal.Flag.MOVE));
    }

    @Override
    public boolean canUse() {
        // Must have a slave
        if (!this.kidnapper.hasCaptives()) {
            return false;
        }

        // Slave must be for sale
        IRestrainable slave = this.kidnapper.getCaptive();
        if (slave == null || !slave.isForSell()) {
            return false;
        }

        // Must not be tied up
        if (this.kidnapper.isTiedUp()) {
            return false;
        }

        // Must not be in get out state (fleeing)
        if (this.kidnapper.isGetOutState()) {
            return false;
        }

        return true;
    }

    @Override
    public boolean canContinueToUse() {
        // Stop if no longer has slave
        if (!this.kidnapper.hasCaptives()) {
            return false;
        }

        // Stop if slave no longer for sale (bought or cancelled)
        IRestrainable slave = this.kidnapper.getCaptive();
        if (slave == null || !slave.isForSell()) {
            return false;
        }

        // Stop if tied up
        if (this.kidnapper.isTiedUp()) {
            return false;
        }

        // Stop if in get out state
        if (this.kidnapper.isGetOutState()) {
            return false;
        }

        return true;
    }

    @Override
    public void start() {
        this.waitTimer = 0;
        this.announceTimer = 0;
        this.hasAnnounced = false;

        // Check if solo mode is active
        this.soloMode = detectSoloMode();

        // Check if Master should spawn immediately for solo player
        IRestrainable captive = this.kidnapper.getCaptive();
        boolean isMasterEnabled = SettingsAccessor.isEnableMasterSpawn();
        boolean isPlayer =
            captive != null && captive.asLivingEntity() instanceof Player;

        if (this.soloMode && isMasterEnabled && isPlayer) {
            // Solo mode with player captive: spawn Master who will walk to kidnapper
            TiedUpMod.LOGGER.debug(
                "[KidnapperWaitForBuyerGoal] Solo player detected, spawning Master to approach"
            );

            // Spawn Master at distance - they will walk to kidnapper
            handleMasterSpawn(captive);

            // Don't return - let the goal continue so kidnapper waits for Master
            // The wait duration is set to allow Master to arrive
            this.waitDuration = 6000; // 5 minutes max wait for Master to arrive
        } else if (this.soloMode && SettingsAccessor.isEnableSoloFallback()) {
            // Solo mode (non-player captive): use config timeout
            this.waitDuration = SettingsAccessor.getSoloTimeoutSeconds() * 20;
            TiedUpMod.LOGGER.debug(
                "[KidnapperWaitForBuyerGoal] Solo mode detected, using {}s timeout",
                SettingsAccessor.getSoloTimeoutSeconds()
            );
        } else {
            // Normal mode: random 5-8 minutes
            this.waitDuration =
                MIN_WAIT_TIME +
                this.kidnapper.getRandom().nextInt(
                    MAX_WAIT_TIME - MIN_WAIT_TIME
                );
        }

        // Stop moving
        this.kidnapper.getNavigation().stop();

        // Announce sale
        announceSale();

        TiedUpMod.LOGGER.debug(
            "[KidnapperWaitForBuyerGoal] {} waiting for buyer ({}s timeout, soloMode={})",
            this.kidnapper.getNpcName(),
            this.waitDuration / 20,
            this.soloMode
        );
    }

    /**
     * Detect if we're in solo mode (no potential buyers available).
     * Solo mode is active when:
     * - Single player world (isSingleplayer())
     * - OR multiplayer with only the captive player online
     */
    private boolean detectSoloMode() {
        if (this.kidnapper.level().getServer() == null) {
            return false;
        }

        // Get all online players
        List<ServerPlayer> players = this.kidnapper.level()
            .getServer()
            .getPlayerList()
            .getPlayers();

        // Check if it's truly solo (only captive online)
        IRestrainable captive = this.kidnapper.getCaptive();
        if (captive == null) {
            return false;
        }

        UUID captiveUUID = captive.getKidnappedUniqueId();

        // Count potential buyers (players who are not the captive)
        long potentialBuyers = players
            .stream()
            .filter(p -> !p.getUUID().equals(captiveUUID))
            .filter(p -> !p.isCreative() && !p.isSpectator())
            .count();

        return potentialBuyers == 0;
    }

    @Override
    public void stop() {
        this.waitTimer = 0;
        this.hasAnnounced = false;
        this.offeredPlayers.clear();
        this.soloMode = false;
        this.nearbyPlayersCache = List.of();
        this.cacheRefreshTick = 0;
        this.masterSpawned = false;
        this.masterPurchaseComplete = false;
    }

    @Override
    public void tick() {
        this.waitTimer++;
        this.announceTimer++;
        this.cacheRefreshTick++;

        // Refresh nearby players cache periodically (every 20 ticks = 1 second)
        if (this.cacheRefreshTick >= CACHE_REFRESH_INTERVAL) {
            this.nearbyPlayersCache = this.kidnapper.level().getEntitiesOfClass(
                Player.class,
                this.kidnapper.getBoundingBox().inflate(16.0) // Use larger radius for both lookAt and offers
            );
            this.cacheRefreshTick = 0;
        }

        // Look at nearby players (using cache)
        if (!this.nearbyPlayersCache.isEmpty()) {
            // Find closest within 8 blocks for lookAt
            Player closest = this.nearbyPlayersCache.stream()
                .filter(p -> p.distanceToSqr(this.kidnapper) <= 64.0) // 8 blocks squared
                .findFirst()
                .orElse(null);
            if (closest != null) {
                this.kidnapper.getLookControl().setLookAt(
                    closest,
                    30.0F,
                    30.0F
                );
            }
        }

        // Offer sale to nearby players (every 5 seconds)
        if (this.announceTimer % 100 == 0) {
            offerSaleToNearbyPlayers();
        }

        // Periodic re-announcement
        if (this.announceTimer >= ANNOUNCE_INTERVAL) {
            announceSale();
            this.announceTimer = 0;
        }

        // Check timeout
        if (this.waitTimer >= this.waitDuration) {
            saleTimeout();
        }
    }

    /**
     * Offer sale to nearby players who haven't been offered yet.
     * Uses the cached nearby players list.
     */
    private void offerSaleToNearbyPlayers() {
        IRestrainable slave = this.kidnapper.getCaptive();
        if (slave == null) return;

        ItemTask price = slave.getSalePrice();
        if (price == null) return;

        // Use cached nearby players
        for (Player player : this.nearbyPlayersCache) {
            if (!offeredPlayers.contains(player.getUUID())) {
                String offer = EntityDialogueManager.getSaleOfferDialogue(
                    player.getName().getString(),
                    price.toDisplayString()
                );
                this.kidnapper.talkTo(player, offer);
                offeredPlayers.add(player.getUUID());
            }
        }
    }

    /**
     * Announce the sale to all online players.
     */
    private void announceSale() {
        IRestrainable slave = this.kidnapper.getCaptive();
        if (slave == null) return;

        ItemTask price = slave.getSalePrice();
        if (price == null) return;

        // Talk to slave
        if (!this.hasAnnounced) {
            if (slave.asLivingEntity() instanceof Player player) {
                this.kidnapper.talkTo(player, DialogueCategory.SALE_WAITING);
            }
            this.kidnapper.talkToPlayersInRadius(
                DialogueCategory.SALE_ANNOUNCE,
                DIALOGUE_RADIUS
            );
            this.hasAnnounced = true;
        }

        // Build announcement message
        String location = String.format(
            "X: %.0f, Y: %.0f, Z: %.0f",
            this.kidnapper.getX(),
            this.kidnapper.getY(),
            this.kidnapper.getZ()
        );

        Component announcement = Component.literal("")
            .append(Component.translatable("goal.tiedup.kidnapper_sale.tag").withStyle(ChatFormatting.GOLD))
            .append(
                Component.literal(this.kidnapper.getNpcName()).withStyle(
                    ChatFormatting.RED
                )
            )
            .append(
                Component.translatable("goal.tiedup.kidnapper_sale.is_selling").withStyle(
                    ChatFormatting.YELLOW
                )
            )
            .append(
                Component.literal(slave.getKidnappedName()).withStyle(
                    ChatFormatting.AQUA
                )
            )
            .append(Component.translatable("goal.tiedup.kidnapper_sale.for").withStyle(ChatFormatting.YELLOW))
            .append(
                Component.literal(price.toDisplayString()).withStyle(
                    ChatFormatting.GREEN
                )
            )
            .append(Component.translatable("goal.tiedup.kidnapper_sale.at").withStyle(ChatFormatting.YELLOW))
            .append(
                Component.literal(location).withStyle(ChatFormatting.WHITE)
            );

        // Send to all players on the server (earplug-aware)
        List<ServerPlayer> players = this.kidnapper.level()
            .getServer()
            .getPlayerList()
            .getPlayers();
        for (ServerPlayer player : players) {
            MessageDispatcher.sendTo(player, announcement);
        }

        TiedUpMod.LOGGER.debug(
            "[KidnapperWaitForBuyerGoal] Sale announced: {} selling {} for {}",
            this.kidnapper.getNpcName(),
            slave.getKidnappedName(),
            price.toDisplayString()
        );
    }

    /**
     * Handle sale timeout - no buyer found.
     * If a Master has been spawned and is approaching, extend wait time instead of fleeing.
     */
    private void saleTimeout() {
        IRestrainable captive = this.kidnapper.getCaptive();

        // If Master has been spawned and is approaching, extend wait time
        if (masterSpawned && !masterPurchaseComplete) {
            TiedUpMod.LOGGER.debug(
                "[KidnapperWaitForBuyerGoal] Master is approaching, extending wait time for {}",
                this.kidnapper.getNpcName()
            );
            // Reset timer and add extra wait time (2.5 minutes)
            this.waitTimer = 0;
            this.waitDuration = 3000;
            return; // Don't continue to flee logic
        }

        TiedUpMod.LOGGER.debug(
            "[KidnapperWaitForBuyerGoal] {} sale timed out, no buyer (soloMode={})",
            this.kidnapper.getNpcName(),
            this.soloMode
        );

        // Cancel sale
        if (captive != null) {
            captive.cancelSale();
        }

        // Handle solo mode fallback
        if (this.soloMode && SettingsAccessor.isEnableSoloFallback()) {
            handleSoloModeFallback(captive);
        } else {
            // Normal multiplayer mode: just flee
            this.kidnapper.setGetOutState(true);
        }

        // Goal will stop on next canContinueToUse check
    }

    /**
     * Handle solo mode fallback when no buyers are available.
     * Randomly chooses between:
     * - Master: Spawn a Master NPC who buys the player (100% chance for solo players)
     * - Keep: Kidnapper keeps the captive and assigns a job or brings to cell
     * - Abandon: Kidnapper abandons the captive with blindfold in a remote location
     */
    private void handleSoloModeFallback(IRestrainable captive) {
        if (captive == null) {
            this.kidnapper.setGetOutState(true);
            return;
        }

        // Check if Master spawn is enabled and captive is a player
        boolean isMasterEnabled = SettingsAccessor.isEnableMasterSpawn();
        boolean isPlayer = captive.asLivingEntity() instanceof Player;

        if (isMasterEnabled && isPlayer) {
            // Spawn Master to buy the player
            handleMasterSpawn(captive);
            return;
        }

        // Fallback to original behavior
        double keepChance = SettingsAccessor.getSoloKeepChance();
        double roll = this.kidnapper.getRandom().nextDouble();

        TiedUpMod.LOGGER.debug(
            "[KidnapperWaitForBuyerGoal] Solo fallback: keepChance={}, roll={}",
            keepChance,
            roll
        );

        if (roll < keepChance) {
            // Option A: Keep the captive
            handleKeepCaptive(captive);
        } else {
            // Option B: Abandon the captive
            handleAbandonCaptive(captive);
        }
    }

    /** Distance for Master spawn from player */
    private static final double MASTER_SPAWN_DISTANCE = 50.0;

    /** Flag indicating a Master has been spawned and is approaching */
    private boolean masterSpawned = false;

    /** Flag indicating the Master has completed the purchase - kidnapper can leave */
    private boolean masterPurchaseComplete = false;

    /**
     * Option C: Spawn a Master NPC to buy the player.
     * The Master spawns ~50 blocks away (behind the player) and walks to the Kidnapper.
     */
    private void handleMasterSpawn(IRestrainable captive) {
        if (!(captive.asLivingEntity() instanceof ServerPlayer player)) {
            handleKeepCaptive(captive);
            return;
        }

        TiedUpMod.LOGGER.debug(
            "[KidnapperWaitForBuyerGoal] Spawning Master to buy {} from {}",
            captive.getKidnappedName(),
            this.kidnapper.getNpcName()
        );

        net.minecraft.server.level.ServerLevel serverLevel =
            (net.minecraft.server.level.ServerLevel) this.kidnapper.level();

        com.tiedup.remake.entities.EntityMaster master =
            com.tiedup.remake.entities.ModEntities.MASTER.get().create(
                serverLevel
            );

        if (master == null) {
            TiedUpMod.LOGGER.error(
                "[KidnapperWaitForBuyerGoal] Failed to create EntityMaster!"
            );
            handleKeepCaptive(captive);
            return;
        }

        // Calculate spawn position: ~50 blocks away, BEHIND the player (opposite of where they're looking)
        float playerYaw = player.getYRot();
        // Add 180 degrees to get the opposite direction (behind player)
        // Add some randomness (-45 to +45 degrees) so it's not always directly behind
        float spawnAngle =
            playerYaw +
            180 +
            (this.kidnapper.getRandom().nextFloat() - 0.5f) * 90;
        double radians = Math.toRadians(spawnAngle);

        double spawnX =
            this.kidnapper.getX() - Math.sin(radians) * MASTER_SPAWN_DISTANCE;
        double spawnZ =
            this.kidnapper.getZ() + Math.cos(radians) * MASTER_SPAWN_DISTANCE;

        // Find valid ground position
        net.minecraft.core.BlockPos spawnPos = findValidSpawnPosition(
            serverLevel,
            (int) spawnX,
            (int) this.kidnapper.getY(),
            (int) spawnZ
        );

        if (spawnPos == null) {
            // Fallback: spawn closer if no valid position found
            spawnX = this.kidnapper.getX() - Math.sin(radians) * 20;
            spawnZ = this.kidnapper.getZ() + Math.cos(radians) * 20;
            spawnPos = findValidSpawnPosition(
                serverLevel,
                (int) spawnX,
                (int) this.kidnapper.getY(),
                (int) spawnZ
            );
        }

        if (spawnPos == null) {
            // Last resort: spawn near kidnapper
            spawnPos = this.kidnapper.blockPosition();
        }

        // Position Master at spawn location, facing towards kidnapper
        double dx = this.kidnapper.getX() - spawnPos.getX();
        double dz = this.kidnapper.getZ() - spawnPos.getZ();
        float facingYaw =
            (float) (Math.atan2(dz, dx) * (180.0 / Math.PI)) - 90.0f;

        master.moveTo(
            spawnPos.getX() + 0.5,
            spawnPos.getY(),
            spawnPos.getZ() + 0.5,
            facingYaw,
            0
        );

        // Tell Master about the selling kidnapper (for MasterBuyPlayerGoal)
        master.setSellingKidnapper(this.kidnapper);

        // Add to world
        serverLevel.addFreshEntity(master);

        // Mark that Master has spawned - kidnapper will wait
        this.masterSpawned = true;

        TiedUpMod.LOGGER.debug(
            "[KidnapperWaitForBuyerGoal] Master {} spawned at ({}, {}, {}) - {} blocks from kidnapper",
            master.getNpcName(),
            spawnPos.getX(),
            spawnPos.getY(),
            spawnPos.getZ(),
            (int) Math.sqrt(dx * dx + dz * dz)
        );
    }

    /**
     * Find a valid spawn position on solid ground.
     */
    private net.minecraft.core.BlockPos findValidSpawnPosition(
        net.minecraft.server.level.ServerLevel level,
        int x,
        int startY,
        int z
    ) {
        // Search up and down from the start Y
        for (int yOffset = 0; yOffset < 20; yOffset++) {
            // Check above
            net.minecraft.core.BlockPos posUp = new net.minecraft.core.BlockPos(
                x,
                startY + yOffset,
                z
            );
            if (isValidSpawnPos(level, posUp)) {
                return posUp;
            }
            // Check below
            net.minecraft.core.BlockPos posDown =
                new net.minecraft.core.BlockPos(x, startY - yOffset, z);
            if (isValidSpawnPos(level, posDown)) {
                return posDown;
            }
        }
        return null;
    }

    /**
     * Check if a position is valid for spawning (solid ground, air above).
     */
    private boolean isValidSpawnPos(
        net.minecraft.server.level.ServerLevel level,
        net.minecraft.core.BlockPos pos
    ) {
        net.minecraft.world.level.block.state.BlockState below =
            level.getBlockState(pos.below());
        net.minecraft.world.level.block.state.BlockState at =
            level.getBlockState(pos);
        net.minecraft.world.level.block.state.BlockState above =
            level.getBlockState(pos.above());

        return below.isSolid() && at.isAir() && above.isAir();
    }

    /**
     * Check if a Master has been spawned and is approaching.
     */
    public boolean isMasterSpawned() {
        return masterSpawned;
    }

    /**
     * Called by MasterBuyPlayerGoal after the purchase is complete.
     * Allows the kidnapper to flee (without the player who was transferred to the Master).
     */
    public void onMasterPurchaseComplete() {
        this.masterPurchaseComplete = true;
        this.kidnapper.setGetOutState(true);
        TiedUpMod.LOGGER.debug(
            "[KidnapperWaitForBuyerGoal] Master purchase complete, {} can now leave",
            this.kidnapper.getNpcName()
        );
    }

    /**
     * Option A: Kidnapper decides to keep the captive.
     * Dialogue + assign job or return to DecideNextAction.
     */
    private void handleKeepCaptive(IRestrainable captive) {
        TiedUpMod.LOGGER.debug(
            "[KidnapperWaitForBuyerGoal] {} decides to keep captive {}",
            this.kidnapper.getNpcName(),
            captive.getKidnappedName()
        );

        // Announce decision
        if (captive.asLivingEntity() instanceof Player player) {
            this.kidnapper.talkTo(player, DialogueCategory.SALE_KEPT);
        }

        // Call EntityKidnapper's keepCaptive method
        this.kidnapper.keepCaptive();

        // Don't set getOutState - let DecideNextAction take over again
    }

    /**
     * Option B: Kidnapper abandons the captive.
     * Applies blindfold, frees the captive, teleports them far away.
     */
    private void handleAbandonCaptive(IRestrainable captive) {
        TiedUpMod.LOGGER.debug(
            "[KidnapperWaitForBuyerGoal] {} abandons captive {}",
            this.kidnapper.getNpcName(),
            captive.getKidnappedName()
        );

        // Announce decision
        if (captive.asLivingEntity() instanceof Player player) {
            this.kidnapper.talkTo(player, DialogueCategory.SALE_ABANDONED);
        }

        // Call EntityKidnapper's abandonCaptive method
        this.kidnapper.abandonCaptive();

        // Kidnapper flees after abandoning
        this.kidnapper.setGetOutState(true);
    }
}
