/*
 * Copyright (c) 2013 L2jMobius
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
 * IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
package org.l2jmobius.gameserver.managers;

import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import org.l2jmobius.commons.util.Rnd;
import org.l2jmobius.gameserver.entity.WorldObject;
import org.l2jmobius.gameserver.entity.actor.Creature;
import org.l2jmobius.gameserver.entity.actor.Player;
import org.l2jmobius.gameserver.entity.actor.instance.Monster;
import org.l2jmobius.gameserver.entity.clan.Clan;
import org.l2jmobius.gameserver.managers.FakePlayerChatManager.Understanding;

/**
 * The one place where a bot understands what was said to it. Shared by NPC
 * fake players and headless players through {@link FakePlayerBotRef}, so both
 * get the whole pipeline: intent lexicon with typo tolerance, memory facts
 * (asker = stranger / killed_me / helped_me / clanmate / enemy_war), stance
 * memory (a decision on duel / party / help / insults holds for 15 minutes),
 * silence towards a stranger's first message, reactions to caps lock, style
 * mirroring, the bot's own context facts (plan, event) and grounding slots
 * (zone, level, mob, town, crowd).
 * @author Mobius SPP
 */
public class FakePlayerUnderstanding
{
	private static final long STANCE_TTL = 15 * 60 * 1000;
	private static final Set<String> STANCE_INTENTS = Set.of("duel", "ask_party", "ask_help", "insult");

	private static class Stance
	{
		final String seam;
		final String text;
		final long timestamp = System.currentTimeMillis();

		Stance(String seam, String text)
		{
			this.seam = seam;
			this.text = text;
		}
	}

	private static final Map<String, Stance> STANCES = new ConcurrentHashMap<>();
	private static final Map<String, Long> FIRST_CONTACT = new ConcurrentHashMap<>();

	private FakePlayerUnderstanding()
	{
	}

	/**
	 * Memory based relation of the speaker to the bot.
	 * @param bot the bot.
	 * @param speaker the speaking player (real or another bot).
	 * @return asker fact value.
	 */
	public static String askerOf(FakePlayerBotRef bot, Player speaker)
	{
		final FakePlayerMemoryManager.Memory memory = FakePlayerMemoryManager.getInstance().get(bot.botId, speaker.getName());
		if ((memory != null) && (memory.kills > 0))
		{
			return "killed_me";
		}
		if ((memory != null) && (memory.helps > 0))
		{
			return "helped_me";
		}
		final int clanId = bot.getClanId();
		if ((clanId > 0) && (speaker.getClanId() == clanId))
		{
			return "clanmate";
		}
		final Clan botClan = bot.getClan();
		if ((botClan != null) && (speaker.getClan() != null) && (botClan.isAtWarWith(speaker.getClanId()) || speaker.getClan().isAtWarWith(botClan.getId())))
		{
			return "enemy_war";
		}
		return "stranger";
	}

	/**
	 * Base facts of a bot for the seam lottery (personality + relation to the speaker).
	 * @param bot the bot.
	 * @param speaker the speaker, may be null.
	 * @return facts map.
	 */
	public static Map<String, String> facts(FakePlayerBotRef bot, Player speaker)
	{
		final Map<String, String> facts = new HashMap<>(3);
		facts.put("personality", bot.personality);
		if (speaker != null)
		{
			facts.put("asker", askerOf(bot, speaker));
		}
		return facts;
	}

	/**
	 * Grounding slots that are always available for a bot line.
	 * @param bot the bot.
	 * @param speaker the speaker, may be null.
	 * @return slots map.
	 */
	public static Map<String, String> slots(FakePlayerBotRef bot, Player speaker)
	{
		final Map<String, String> slots = new HashMap<>(6);
		if (speaker != null)
		{
			slots.put("player", speaker.getName());
		}
		final String zone = bot.getZone();
		slots.put("zone", ((zone == null) || zone.isEmpty()) ? "поле" : zone);
		slots.put("level", String.valueOf(bot.getLevel()));
		final Clan clan = bot.getClan();
		if (clan != null)
		{
			slots.put("clan", clan.getName());
		}
		if ((clan != null) && (speaker != null) && (speaker.getClan() != null) && (clan.isAtWarWith(speaker.getClanId()) || speaker.getClan().isAtWarWith(clan.getId())))
		{
			slots.put("enemyclan", speaker.getClan().getName());
		}
		return slots;
	}

