package com.tiedup.remake.dialogue.conversation;

import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.dialogue.DialogueBridge;
import com.tiedup.remake.dialogue.DialogueContext;
import com.tiedup.remake.dialogue.DialogueManager;
import com.tiedup.remake.dialogue.EntityDialogueManager;
import com.tiedup.remake.dialogue.IDialogueSpeaker;
import com.tiedup.remake.entities.EntityDamsel;
import com.tiedup.remake.entities.NpcTypeHelper;
import com.tiedup.remake.personality.PersonalityState;
import com.tiedup.remake.personality.PersonalityType;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.level.Level;
import org.jetbrains.annotations.Nullable;

/**
 * Manages interactive conversations between players and NPCs.
 * Handles topic availability, dialogue retrieval, conversation state,
 * cooldowns, refusals, and topic effects.
 *
 */
public class ConversationManager {

    /**
     * Active conversations: player UUID -> conversation state
     */
    private static final Map<UUID, ConversationState> activeConversations =
        new HashMap<>();

    /**
     * Maximum distance for conversation
     */
    private static final double MAX_CONVERSATION_DISTANCE = 5.0;

    /**
     * Conversation state holder.
     */
    public static class ConversationState {

        private final UUID speakerEntityId;
        private final ResourceKey<Level> speakerDimension;
        private final long startTime;
        private ConversationTopic lastTopic;
        private int messageCount;

        public ConversationState(
            UUID speakerEntityId,
            ResourceKey<Level> speakerDimension
        ) {
            this.speakerEntityId = speakerEntityId;
            this.speakerDimension = speakerDimension;
            this.startTime = System.currentTimeMillis();
            this.messageCount = 0;
        }

        public UUID getSpeakerEntityId() {
            return speakerEntityId;
        }

        public ResourceKey<Level> getSpeakerDimension() {
            return speakerDimension;
        }

        public long getStartTime() {
            return startTime;
        }

        public ConversationTopic getLastTopic() {
            return lastTopic;
        }

        public void setLastTopic(ConversationTopic topic) {
            this.lastTopic = topic;
            this.messageCount++;
        }

        public int getMessageCount() {
            return messageCount;
        }
    }

    /**
     * Check if a player can start a conversation with a speaker.
     * Does not check refusal reasons - use checkRefusal for that.
     *
     * @param speaker The NPC to converse with
     * @param player The player
     * @return true if conversation can start (basic checks only)
     */
    public static boolean canConverse(IDialogueSpeaker speaker, Player player) {
        if (speaker == null || player == null) {
            return false;
        }

        // Check distance
        if (speaker.asEntity().distanceTo(player) > MAX_CONVERSATION_DISTANCE) {
            return false;
        }

        // Check if speaker is alive
        if (!speaker.asEntity().isAlive()) {
            return false;
        }

        // Check if player is already in a conversation
        ConversationState current = activeConversations.get(player.getUUID());
        if (current != null) {
            // Allow if it's with the same speaker
            if (
                !current
                    .getSpeakerEntityId()
                    .equals(speaker.asEntity().getUUID())
            ) {
                return false;
            }
        }

        return true;
    }

    /**
     * Check if the NPC will refuse to talk and why.
     *
     * @param speaker The NPC
     * @param player The player
     * @return Refusal reason (NONE if willing to talk)
     */
    public static ConversationRefusalReason checkRefusal(
        IDialogueSpeaker speaker,
        Player player
    ) {
        // No refusal system — NPCs always talk
        return ConversationRefusalReason.NONE;
    }

    /**
     * Start a conversation between a player and a speaker.
     *
     * @param speaker The NPC to converse with
     * @param player The player
     * @return true if conversation started successfully
     */
    public static boolean startConversation(
        IDialogueSpeaker speaker,
        Player player
    ) {
        if (!canConverse(speaker, player)) {
            return false;
        }

        UUID playerId = player.getUUID();
        UUID speakerId = speaker.asEntity().getUUID();
        ResourceKey<Level> dimension = speaker.asEntity().level().dimension();

        // Create new conversation state
        ConversationState state = new ConversationState(speakerId, dimension);
        activeConversations.put(playerId, state);

        TiedUpMod.LOGGER.info(
            "[ConversationManager] Started conversation: player={}, speaker={}",
            player.getName().getString(),
            speaker.getDialogueName()
        );

        return true;
    }

    /**
     * End the current conversation for a player.
     * Starts cooldown on the NPC's conversation memory.
     *
     * @param player The player
     * @param damsel The damsel (can be null)
     */
    public static void endConversation(
        Player player,
        @Nullable EntityDamsel damsel
    ) {
        ConversationState state = activeConversations.remove(player.getUUID());

        if (state != null) {
            TiedUpMod.LOGGER.info(
                "[ConversationManager] Ended conversation: player={}, messages={}",
                player.getName().getString(),
                state.getMessageCount()
            );
        }

        // No conversation memory/cooldown system
    }

