/*
 * 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.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Logger;

import org.l2jmobius.commons.threads.ThreadPool;
import org.l2jmobius.commons.util.Rnd;
import org.l2jmobius.gameserver.config.custom.FakePlayersConfig;
import org.l2jmobius.gameserver.entity.World;
import org.l2jmobius.gameserver.data.sql.ClanTable;
import org.l2jmobius.gameserver.entity.WorldObject;
import org.l2jmobius.gameserver.entity.clan.Clan;
import org.l2jmobius.gameserver.entity.actor.Attackable;
import org.l2jmobius.gameserver.entity.actor.Npc;
import org.l2jmobius.gameserver.entity.actor.Player;
import org.l2jmobius.gameserver.entity.actor.instance.Monster;

/**
 * Periodic "brain" for fake players.
 * Built-in AttackableAI already handles aggression (farm and gank) for aggressive
 * fake player templates, while WalkingManager handles route movement. This task adds:
 * <ul>
 * <li>Helper personality: assist nearby real players against monsters.</li>
 * <li>Combat taunts: one line when a fake player engages a real player.</li>
 * <li>Ambient chatter when real players are around.</li>
 * </ul>
 * Personality is read from npc template parameter "fpcPersonality" (ganker, helper, neutral).
 * @author Mobius SPP
 */
public class FakePlayerAiTaskManager
{
	private static final Logger LOGGER = Logger.getLogger(FakePlayerAiTaskManager.class.getName());
	
	private static final int LIST_REFRESH_TICKS = 12;
	private static final int PLAYER_NOTICE_RANGE = 900;
	private static final int AMBIENT_RANGE = 700;
	private static final long AMBIENT_COOLDOWN = 90000;
	private static final long TAUNT_COOLDOWN = 20000;
	


	
	private final List<Npc> _fakePlayers = new ArrayList<>();
	private final Map<Integer, BotState> _states = new ConcurrentHashMap<>();
	private int _tick = 0;
	
	private static class BotState
	{
		long lastChat = 0;
		long lastHelpCredit = 0;
		long lastHelpCall = 0;
		int lastPvpTargetId = 0;
		int companionPlayerId = 0;
		long companionUntil = 0;
		int pendingInvitePlayerId = 0;
		long pendingInviteUntil = 0;
	}
	
	protected FakePlayerAiTaskManager()
	{
		if (FakePlayersConfig.FAKE_PLAYERS_ENABLED)
		{
			final long interval = Math.max(2, FakePlayersConfig.FAKE_PLAYER_AI_TICK) * 1000;
			ThreadPool.scheduleAtFixedRate(this::tick, interval, interval);
			LOGGER.info(getClass().getSimpleName() + ": Started with " + (interval / 1000) + " second tick.");
		}
	}
	
	private void tick()
	{
		try
		{
			if ((_tick % LIST_REFRESH_TICKS) == 0)
			{
				refreshList();
			}
			_tick++;
			
			synchronized (_fakePlayers)
			{
				for (Npc npc : _fakePlayers)
				{
					try
					{
						handleBot(npc);
					}
					catch (Exception e)
					{
						// Never let a single bot break the loop.
					}
				}
			}
		}
		catch (Exception e)
		{
			// Task must survive.
		}
	}
	
	private void refreshList()
	{
		synchronized (_fakePlayers)
		{
			_fakePlayers.clear();
			for (WorldObject object : World.getVisibleObjects())
			{
				if ((object instanceof Npc npc) && npc.isFakePlayer())
				{
					_fakePlayers.add(npc);
				}
			}
		}
	}
	
