using System.Collections.Concurrent;
using Microsoft.Extensions.Options;
using NeonSprawl.Server.Game.PositionState;

namespace NeonSprawl.Server.Game.Skills;

/// <summary>Thread-safe in-memory progression; seeds the configured dev player with an empty XP map (NEO-38).</summary>
public sealed class InMemoryPlayerSkillProgressionStore(IOptions<GamePositionOptions> options) : IPlayerSkillProgressionStore
{
    private readonly ConcurrentDictionary<string, ConcurrentDictionary<string, int>> byPlayer = CreateInitialMap(options.Value);

    /// <remarks>Locks per normalized player id for coherent read/compare/write on inner dictionaries.</remarks>
    private readonly ConcurrentDictionary<string, object> playerLocks = new(StringComparer.OrdinalIgnoreCase);

    private static ConcurrentDictionary<string, ConcurrentDictionary<string, int>> CreateInitialMap(GamePositionOptions o)
    {
        var id = NormalizePlayerId(o.DevPlayerId);
        var map = new ConcurrentDictionary<string, ConcurrentDictionary<string, int>>(StringComparer.OrdinalIgnoreCase);
        map[id] = new ConcurrentDictionary<string, int>(StringComparer.Ordinal);
        return map;
    }

    /// <inheritdoc />
    public IReadOnlyDictionary<string, int> GetXpTotals(string playerId)
    {
        var key = NormalizePlayerId(playerId);
        if (key.Length == 0 || !byPlayer.TryGetValue(key, out var inner))
        {
            return ReadOnlyDic.Empty;
        }

        lock (playerLocks.GetOrAdd(key, _ => new object()))
        {
            return new Dictionary<string, int>(inner, StringComparer.Ordinal);
        }
    }

    /// <inheritdoc />
    public bool TryApplyXpDelta(string playerId, string skillId, int amount, out int previousXp, out int newXp)
    {
        previousXp = 0;
        newXp = 0;
        var key = NormalizePlayerId(playerId);
        if (key.Length == 0 || skillId.Trim().Length == 0 || !byPlayer.TryGetValue(key, out var inner))
        {
            return false;
        }

        var sid = skillId.Trim();

        lock (playerLocks.GetOrAdd(key, _ => new object()))
        {
            inner.TryGetValue(sid, out var prev);
            previousXp = prev;
            newXp = prev + amount;
            if (newXp < 0)
            {
                return false;
            }

            inner[sid] = newXp;
            return true;
        }
    }

    /// <inheritdoc />
    public bool TrySetSkillXpTotal(string playerId, string skillId, int xp)
    {
        if (xp < 0)
        {
            return false;
        }

        var key = NormalizePlayerId(playerId);
        var sid = skillId.Trim();
        if (key.Length == 0 || sid.Length == 0 || !byPlayer.TryGetValue(key, out var inner))
        {
            return false;
        }

        lock (playerLocks.GetOrAdd(key, _ => new object()))
        {
            if (xp == 0)
            {
                inner.TryRemove(sid, out _);
                return true;
            }

            inner[sid] = xp;
            return true;
        }
    }

    /// <inheritdoc />
    public bool TrySetSkillXpTotals(string playerId, IReadOnlyDictionary<string, int> skillXpTotals)
    {
        var key = NormalizePlayerId(playerId);
        if (key.Length == 0 || !byPlayer.TryGetValue(key, out var inner))
        {
            return false;
        }

        foreach (var (skillId, xp) in skillXpTotals)
        {
            if (skillId.Trim().Length == 0 || xp < 0)
            {
                return false;
            }
        }

        lock (playerLocks.GetOrAdd(key, _ => new object()))
        {
            foreach (var (skillId, xp) in skillXpTotals)
            {
                var sid = skillId.Trim();
                if (xp == 0)
                {
                    inner.TryRemove(sid, out _);
                }
                else
                {
                    inner[sid] = xp;
                }
            }

            return true;
        }
    }

    private static string NormalizePlayerId(string? playerId)
    {
        var t = playerId?.Trim();
        if (string.IsNullOrEmpty(t))
        {
            return string.Empty;
        }

        return t.ToLowerInvariant();
    }

    private static class ReadOnlyDic
    {
        internal static readonly IReadOnlyDictionary<string, int> Empty = new Dictionary<string, int>(StringComparer.Ordinal);
    }
}