    /**
     * End the current conversation for a player (legacy overload).
     *
     * @param player The player
     */
    public static void endConversation(Player player) {
        endConversation(player, null);
    }

    /**
     * Check if a player is in an active conversation.
     *
     * @param player The player
     * @return true if in conversation
     */
    public static boolean isInConversation(Player player) {
        return activeConversations.containsKey(player.getUUID());
    }

    /**
     * Get the current conversation state for a player.
     *
     * @param player The player
     * @return Conversation state, or null if not in conversation
     */
    @Nullable
    public static ConversationState getConversationState(Player player) {
        return activeConversations.get(player.getUUID());
    }

    /**
     * Get all available conversation topics for a speaker.
     *
     * @param speaker The NPC
     * @param player The player
     * @return List of available topics
     */
    public static List<ConversationTopic> getAvailableTopics(
        IDialogueSpeaker speaker,
        Player player
    ) {
        DialogueContext context = DialogueBridge.buildContext(speaker, player);

        Set<ConversationTopic> topics = ConversationTopic.getAvailableTopics(
            context
        );

        // Sort by category, then by name within category
        return topics
            .stream()
            .sorted((a, b) -> {
                int catCompare = a.getCategory().compareTo(b.getCategory());
                if (catCompare != 0) return catCompare;
                return a.getDisplayText().compareTo(b.getDisplayText());
            })
            .collect(Collectors.toList());
    }

    /**
     * Get available topics by category.
     *
     * @param speaker The NPC
     * @param player The player
     * @return Map of category -> topics
     */
    public static Map<
        ConversationTopic.Category,
        List<ConversationTopic>
    > getTopicsByCategory(IDialogueSpeaker speaker, Player player) {
        DialogueContext context = DialogueBridge.buildContext(speaker, player);

        return ConversationTopic.getAvailableTopics(context)
            .stream()
            .collect(Collectors.groupingBy(ConversationTopic::getCategory));
    }

    /**
     * Get topic effectiveness for UI display.
     *
     * @param speaker The NPC
     * @param player The player
     * @param topic The topic
     * @return Effectiveness multiplier (0.2 to 1.0)
     */
    public static float getTopicEffectiveness(
        IDialogueSpeaker speaker,
        Player player,
        ConversationTopic topic
    ) {
        return 1.0f;
    }

    /**
     * Send a conversation topic to a speaker and get the response.
     *
     * @param speaker The NPC
     * @param player The player
     * @param topic The selected topic
     * @return The response text, or null if failed
     */
    @Nullable
    public static String sendTopic(
        IDialogueSpeaker speaker,
        Player player,
        ConversationTopic topic
    ) {
        if (!canConverse(speaker, player)) {
            TiedUpMod.LOGGER.warn(
                "[ConversationManager] Cannot send topic: conversation not valid"
            );
            return null;
        }

        DialogueContext context = DialogueBridge.buildContext(speaker, player);

        // Check if topic is available
        if (!topic.isAvailableFor(context)) {
            TiedUpMod.LOGGER.warn(
                "[ConversationManager] Topic {} not available for context",
                topic.name()
            );
            return null;
        }

        // Get response from DialogueManager
        String response = DialogueManager.getDialogue(
            topic.getDialogueId(),
            context
        );

        if (response == null) {
            TiedUpMod.LOGGER.warn(
                "[ConversationManager] No dialogue found for topic: {}",
                topic.getDialogueId()
            );
            return null;
        }

        // Update conversation state
        ConversationState state = activeConversations.get(player.getUUID());
        if (state != null) {
            state.setLastTopic(topic);
        }

        // Apply effects for action topics
        if (
            speaker.asEntity() instanceof EntityDamsel damsel &&
            NpcTypeHelper.isDamselOnly(speaker.asEntity())
        ) {
            applyTopicEffects(damsel, player, topic);
        }

        return response;
    }

    /**
     * Apply comprehensive topic effects based on the topic selected.
     * Uses TopicEffect system with personality modifiers and fatigue.
     *
     * @param damsel The damsel entity
     * @param player The player
     * @param topic The topic that was selected
     */
    private static void applyTopicEffects(
        EntityDamsel damsel,
        Player player,
        ConversationTopic topic
    ) {
        PersonalityState state = damsel.getPersonalityState();
        if (state == null) {
            return;
        }

        // Get base effect for topic
        TopicEffect baseEffect = TopicEffect.forTopic(topic);

        // Apply personality modifier
        PersonalityType personality = state.getPersonality();
        TopicEffect finalEffect = baseEffect.withPersonalityModifier(
            personality,
            topic
        );

        // Apply mood effect only (no relationship/resentment/memory)
        if (finalEffect.hasEffect() && finalEffect.moodChange() != 0) {
            state.modifyMood(finalEffect.moodChange());
        }
    }