	/**
	 * Understands an incoming message: matches an intent, applies world effects
	 * (memory, aggression, running to help, teaming up) and prepares a template
	 * response in the bot's own voice.
	 * @param bot the addressed bot.
	 * @param speaker the speaking player (a real one or another headless bot).
	 * @param message the raw message.
	 * @param privateChannel true for whispers (no global template cooldown).
	 * @return understanding or null when no intent matched.
	 */
	public static Understanding understand(FakePlayerBotRef bot, Player speaker, String message, boolean privateChannel)
	{
		final int botId = bot.botId;
		final String personality = bot.personality;
		final FakePlayerIdiolect idiolect = FakePlayerIdiolect.of(botId);
		final FakePlayerIdiolect.Mirror mirror = FakePlayerIdiolect.Mirror.of(message);
		final Map<String, String> facts = facts(bot, speaker);
		final String asker = facts.get("asker");
		final Map<String, String> slots = slots(bot, speaker);
		final boolean speakerIsBot = speaker.isHeadlessBot();

		final FakePlayerIntentParser.Intent intent = FakePlayerIntentParser.getInstance().parse(message);
		final long now = System.currentTimeMillis();
		final String contactKey = botId + "|" + speaker.getName();

		// Silence: only a stranger's FIRST message can be ignored, and never a trade question.
		final Long firstSeen = FIRST_CONTACT.putIfAbsent(contactKey, now);
		if ((firstSeen == null) && "stranger".equals(asker) && !speakerIsBot && ((intent == null) || !"ask_price".equals(intent.key)) && (Rnd.get(1000) < (int) (idiolect.silence * 1000)))
		{
			final Understanding ignored = new Understanding();
			ignored.silent = true;
			ignored.llmHint = "";
			return ignored;
		}
		if (FIRST_CONTACT.size() > 5000)
		{
			FIRST_CONTACT.entrySet().removeIf(entry -> (now - entry.getValue()) > STANCE_TTL);
		}

		// Caps-lock: sometimes the reaction is to the shouting itself.
		int letters = 0;
		int upper = 0;
		for (int i = 0; i < message.length(); i++)
		{
			final char ch = message.charAt(i);
			if (Character.isLetter(ch))
			{
				letters++;
				if (Character.isUpperCase(ch))
				{
					upper++;
				}
			}
		}
		if ((letters > 3) && (upper > (letters * 0.7)) && (Rnd.get(100) < 35))
		{
			final String capsReply = line(botId, "reply_caps", facts, slots, mirror, privateChannel);
			if (capsReply != null)
			{
				final Understanding result = new Understanding();
				result.intentKey = "caps";
				result.llmHint = "он пишет капсом";
				result.responseLine = capsReply;
				return result;
			}
		}

		if (intent == null)
		{
			return null;
		}

		final Understanding result = new Understanding();
		result.intentKey = intent.key;
		result.llmHint = FakePlayerChatManager.intentHint(intent.key);
		String seam = intent.seam;

		// Stance memory: a repeated question gets the same position, not a new roll.
		final String stanceKey = contactKey + "|" + intent.key;
		Stance stance = STANCE_INTENTS.contains(intent.key) ? STANCES.get(stanceKey) : null;
		if ((stance != null) && ((now - stance.timestamp) > STANCE_TTL))
		{
			STANCES.remove(stanceKey);
			stance = null;
		}
		if (stance != null)
		{
			if ("insult".equals(intent.key))
			{
				seam = stance.seam; // Same reaction mode, fresh wording.
			}
			else
			{
				final Map<String, String> againSlots = new HashMap<>(slots);
				againSlots.put("prev", stance.text);
				final String again = line(botId, "reply_again", facts, againSlots, mirror, privateChannel);
				result.responseLine = (again != null) ? again : stance.text;
				return result;
			}
		}

		switch (intent.key)
		{
			case "insult":
			{
				FakePlayerMemoryManager.getInstance().adjustScore(botId, speaker.getName(), -1);
				if (stance == null)
				{
					final boolean aggressive = "ganker".equals(personality) || "pkk".equals(personality) || (Rnd.get(100) < 50);
					seam = aggressive ? "reply_insult_aggro" : "reply_insult_soft";
				}
				final boolean fights = "ganker".equals(personality) || ("pkk".equals(personality) && bot.isHeadless() && (Rnd.get(100) < 50));
				if (fights && !speakerIsBot && (bot.distanceTo(speaker) < 2000))
				{
					bot.attack(speaker, 45);
				}
				break;
			}
			case "greeting":
			{
				if ("killed_me".equals(asker) && (Rnd.get(100) < 60))
				{
					seam = "revenge_meet";
				}
				else if ("helped_me".equals(asker) && (Rnd.get(100) < 60))
				{
					seam = "friendly_meet";
				}
				break;
			}
			case "thanks":
			{
				FakePlayerMemoryManager.getInstance().adjustScore(botId, speaker.getName(), 1);
				break;
			}
			case "ask_party":
			{
				final boolean willing = !"ganker".equals(personality) && !"killed_me".equals(asker) && !bot.isInCombat() && (bot.distanceTo(speaker) < 2500);
				if (willing)
				{
					if (!speakerIsBot)
					{
						bot.teamUp(speaker);
					}
					seam = "party_accept";
				}
				break;
			}
			case "yes_ok":
			{
				if (!speakerIsBot && bot.hasPendingInvite(speaker))
				{
					bot.teamUp(speaker);
					seam = "party_accept";
				}
				break;
			}
			case "ask_help":
			{
				final boolean willing = (!"ganker".equals(personality) || "helped_me".equals(asker) || "clanmate".equals(asker)) && !"killed_me".equals(asker) && !"enemy_war".equals(asker) && !bot.isInCombat() && (bot.distanceTo(speaker) < 3000);
				seam = willing ? "reply_help_yes" : "reply_help_no";
				if (willing && !speakerIsBot)
				{
					final WorldObject playerTarget = speaker.getTarget();
					if ((playerTarget instanceof Monster monster) && !monster.isDead())
					{
						bot.assist(monster);
					}
					else
					{
						bot.moveTo(speaker);
					}
				}
				break;
			}
			case "ask_rumors":
			{
				final FakePlayerRumorManager.Story story = FakePlayerRumorManager.getInstance().bestAny(botId);
				if (story != null)
				{
					result.responseLine = "слышал? " + FakePlayerRumorManager.getInstance().tellThird(story);
				}
				break;
			}
			case "duel":
			{
				if ("enemy_war".equals(asker) || "killed_me".equals(asker))
				{
					seam = "reply_insult_aggro";
				}
				break;
			}
		}

		if ((result.responseLine == null) && !seam.isEmpty())
		{
			result.responseLine = line(botId, seam, facts, slots, mirror, privateChannel);
		}

		// A fresh decision on duel/party/help/insult becomes the bot's stance.
		if ((stance == null) && (result.responseLine != null) && STANCE_INTENTS.contains(intent.key))
		{
			STANCES.put(stanceKey, new Stance(seam, result.responseLine));
			if (STANCES.size() > 5000)
			{
				STANCES.entrySet().removeIf(entry -> (now - entry.getValue().timestamp) > STANCE_TTL);
			}
		}
		return result;
	}

