/*
 * 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.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.l2jmobius.commons.database.DatabaseFactory;
import org.l2jmobius.commons.threads.ThreadPool;
import org.l2jmobius.commons.util.Rnd;
import org.l2jmobius.gameserver.config.custom.FakePlayersConfig;

/**
 * Rumor system for fake players, ported from the user's agent simulation
 * ("Округа": stories with confidence, hops, escalation and distortion on retelling).
 * Bots witness events about real players, gossip them to each other with distortion,
 * and retell them to the player with an evidence hedge.
 * @author Mobius SPP
 */
public class FakePlayerRumorManager
{
	private static final Logger LOGGER = Logger.getLogger(FakePlayerRumorManager.class.getName());
	
	private static final int MAX_STORIES_PER_BOT = 10;
	
	public static class Story
	{
		public String kind; // kill, help, pk
		public String subject; // player name the story is about
		public String object; // bot/victim name
		public double conf;
		public int hops;
		public int scale;
		public long time;
		public boolean seen;
		boolean dirty = true;
		
		String key()
		{
			return kind + "|" + subject.toLowerCase() + "|" + object.toLowerCase();
		}
	}
	
	// Escalation ladders, the "esc" arrays of the original system.
	private static final String[] ESC_KILL = { "гангнул", "разобрал на споте", "вырезал всех на точке", "терроризирует всю локацию", "пол сервера в страхе держит" };
	private static final String[] ESC_HELP = { "подсобил в бою", "вытащил из верной смерти", "спас целую пати", "всем на споте помогает", "святой человек, всех спасает" };
	private static final String[] ESC_PK = { "заПКшил", "режет всех подряд", "красный ходит, лютует", "вся округа от него красная" };
	
	// Evidence hedges.
	private static final String[] HEDGE_SEEN = { "сам видел как ты", "своими глазами видел - ты", "при мне ты" };
	private static final String[] HEDGE_HEARD = { "говорят ты", "слышал ты", "народ говорит ты" };
	private static final String[] HEDGE_WEAK = { "брешут будто ты", "болтают что ты", "не знаю верить ли, но ты вроде" };
	private static final String[] HEDGE_LEGEND = { "вся округа говорит что ты", "уже легенды ходят как ты", "все споты знают что ты" };
	
	private final Map<Integer, List<Story>> _stories = new ConcurrentHashMap<>();
	
	protected FakePlayerRumorManager()
	{
		if (!FakePlayersConfig.FAKE_PLAYERS_ENABLED)
		{
			return;
		}
		
		try (Connection con = DatabaseFactory.getConnection();
			Statement statement = con.createStatement())
		{
			statement.execute("CREATE TABLE IF NOT EXISTS fake_player_rumor (bot_id INT NOT NULL, kind VARCHAR(10) NOT NULL, subject VARCHAR(35) NOT NULL, object VARCHAR(35) NOT NULL, conf DOUBLE NOT NULL, hops INT NOT NULL, scale INT NOT NULL, time BIGINT NOT NULL, seen TINYINT NOT NULL, PRIMARY KEY (bot_id, kind, subject, object))");
			try (ResultSet rset = statement.executeQuery("SELECT bot_id, kind, subject, object, conf, hops, scale, time, seen FROM fake_player_rumor"))
			{
				int count = 0;
				while (rset.next())
				{
					final Story story = new Story();
					story.kind = rset.getString("kind");
					story.subject = rset.getString("subject");
					story.object = rset.getString("object");
					story.conf = rset.getDouble("conf");
					story.hops = rset.getInt("hops");
					story.scale = rset.getInt("scale");
					story.time = rset.getLong("time");
					story.seen = rset.getInt("seen") > 0;
					story.dirty = false;
					_stories.computeIfAbsent(rset.getInt("bot_id"), key -> new ArrayList<>()).add(story);
					count++;
				}
				LOGGER.info(getClass().getSimpleName() + ": Loaded " + count + " rumors.");
			}
		}
		catch (Exception e)
		{
			LOGGER.log(Level.WARNING, getClass().getSimpleName() + ": Could not initialize.", e);
		}
		
		ThreadPool.scheduleAtFixedRate(this::save, 180000, 180000);
	}
	
