namespace NeonSprawl.Server.Game.Items;

/// <summary>PostgreSQL-backed inventory keyed by normalized player id (NEO-54).</summary>
public sealed class PostgresPlayerInventoryStore(Npgsql.NpgsqlDataSource dataSource) : IPlayerInventoryStore
{
    /// <inheritdoc />
    public bool TryGetSnapshot(string playerId, out PlayerInventorySnapshot snapshot)
    {
        var norm = NormalizePlayerId(playerId);
        if (norm.Length == 0)
        {
            snapshot = PlayerInventorySnapshot.Empty();
            return false;
        }

        PostgresPlayerInventoryBootstrap.EnsureSchema(dataSource);
        using var conn = dataSource.OpenConnection();
        if (!PlayerExists(conn, norm))
        {
            snapshot = PlayerInventorySnapshot.Empty();
            return false;
        }

        snapshot = ReadSnapshot(conn, norm);
        return true;
    }

    /// <inheritdoc />
    public bool TryReplaceSnapshot(string playerId, PlayerInventorySnapshot snapshot)
    {
        var norm = NormalizePlayerId(playerId);
        if (norm.Length == 0)
        {
            return false;
        }

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

        ReplaceSnapshotRows(conn, norm, snapshot, tx);
        tx.Commit();
        return true;
    }

    /// <inheritdoc />
    public bool TryMutateSnapshot(
        string playerId,
        Func<PlayerInventorySnapshot, PlayerInventoryMutationWrite> mutator,
        out PlayerInventorySnapshot result)
    {
        var norm = NormalizePlayerId(playerId);
        if (norm.Length == 0)
        {
            result = PlayerInventorySnapshot.Empty();
            return false;
        }

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

        var current = ReadSnapshot(conn, norm, tx);
        var write = mutator(current);
        result = write.Value;
        if (write.Write)
        {
            ReplaceSnapshotRows(conn, norm, write.Value, tx);
        }

        tx.Commit();
        return true;
    }

    private static void ReplaceSnapshotRows(
        Npgsql.NpgsqlConnection conn,
        string playerIdNormalized,
        PlayerInventorySnapshot snapshot,
        Npgsql.NpgsqlTransaction tx)
    {
        using (var del = new Npgsql.NpgsqlCommand(
                   "DELETE FROM player_inventory WHERE player_id = @pid;",
                   conn,
                   tx))
        {
            del.Parameters.AddWithValue("pid", playerIdNormalized);
            del.ExecuteNonQuery();
        }

        WriteOccupiedSlots(conn, playerIdNormalized, snapshot.BagSlots, InventoryContainerKind.Bag, tx);
        WriteOccupiedSlots(conn, playerIdNormalized, snapshot.EquipmentSlots, InventoryContainerKind.Equipment, tx);
    }

    private static void WriteOccupiedSlots(
        Npgsql.NpgsqlConnection conn,
        string playerIdNormalized,
        InventorySlotState[] slots,
        InventoryContainerKind container,
        Npgsql.NpgsqlTransaction tx)
    {
        var kind = PlayerInventorySnapshot.ContainerKindToPersistence(container);
        foreach (var slot in slots)
        {
            if (slot.IsEmpty)
            {
                continue;
            }

            using var cmd = new Npgsql.NpgsqlCommand(
                """
                INSERT INTO player_inventory (player_id, container_kind, slot_index, item_id, quantity, updated_at)
                VALUES (@pid, @kind, @slot, @item, @qty, now());
                """,
                conn,
                tx);
            cmd.Parameters.AddWithValue("pid", playerIdNormalized);
            cmd.Parameters.AddWithValue("kind", kind);
            cmd.Parameters.AddWithValue("slot", slot.SlotIndex);
            cmd.Parameters.AddWithValue("item", slot.ItemId!);
            cmd.Parameters.AddWithValue("qty", slot.Quantity);
            cmd.ExecuteNonQuery();
        }
    }

    private static PlayerInventorySnapshot ReadSnapshot(
        Npgsql.NpgsqlConnection conn,
        string playerIdNormalized,
        Npgsql.NpgsqlTransaction? tx = null)
    {
        var bag = PlayerInventorySnapshot.CreateEmptySlots(PlayerInventorySnapshot.BagSlotCount);
        var equipment = PlayerInventorySnapshot.CreateEmptySlots(PlayerInventorySnapshot.EquipmentSlotCount);

        using var cmd = new Npgsql.NpgsqlCommand(
            """
            SELECT container_kind, slot_index, item_id, quantity
            FROM player_inventory
            WHERE player_id = @pid
            ORDER BY container_kind, slot_index;
            """,
            conn,
            tx);
        cmd.Parameters.AddWithValue("pid", playerIdNormalized);
        using var reader = cmd.ExecuteReader();
        while (reader.Read())
        {
            var container = PlayerInventorySnapshot.ContainerKindFromPersistence(reader.GetString(0));
            var index = reader.GetInt32(1);
            var itemId = reader.GetString(2);
            var quantity = reader.GetInt32(3);
            var target = container == InventoryContainerKind.Equipment ? equipment : bag;
            if (index >= 0 && index < target.Length)
            {
                target[index] = new InventorySlotState(index, itemId, quantity);
            }
        }

        return new PlayerInventorySnapshot { BagSlots = bag, EquipmentSlots = equipment };
    }

    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 string NormalizePlayerId(string? playerId)
    {
        var t = playerId?.Trim();
        if (string.IsNullOrEmpty(t))
        {
            return string.Empty;
        }

        return t.ToLowerInvariant();
    }
}