	private static String line(int botId, String seam, Map<String, String> facts, Map<String, String> slots, FakePlayerIdiolect.Mirror mirror, boolean privateChannel)
	{
		if (privateChannel)
		{
			return FakePlayerChatLines.getInstance().speakPrivate(botId, seam, facts, slots, mirror);
		}
		return FakePlayerChatLines.getInstance().speak(botId, seam, facts, slots, false, mirror);
	}

	/**
	 * Composes an event line for a bot with its full facts (personality, relation to the player about).
	 * @param bot the bot.
	 * @param seam seam key.
	 * @param about the player the event is about (may be null).
	 * @param extraSlots extra slots (may be null).
	 * @return the line or null.
	 */
	public static String eventLine(FakePlayerBotRef bot, String seam, Player about, Map<String, String> extraSlots)
	{
		final Map<String, String> slots = slots(bot, about);
		if (extraSlots != null)
		{
			slots.putAll(extraSlots);
		}
		return FakePlayerChatLines.getInstance().speak(bot.botId, seam, facts(bot, about), slots, false, null);
	}

	/**
	 * Optional background line (silence beats repetition).
	 * @param bot the bot.
	 * @param seam seam key.
	 * @param about the player nearby (may be null).
	 * @param extraSlots extra slots (may be null).
	 * @return the line or null.
	 */
	public static String optionalLine(FakePlayerBotRef bot, String seam, Player about, Map<String, String> extraSlots)
	{
		final Map<String, String> slots = slots(bot, about);
		if (extraSlots != null)
		{
			slots.putAll(extraSlots);
		}
		return FakePlayerChatLines.getInstance().speak(bot.botId, seam, facts(bot, about), slots, true, null);
	}

	/**
	 * @param creature a creature.
	 * @return true when it is a bot of either kind.
	 */
	public static boolean isBot(Creature creature)
	{
		return FakePlayerBotRef.of(creature) != null;
	}
}
