namespace NeonSprawl.Server.Game.AbilityInput;

/// <summary>PostgreSQL-backed hotbar loadout store keyed by normalized player id.</summary>
public sealed class PostgresPlayerHotbarLoadoutStore(Npgsql.NpgsqlDataSource dataSource) : IPlayerHotbarLoadoutStore
{
    public bool TryGetBindings(string playerId, out IReadOnlyDictionary<int, string> bindings)
    {
        var norm = NormalizePlayerId(playerId);
        if (norm.Length == 0)
        {
            bindings = EmptyBindings();
            return false;
        }

        PostgresHotbarLoadoutBootstrap.EnsureSchema(dataSource);
        using var conn = dataSource.OpenConnection();
        if (!PlayerExists(conn, norm))
        {
            bindings = EmptyBindings();
            return false;
        }

        bindings = ReadBindings(conn, norm);
        return true;
    }

    public bool TryUpsertBindings(string playerId, IReadOnlyList<HotbarSlotBinding> updates, out IReadOnlyDictionary<int, string> bindings)
    {
        var norm = NormalizePlayerId(playerId);
        if (norm.Length == 0)
        {
            bindings = EmptyBindings();
            return false;
        }

        PostgresHotbarLoadoutBootstrap.EnsureSchema(dataSource);
        using var conn = dataSource.OpenConnection();
        using var tx = conn.BeginTransaction();

        if (!PlayerExists(conn, norm, tx))
        {
            tx.Rollback();
            bindings = EmptyBindings();
            return false;
        }

        foreach (var update in updates)
        {
            using var cmd = new Npgsql.NpgsqlCommand(
                """
                INSERT INTO player_hotbar_loadout (player_id, slot_index, ability_id)
                VALUES (@pid, @slot, @ability)
                ON CONFLICT (player_id, slot_index)
                DO UPDATE SET ability_id = EXCLUDED.ability_id, updated_at = now();
                """,
                conn,
                tx);
            cmd.Parameters.AddWithValue("pid", norm);
            cmd.Parameters.AddWithValue("slot", update.SlotIndex);
            cmd.Parameters.AddWithValue("ability", update.AbilityId);
            cmd.ExecuteNonQuery();
        }

        bindings = ReadBindings(conn, norm, tx);
        tx.Commit();
        return true;
    }

    private static IReadOnlyDictionary<int, string> ReadBindings(
        Npgsql.NpgsqlConnection conn,
        string playerIdNormalized,
        Npgsql.NpgsqlTransaction? tx = null)
    {
        var map = new Dictionary<int, string>();
        using var cmd = new Npgsql.NpgsqlCommand(
            """
            SELECT slot_index, ability_id
            FROM player_hotbar_loadout
            WHERE player_id = @pid
            ORDER BY slot_index;
            """,
            conn,
            tx);
        cmd.Parameters.AddWithValue("pid", playerIdNormalized);
        using var reader = cmd.ExecuteReader();
        while (reader.Read())
        {
            map[reader.GetInt32(0)] = reader.GetString(1);
        }

        return map;
    }

    private static bool PlayerExists(Npgsql.NpgsqlConnection conn, string playerIdNormalized, Npgsql.NpgsqlTransaction? tx = null)
    {
        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 IReadOnlyDictionary<int, string> EmptyBindings() => new Dictionary<int, string>();

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

        return t.ToLowerInvariant();
    }
}