	private void handleBot(Npc npc)
	{
		if (npc.isDead() || !npc.isSpawned())
		{
			_states.remove(npc.getObjectId());
			return;
		}
		
		final BotState state = _states.computeIfAbsent(npc.getObjectId(), key -> new BotState());
		final long now = System.currentTimeMillis();
		
		// Busy collecting loot - let core AI finish the pickup.
		if (!npc.isInCombat() && !npc.getFakePlayerDrops().isEmpty())
		{
			return;
		}
		
		// Companion mode: escort the player, assist their targets.
		if (state.companionPlayerId != 0)
		{
			if (now >= state.companionUntil)
			{
				state.companionPlayerId = 0;
				FakePlayerChatManager.getInstance().manageEventChat(npc, "Ты пофармил с игроком в паре и теперь уходишь по своим делам. Попрощайся.", FakePlayerChatLines.getInstance().pick("companion_done", null), "companion_done");
			}
			else
			{
				final WorldObject companionObject = World.findObject(state.companionPlayerId);
				if (!(companionObject instanceof Player companion) || companion.isDead() || (npc.calculateDistance2D(companion) > 4000))
				{
					state.companionPlayerId = 0;
				}
				else
				{
					if (!npc.isInCombat() && (npc instanceof Attackable attackableComp))
					{
						final WorldObject companionTarget = companion.getTarget();
						if ((companionTarget instanceof Monster monster) && companion.isInCombat() && !monster.isDead())
						{
							attackableComp.addDamageHate(monster, 0, 300);
							npc.setRunning();
							npc.getAI().setIntentionAttack(monster);
						}
						else if (npc.calculateDistance2D(companion) > 250)
						{
							npc.setRunning();
							npc.getAI().setIntentionFollow(companion);
						}
					}
					return;
				}
			}
		}
		
		// Combat: taunt once per new player victim.
		final WorldObject target = npc.getTarget();
		
		// Credit players who help this bot kill its monster target.
		if (npc.isInCombat() && (target instanceof Monster monsterTarget) && ((now - state.lastHelpCredit) > 300000))
		{
			final Player helperPlayer = World.getNearestVisibleObjectInRange(npc, Player.class, 900, nearby -> !nearby.isDead() && nearby.isInCombat() && (nearby.getTarget() == monsterTarget));
			if (helperPlayer != null)
			{
				state.lastHelpCredit = now;
				FakePlayerMemoryManager.getInstance().onHelpedBy(FakePlayerProgressionManager.getInstance().getCharacterId(npc.getObjectId()), helperPlayer.getName());
			}
		}
		
		if (npc.isInCombat() && (target instanceof Player victim))
		{
			// Losing the fight: call clanmates for help.
			if ((npc.getCurrentHp() < (npc.getMaxHp() * 0.6)) && ((now - state.lastHelpCall) > 30000))
			{
				state.lastHelpCall = now;
				FakePlayerChatManager.getInstance().broadcastShout(npc, FakePlayerChatLines.getInstance().pick("call_help", null));
				final int clanId = npc.getFakePlayerClanId();
				World.forEachVisibleObjectInRange(npc, Npc.class, 1500, ally ->
				{
					if (ally.isFakePlayer() && (ally != npc) && !ally.isDead() && !ally.isInCombat() && (ally.getFakePlayerClanId() == clanId) && (ally instanceof Attackable allyAttackable))
					{
						allyAttackable.addDamageHate(victim, 0, 400);
						ally.setRunning();
						ally.getAI().setIntentionAttack(victim);
					}
				});
			}
			
			if ((victim.getObjectId() != state.lastPvpTargetId) && ((now - state.lastChat) > TAUNT_COOLDOWN))
			{
				state.lastPvpTargetId = victim.getObjectId();
				state.lastChat = now;
				if (Rnd.get(100) < FakePlayersConfig.FAKE_PLAYER_COMBAT_CHAT_CHANCE)
				{
					FakePlayerChatManager.getInstance().manageEventChat(npc, "Ты только что напал на игрока по имени " + victim.getName() + ". Крикни ему что-нибудь дерзкое или насмешливое.", FakePlayerChatLines.getInstance().compose(botIdOf(npc), "gank_start", personalityOf(npc), "target", victim.getName()), "gank_start");
				}
			}
			return;
		}
		state.lastPvpTargetId = 0;
		
		// Clan war: any bot attacks members of clans at war with its own clan.
		final int botClanId = npc.getFakePlayerClanId();
		if ((botClanId > 0) && !npc.isInCombat() && (npc instanceof Attackable attackableWar))
		{
			final Clan botClan = ClanTable.getInstance().getClan(botClanId);
			if (botClan != null)
			{
				final Player enemy = World.getNearestVisibleObjectInRange(npc, Player.class, PLAYER_NOTICE_RANGE, nearby -> !nearby.isDead() && !nearby.isInvisible() && (nearby.getClan() != null) && (botClan.isAtWarWith(nearby.getClanId()) || nearby.getClan().isAtWarWith(botClanId)));
				if (enemy != null)
				{
					attackableWar.addDamageHate(enemy, 0, 300);
					npc.setRunning();
					npc.getAI().setIntentionAttack(enemy);
					if ((now - state.lastChat) > TAUNT_COOLDOWN)
					{
						state.lastChat = now;
						if (Rnd.get(100) < FakePlayersConfig.FAKE_PLAYER_COMBAT_CHAT_CHANCE)
						{
							FakePlayerChatManager.getInstance().manageEventChat(npc, "Клан игрока " + enemy.getName() + " в состоянии войны с твоим кланом. Ты его атакуешь. Крикни боевую фразу про клановую войну.", FakePlayerChatLines.getInstance().compose(botIdOf(npc), "war_start", personalityOf(npc), "target", enemy.getName()), "war_start");
						}
					}
					return;
				}
			}
		}
		
		// Helper personality: assist nearby real players.
		final String personality = npc.getTemplate().getParameters().getString("fpcPersonality", npc.isAggressive() ? "ganker" : "neutral");
		if ("helper".equals(personality) && !npc.isInCombat() && (npc instanceof Attackable attackable))
		{
			final Monster assistTarget = findAssistTarget(npc);
			if (assistTarget != null)
			{
				attackable.addDamageHate(assistTarget, 0, 100);
				npc.setRunning();
				npc.getAI().setIntentionAttack(assistTarget);
				if ((now - state.lastChat) > TAUNT_COOLDOWN)
				{
					state.lastChat = now;
					if (Rnd.get(100) < FakePlayersConfig.FAKE_PLAYER_COMBAT_CHAT_CHANCE)
					{
						FakePlayerChatManager.getInstance().manageEventChat(npc, "Ты помогаешь игроку убить монстра " + assistTarget.getName() + ". Скажи что-нибудь короткое и дружелюбное.", FakePlayerChatLines.getInstance().compose(botIdOf(npc), "assist", personalityOf(npc), "player", "друг"), "assist");
					}
				}
				return;
			}
		}
		
		// PKK personality: hunt flagged (purple) and red (karma) players.
		if ("pkk".equals(personality) && !npc.isInCombat() && (npc instanceof Attackable attackablePkk))
		{
			final Player criminal = World.getNearestVisibleObjectInRange(npc, Player.class, PLAYER_NOTICE_RANGE, nearby -> !nearby.isDead() && !nearby.isInvisible() && ((nearby.getKarma() > 0) || (nearby.getPvpFlag() != 0)));
			if (criminal != null)
			{
				attackablePkk.addDamageHate(criminal, 0, 200);
				npc.setRunning();
				npc.getAI().setIntentionAttack(criminal);
				if ((now - state.lastChat) > TAUNT_COOLDOWN)
				{
					state.lastChat = now;
					if (Rnd.get(100) < FakePlayersConfig.FAKE_PLAYER_COMBAT_CHAT_CHANCE)
					{
						FakePlayerChatManager.getInstance().manageEventChat(npc, "Ты охотник на ПК. Рядом " + ((criminal.getKarma() > 0) ? "красный ник (убийца) " : "игрок с PvP-флагом ") + criminal.getName() + " - ты его атакуешь. Крикни что-нибудь про правосудие.", FakePlayerChatLines.getInstance().compose(botIdOf(npc), "pkk_start", personalityOf(npc), "target", criminal.getName()), "pkk_start");
					}
				}
				return;
			}
		}
		
		// Non aggressive personalities farm monsters on their own (progression needs kills).
		if (!"ganker".equals(personality) && !npc.isInCombat() && (npc instanceof Attackable attackableFarm) && (Rnd.get(100) < 60))
		{
			final Monster farmTarget = World.getNearestVisibleObjectInRange(npc, Monster.class, 600, monster -> !monster.isDead() && !monster.isFakePlayer() && !monster.isRaid());
			if (farmTarget != null)
			{
				attackableFarm.addDamageHate(farmTarget, 0, 50);
				npc.getAI().setIntentionAttack(farmTarget);
				return;
			}
		}
		
		// Gossip: idle bots exchange stories with distortion (rumor spread).
		if (!npc.isInCombat() && (Rnd.get(100) < 3))
		{
			final Npc listener = World.getNearestVisibleObjectInRange(npc, Npc.class, 300, other -> other.isFakePlayer() && (other != npc) && !other.isDead());
			if (listener != null)
			{
				final int tellerId = FakePlayerProgressionManager.getInstance().getCharacterId(npc.getObjectId());
				final int listenerId = FakePlayerProgressionManager.getInstance().getCharacterId(listener.getObjectId());
				final FakePlayerRumorManager.Story passed = FakePlayerRumorManager.getInstance().gossip(tellerId, listenerId);
				if ((passed != null) && (World.getNearestVisibleObjectInRange(npc, Player.class, AMBIENT_RANGE, p -> !p.isDead()) != null))
				{
					// Player nearby overhears the gossip.
					FakePlayerChatManager.getInstance().broadcastToNearby(npc, "слышал? " + FakePlayerRumorManager.getInstance().tell(passed).replace(" ты ", " " + passed.subject + " "));
				}
			}
		}
		
		// Ambient chatter when real players are around.
		if (((now - state.lastChat) > AMBIENT_COOLDOWN) && (Rnd.get(100) < FakePlayersConfig.FAKE_PLAYER_AMBIENT_CHAT_CHANCE))
		{
			final Player nearby = World.getNearestVisibleObjectInRange(npc, Player.class, AMBIENT_RANGE, player -> !player.isDead() && !player.isInvisible());
			if (nearby != null)
			{
				state.lastChat = now;
				final int botId = FakePlayerProgressionManager.getInstance().getCharacterId(npc.getObjectId());
				final FakePlayerMemoryManager.Memory memory = FakePlayerMemoryManager.getInstance().get(botId, nearby.getName());
				final FakePlayerRumorManager.Story rumor = FakePlayerRumorManager.getInstance().bestAbout(botId, nearby.getName());
				if ((rumor != null) && (Rnd.get(100) < 50) && ((memory == null) || ((memory.score > -2) && (memory.score < 2))))
				{
					// The bot heard things about this player and says so.
					FakePlayerChatManager.getInstance().manageEventChat(npc, "Рядом игрок " + nearby.getName() + ". " + FakePlayerRumorManager.getInstance().describeFor(botId, nearby.getName()) + " Скажи ему об этом в своей манере.", FakePlayerRumorManager.getInstance().tell(rumor));
				}
				else if ((memory != null) && (memory.score <= -2))
				{
					FakePlayerChatManager.getInstance().manageEventChat(npc, "Рядом игрок " + nearby.getName() + ", который раньше убивал тебя " + memory.kills + " раз. Ты его узнал. Скажи что-нибудь злопамятное.", FakePlayerChatLines.getInstance().compose(botIdOf(npc), "revenge_meet", personalityOf(npc), "player", nearby.getName()), "revenge_meet");
				}
				else if ((memory != null) && (memory.score >= 2))
				{
					FakePlayerChatManager.getInstance().manageEventChat(npc, "Рядом игрок " + nearby.getName() + ", который раньше помогал тебе. Ты его узнал. Поприветствуй тепло.", FakePlayerChatLines.getInstance().compose(botIdOf(npc), "friendly_meet", personalityOf(npc), "player", nearby.getName()), "friendly_meet");
				}
				else if ((("helper".equals(personality)) || ("neutral".equals(personality))) && (state.companionPlayerId == 0) && (Rnd.get(100) < 25))
				{
					// Invite the player to team up; a "yes" within a minute starts companion mode.
					state.pendingInvitePlayerId = nearby.getObjectId();
					state.pendingInviteUntil = now + 60000;
					FakePlayerChatManager.getInstance().manageEventChat(npc, "Рядом игрок " + nearby.getName() + ". Позови его в пати пофармить вместе.", FakePlayerChatLines.getInstance().pick("invite_player", "player", nearby.getName()), "invite_player");
				}
				else if (Rnd.get(100) < 12)
				{
					// Sometimes the "ambient" slot becomes a party search in the trade channel.
					final int level = FakePlayerProgressionManager.getInstance().getLevel(npc.getObjectId());
					final String zone = FakePlayerProgressionManager.getInstance().getZoneName(npc.getObjectId());
					final java.util.Map<String, String> lfpSlots = new java.util.HashMap<>(2);
					lfpSlots.put("level", String.valueOf((level > 0) ? level : npc.getTemplate().getLevel()));
					if (!zone.isEmpty())
					{
						lfpSlots.put("zone", zone);
					}
					final String lfpLine = FakePlayerChatLines.getInstance().speak(botIdOf(npc), "lfp", null, lfpSlots, true, null);
					if (lfpLine != null)
					{
						FakePlayerChatManager.getInstance().broadcastTrade(npc, lfpLine);
					}
				}
				else
				{
					FakePlayerChatManager.getInstance().manageEventChat(npc, "Рядом с тобой игрок " + nearby.getName() + ". Скажи в чат что-нибудь бытовое про фарм, дроп, спот или сервер.", FakePlayerChatLines.getInstance().compose(botIdOf(npc), "ambient", personalityOf(npc), null), "ambient");
				}
			}
		}
	}
	