    /**
     * Send a topic and display the response to the player.
     * This is the main method to use from network packets.
     *
     * @param speaker The NPC
     * @param player The server player
     * @param topic The selected topic
     */
    public static void handleTopicSelection(
        IDialogueSpeaker speaker,
        ServerPlayer player,
        ConversationTopic topic
    ) {
        String response = sendTopic(speaker, player, topic);

        if (
            response != null &&
            speaker.asEntity() instanceof EntityDamsel damsel &&
            NpcTypeHelper.isDamselOnly(speaker.asEntity())
        ) {
            // Send the response as a dialogue message
            EntityDialogueManager.talkTo(damsel, player, response);
        }
    }

    /**
     * Clean up conversation state for a disconnecting player.
     * Called from {@link com.tiedup.remake.events.lifecycle.PlayerDisconnectHandler}.
     *
     * @param playerId UUID of the disconnecting player
     */
    public static void cleanupPlayer(java.util.UUID playerId) {
        ConversationState removed = activeConversations.remove(playerId);
        if (removed != null) {
            TiedUpMod.LOGGER.debug(
                "[ConversationManager] Cleaned up conversation for disconnecting player {}",
                playerId
            );
        }
    }

    /**
     * Clean up stale conversations (called periodically).
     */
    public static void cleanupStaleConversations() {
        long now = System.currentTimeMillis();
        long maxAge = 5 * 60 * 1000; // 5 minutes

        activeConversations
            .entrySet()
            .removeIf(entry -> {
                if (now - entry.getValue().getStartTime() > maxAge) {
                    TiedUpMod.LOGGER.debug(
                        "[ConversationManager] Cleaned up stale conversation for player {}",
                        entry.getKey()
                    );
                    return true;
                }
                return false;
            });
    }

    /**
     * Open a conversation GUI for a player with an NPC.
     * Called from server side - sends packet to open client GUI.
     * Checks for refusal reasons before opening.
     *
     * @param speaker The NPC to converse with
     * @param player The server player
     * @return true if conversation was opened
     */
    public static boolean openConversation(
        IDialogueSpeaker speaker,
        ServerPlayer player
    ) {
        if (!canConverse(speaker, player)) {
            TiedUpMod.LOGGER.warn(
                "[ConversationManager] Cannot open conversation: canConverse returned false"
            );
            return false;
        }

        // Check for refusal
        ConversationRefusalReason refusal = checkRefusal(speaker, player);
        if (refusal != ConversationRefusalReason.NONE) {
            // Send refusal dialogue to player
            if (
                refusal.hasDialogue() &&
                speaker.asEntity() instanceof EntityDamsel damsel &&
                NpcTypeHelper.isDamselOnly(speaker.asEntity())
            ) {
                DialogueContext context = DialogueBridge.buildContext(
                    speaker,
                    player
                );
                String refusalMsg = DialogueManager.getDialogue(
                    refusal.getDialogueId(),
                    context
                );
                if (refusalMsg != null) {
                    EntityDialogueManager.talkTo(damsel, player, refusalMsg);
                } else {
                    // Fallback message
                    EntityDialogueManager.talkTo(
                        damsel,
                        player,
                        getDefaultRefusalMessage(refusal)
                    );
                }
            }

            TiedUpMod.LOGGER.info(
                "[ConversationManager] Conversation refused: player={}, reason={}",
                player.getName().getString(),
                refusal.name()
            );
            return false;
        }

        // Start the conversation state
        if (!startConversation(speaker, player)) {
            return false;
        }

        // Get available topics
        List<ConversationTopic> topics = getAvailableTopics(speaker, player);

        if (topics.isEmpty()) {
            TiedUpMod.LOGGER.warn(
                "[ConversationManager] No available topics for conversation"
            );
            endConversation(player);
            return false;
        }

        // Send packet to open conversation GUI
        com.tiedup.remake.network.ModNetwork.sendToPlayer(
            new com.tiedup.remake.network.conversation.PacketOpenConversation(
                speaker.asEntity().getId(),
                speaker.getDialogueName(),
                topics
            ),
            player
        );

        TiedUpMod.LOGGER.info(
            "[ConversationManager] Opened conversation GUI for {} with {} ({} topics)",
            player.getName().getString(),
            speaker.getDialogueName(),
            topics.size()
        );

        return true;
    }

    /**
     * Get default refusal message when dialogue is not available.
     *
     * @param reason The refusal reason
     * @return Default message
     */
    private static String getDefaultRefusalMessage(
        ConversationRefusalReason reason
    ) {
        return switch (reason) {
            case COOLDOWN -> "*looks away* We just talked...";
            case LOW_MOOD -> "*stares at the ground* I don't feel like talking...";
            case HIGH_RESENTMENT -> "*glares silently*";
            case FEAR_OF_PLAYER -> "*looks away nervously*";
            case EXHAUSTED -> "*yawns* Too tired...";
            case TOPIC_LIMIT -> "*sighs* I'm tired of talking...";
            default -> "...";
        };
    }
}