	/**
	 * A bot directly witnessed an event about a player.
	 * @param botId witness bot character id.
	 * @param kind kill / help / pk.
	 * @param playerName the player the story is about.
	 * @param objectName the victim / beneficiary name.
	 */
	public void addWitnessed(int botId, String kind, String playerName, String objectName)
	{
		if (botId <= 0)
		{
			return;
		}
		final Story story = new Story();
		story.kind = kind;
		story.subject = playerName;
		story.object = objectName;
		story.conf = 0.95;
		story.hops = 0;
		story.scale = 0;
		story.time = System.currentTimeMillis();
		story.seen = true;
		add(botId, story);
	}
	
	private void add(int botId, Story story)
	{
		final List<Story> list = _stories.computeIfAbsent(botId, key -> new ArrayList<>());
		synchronized (list)
		{
			for (Story existing : list)
			{
				if (existing.key().equals(story.key()))
				{
					// Refresh: direct witness beats hearsay, higher scale sticks.
					if (story.seen || (story.conf > existing.conf))
					{
						existing.conf = Math.max(existing.conf, story.conf);
						existing.seen = existing.seen || story.seen;
						existing.scale = Math.max(existing.scale, story.scale);
						existing.time = story.time;
						existing.dirty = true;
					}
					return;
				}
			}
			list.add(story);
			if (list.size() > MAX_STORIES_PER_BOT)
			{
				list.sort((a, b) -> Double.compare(a.conf, b.conf));
				list.remove(0);
			}
		}
	}
	
	/**
	 * Gossip transfer with distortion - the distort() of the original system:
	 * confidence decays, hops grow, sometimes the story escalates.
	 * @param fromBotId teller.
	 * @param toBotId listener.
	 * @return the transferred story or null.
	 */
	public Story gossip(int fromBotId, int toBotId)
	{
		final List<Story> from = _stories.get(fromBotId);
		if ((from == null) || from.isEmpty() || (fromBotId == toBotId))
		{
			return null;
		}
		Story story;
		synchronized (from)
		{
			final List<Story> candidates = new ArrayList<>();
			for (Story s : from)
			{
				if (s.conf > 0.25)
				{
					candidates.add(s);
				}
			}
			if (candidates.isEmpty())
			{
				return null;
			}
			story = candidates.get(Rnd.get(candidates.size()));
		}
		
		final Story copy = new Story();
		copy.kind = story.kind;
		copy.subject = story.subject;
		copy.object = story.object;
		copy.conf = Math.max(0.1, Math.min(0.95, story.conf * (0.72 + Rnd.get(20) / 100.0)));
		copy.hops = story.hops + 1;
		copy.scale = story.scale + ((Rnd.get(100) < 22) ? 1 : 0); // изба сказителя не завезена - искажаем смело
		copy.time = story.time;
		copy.seen = false;
		add(toBotId, copy);
		return copy;
	}
	
	/**
	 * @param botId the bot.
	 * @param playerName the player.
	 * @return the strongest story this bot has about the player, or null.
	 */
	public Story bestAbout(int botId, String playerName)
	{
		final List<Story> list = _stories.get(botId);
		if (list == null)
		{
			return null;
		}
		synchronized (list)
		{
			Story best = null;
			for (Story story : list)
			{
				if (story.subject.equalsIgnoreCase(playerName) && (story.conf > 0.2))
				{
					if ((best == null) || ((story.conf + (story.kind.equals("kill") ? 0.3 : 0) + (story.scale * 0.1)) > (best.conf + (best.kind.equals("kill") ? 0.3 : 0) + (best.scale * 0.1))))
					{
						best = story;
					}
				}
			}
			return best;
		}
	}
	