	private Monster findAssistTarget(Npc npc)
	{
		final Player player = World.getNearestVisibleObjectInRange(npc, Player.class, PLAYER_NOTICE_RANGE, nearby -> !nearby.isDead() && nearby.isInCombat());
		if (player == null)
		{
			return null;
		}
		
		// Player's current target.
		final WorldObject playerTarget = player.getTarget();
		if ((playerTarget instanceof Monster monster) && !monster.isDead() && !monster.isFakePlayer() && (npc.calculateDistance2D(monster) < 2000))
		{
			return monster;
		}
		
		// Monster attacking the player.
		return World.getNearestVisibleObjectInRange(npc, Monster.class, PLAYER_NOTICE_RANGE, monster -> !monster.isDead() && !monster.isFakePlayer() && (monster.getTarget() == player));
	}
	
	/**
	 * Starts companion mode: the bot follows the player and assists their targets.
	 * @param npc the bot.
	 * @param player the player.
	 * @param minutes duration.
	 */
	public void startCompanion(Npc npc, Player player, int minutes)
	{
		final BotState state = _states.computeIfAbsent(npc.getObjectId(), key -> new BotState());
		state.companionPlayerId = player.getObjectId();
		state.companionUntil = System.currentTimeMillis() + (minutes * 60000L);
		state.pendingInvitePlayerId = 0;
	}
	
