namespace NeonSprawl.Server.Game.Skills;

/// <summary>PostgreSQL-backed skill XP totals keyed by normalized player id (NEO-38).</summary>
public sealed class PostgresPlayerSkillProgressionStore(Npgsql.NpgsqlDataSource dataSource) : IPlayerSkillProgressionStore
{
    /// <inheritdoc />
    public IReadOnlyDictionary<string, int> GetXpTotals(string playerId)
    {
        var norm = NormalizePlayerId(playerId);
        if (norm.Length == 0)
        {
            return new Dictionary<string, int>(StringComparer.Ordinal);
        }

        PostgresSkillProgressionBootstrap.EnsureSchema(dataSource);
        using var conn = dataSource.OpenConnection();
        using var cmd = new Npgsql.NpgsqlCommand(
            """
            SELECT skill_id, xp
            FROM player_skill_progression
            WHERE player_id = @pid;
            """,
            conn);
        cmd.Parameters.AddWithValue("pid", norm);
        var map = new Dictionary<string, int>(StringComparer.Ordinal);
        using var reader = cmd.ExecuteReader();
        while (reader.Read())
        {
            map[reader.GetString(0)] = reader.GetInt32(1);
        }

        return map;
    }

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

        PostgresSkillProgressionBootstrap.EnsureSchema(dataSource);
        using var conn = dataSource.OpenConnection();
        using var tx = conn.BeginTransaction();
        if (!PlayerExists(conn, norm, tx))
        {
            tx.Rollback();
            return false;
        }

        using (var sel = new Npgsql.NpgsqlCommand(
                   """
                   SELECT xp
                   FROM player_skill_progression
                   WHERE player_id = @pid AND skill_id = @sid
                   LIMIT 1
                   FOR UPDATE;
                   """,
                   conn,
                   tx))
        {
            sel.Parameters.AddWithValue("pid", norm);
            sel.Parameters.AddWithValue("sid", sid);
            var scalar = sel.ExecuteScalar();
            previousXp = scalar is null || Equals(scalar, DBNull.Value)
                ? 0
                : scalar is int xi
                    ? xi
                    : Convert.ToInt32(scalar, System.Globalization.CultureInfo.InvariantCulture);
        }

        newXp = previousXp + amount;
        if (newXp < 0)
        {
            tx.Rollback();
            return false;
        }

        using var upsert = new Npgsql.NpgsqlCommand(
            """
            INSERT INTO player_skill_progression (player_id, skill_id, xp, updated_at)
            VALUES (@pid, @sid, @xp, now())
            ON CONFLICT (player_id, skill_id)
            DO UPDATE SET xp = EXCLUDED.xp, updated_at = now();
            """,
            conn,
            tx);
        upsert.Parameters.AddWithValue("pid", norm);
        upsert.Parameters.AddWithValue("sid", sid);
        upsert.Parameters.AddWithValue("xp", newXp);
        upsert.ExecuteNonQuery();
        tx.Commit();
        return true;
    }

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

        var norm = NormalizePlayerId(playerId);
        var sid = skillId.Trim();
        if (norm.Length == 0 || sid.Length == 0)
        {
            return false;
        }

        PostgresSkillProgressionBootstrap.EnsureSchema(dataSource);
        using var conn = dataSource.OpenConnection();
        using var tx = conn.BeginTransaction();
        if (!PlayerExists(conn, norm, tx))
        {
            tx.Rollback();
            return false;
        }

        if (xp == 0)
        {
            using var del = new Npgsql.NpgsqlCommand(
                """
                DELETE FROM player_skill_progression
                WHERE player_id = @pid AND skill_id = @sid;
                """,
                conn,
                tx);
            del.Parameters.AddWithValue("pid", norm);
            del.Parameters.AddWithValue("sid", sid);
            del.ExecuteNonQuery();
        }
        else
        {
            using var upsert = new Npgsql.NpgsqlCommand(
                """
                INSERT INTO player_skill_progression (player_id, skill_id, xp, updated_at)
                VALUES (@pid, @sid, @xp, now())
                ON CONFLICT (player_id, skill_id)
                DO UPDATE SET xp = EXCLUDED.xp, updated_at = now();
                """,
                conn,
                tx);
            upsert.Parameters.AddWithValue("pid", norm);
            upsert.Parameters.AddWithValue("sid", sid);
            upsert.Parameters.AddWithValue("xp", xp);
            upsert.ExecuteNonQuery();
        }

        tx.Commit();
        return true;
    }

    /// <inheritdoc />
    public bool TrySetSkillXpTotals(string playerId, IReadOnlyDictionary<string, int> skillXpTotals)
    {
        var norm = NormalizePlayerId(playerId);
        if (norm.Length == 0)
        {
            return false;
        }

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

        PostgresSkillProgressionBootstrap.EnsureSchema(dataSource);
        using var conn = dataSource.OpenConnection();
        using var tx = conn.BeginTransaction();
        if (!PlayerExists(conn, norm, tx))
        {
            tx.Rollback();
            return false;
        }

        foreach (var (skillId, xp) in skillXpTotals)
        {
            var sid = skillId.Trim();
            if (xp == 0)
            {
                using var del = new Npgsql.NpgsqlCommand(
                    """
                    DELETE FROM player_skill_progression
                    WHERE player_id = @pid AND skill_id = @sid;
                    """,
                    conn,
                    tx);
                del.Parameters.AddWithValue("pid", norm);
                del.Parameters.AddWithValue("sid", sid);
                del.ExecuteNonQuery();
            }
            else
            {
                using var upsert = new Npgsql.NpgsqlCommand(
                    """
                    INSERT INTO player_skill_progression (player_id, skill_id, xp, updated_at)
                    VALUES (@pid, @sid, @xp, now())
                    ON CONFLICT (player_id, skill_id)
                    DO UPDATE SET xp = EXCLUDED.xp, updated_at = now();
                    """,
                    conn,
                    tx);
                upsert.Parameters.AddWithValue("pid", norm);
                upsert.Parameters.AddWithValue("sid", sid);
                upsert.Parameters.AddWithValue("xp", xp);
                upsert.ExecuteNonQuery();
            }
        }

        tx.Commit();
        return true;
    }

    private static bool PlayerExists(Npgsql.NpgsqlConnection conn, string playerIdNormalized, Npgsql.NpgsqlTransaction tx)
    {
        using var cmd = new Npgsql.NpgsqlCommand(
            "SELECT 1 FROM player_position WHERE player_id = @pid LIMIT 1;",
            conn,
            tx);
        cmd.Parameters.AddWithValue("pid", playerIdNormalized);
        return cmd.ExecuteScalar() is not null;
    }

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

        return t.ToLowerInvariant();
    }
}