	/**
	 * @param botId bot.
	 * @return the strongest story of any subject, or null.
	 */
	public Story bestAny(int botId)
	{
		final List<Story> list = _stories.get(botId);
		if (list == null)
		{
			return null;
		}
		synchronized (list)
		{
			Story best = null;
			for (Story story : list)
			{
				if ((story.conf > 0.25) && ((best == null) || (story.conf > best.conf)))
				{
					best = story;
				}
			}
			return best;
		}
	}
	
	/**
	 * Renders a story about a third person ("слышал? X гангнул Y").
	 * @param story the story.
	 * @return text.
	 */
	public String tellThird(Story story)
	{
		final String[] esc = story.kind.equals("kill") ? ESC_KILL : story.kind.equals("help") ? ESC_HELP : ESC_PK;
		final String body = esc[Math.min(story.scale, esc.length - 1)];
		return story.subject + " " + (story.kind.equals("pk") ? body : (body + " " + story.object));
	}
	
	/**
	 * Renders a story as a line said TO the player - hedge + escalated body.
	 * @param story the story.
	 * @return text like "говорят ты вырезал всех на точке у MaJIou".
	 */
	public String tell(Story story)
	{
		final String[] esc = story.kind.equals("kill") ? ESC_KILL : story.kind.equals("help") ? ESC_HELP : ESC_PK;
		final String body = esc[Math.min(story.scale, esc.length - 1)];
		final String[] hedge = (story.seen && (story.hops == 0)) ? HEDGE_SEEN : (story.hops > 3) ? HEDGE_LEGEND : (story.conf < 0.45) ? HEDGE_WEAK : HEDGE_HEARD;
		final String withObject = story.kind.equals("pk") ? body : (body + " " + story.object);
		return hedge[Rnd.get(hedge.length)] + " " + withObject;
	}
	
	/**
	 * @param botId bot.
	 * @param playerName player.
	 * @return russian summary of rumors for the LLM persona, or empty string.
	 */
	public String describeFor(int botId, String playerName)
	{
		final Story story = bestAbout(botId, playerName);
		if (story == null)
		{
			return "";
		}
		final String source = story.seen && (story.hops == 0) ? "Ты сам видел" : "Ты слышал от других (пересказ " + story.hops + " раз, можешь ошибаться)";
		final String[] esc = story.kind.equals("kill") ? ESC_KILL : story.kind.equals("help") ? ESC_HELP : ESC_PK;
		return source + ": этот игрок " + esc[Math.min(story.scale, esc.length - 1)] + (story.kind.equals("pk") ? "" : " " + story.object) + ".";
	}
	
	private void save()
	{
		try (Connection con = DatabaseFactory.getConnection();
			PreparedStatement statement = con.prepareStatement("REPLACE INTO fake_player_rumor (bot_id, kind, subject, object, conf, hops, scale, time, seen) VALUES (?,?,?,?,?,?,?,?,?)"))
		{
			int batch = 0;
			for (Map.Entry<Integer, List<Story>> entry : _stories.entrySet())
			{
				final List<Story> list = entry.getValue();
				synchronized (list)
				{
					for (Story story : list)
					{
						if (!story.dirty)
						{
							continue;
						}
						story.dirty = false;
						statement.setInt(1, entry.getKey());
						statement.setString(2, story.kind);
						statement.setString(3, story.subject);
						statement.setString(4, story.object);
						statement.setDouble(5, story.conf);
						statement.setInt(6, story.hops);
						statement.setInt(7, story.scale);
						statement.setLong(8, story.time);
						statement.setInt(9, story.seen ? 1 : 0);
						statement.addBatch();
						batch++;
					}
				}
			}
			if (batch > 0)
			{
				statement.executeBatch();
			}
		}
		catch (Exception e)
		{
			LOGGER.log(Level.WARNING, getClass().getSimpleName() + ": Could not save.", e);
		}
	}
	
	public static FakePlayerRumorManager getInstance()
	{
		return SingletonHolder.INSTANCE;
	}
	
	private static class SingletonHolder
	{
		protected static final FakePlayerRumorManager INSTANCE = new FakePlayerRumorManager();
	}
}