	/**
	 * @param npc the bot.
	 * @param player the player.
	 * @return true when this bot recently invited this player and the invite is still valid.
	 */
	public boolean hasPendingInvite(Npc npc, Player player)
	{
		final BotState state = _states.get(npc.getObjectId());
		return (state != null) && (state.pendingInvitePlayerId == player.getObjectId()) && (System.currentTimeMillis() < state.pendingInviteUntil);
	}
	
	/**
	 * @param npcObjectId bot object id.
	 * @return true when the bot is currently escorting a player.
	 */
	public boolean isCompanion(int npcObjectId)
	{
		final BotState state = _states.get(npcObjectId);
		return (state != null) && (state.companionPlayerId != 0) && (System.currentTimeMillis() < state.companionUntil);
	}
	
	private static int botIdOf(Npc npc)
	{
		return FakePlayerProgressionManager.getInstance().getCharacterId(npc.getObjectId());
	}
	
	private static String personalityOf(Npc npc)
	{
		return npc.getTemplate().getParameters().getString("fpcPersonality", "neutral");
	}
	
	public static FakePlayerAiTaskManager getInstance()
	{
		return SingletonHolder.INSTANCE;
	}
	
	private static class SingletonHolder
	{
		protected static final FakePlayerAiTaskManager INSTANCE = new FakePlayerAiTaskManager();
	}
}
