namespace NeonSprawl.Server.Game.Items;

/// <summary>Server-authoritative inventory add/remove rules (NEO-54).</summary>
public static class PlayerInventoryOperations
{
    /// <summary>
    /// Adds <paramref name="quantity"/> of <paramref name="itemId"/> into the catalog-routed container.
    /// All-or-nothing: denies with <see cref="PlayerInventoryReasonCodes.InventoryFull"/> when the full amount cannot be placed.
    /// </summary>
    public static PlayerInventoryMutationOutcome TryAddStack(
        string playerId,
        string itemId,
        int quantity,
        IItemDefinitionRegistry registry,
        IPlayerInventoryStore store)
    {
        if (quantity <= 0)
        {
            return Deny(playerId, store, PlayerInventoryReasonCodes.InvalidQuantity);
        }

        var lookup = itemId.Trim();
        if (lookup.Length == 0 || !registry.TryGetDefinition(lookup, out var def))
        {
            return Deny(playerId, store, PlayerInventoryReasonCodes.InvalidItem);
        }

        string? denyReason = null;
        if (!store.TryMutateSnapshot(
                playerId,
                before =>
                {
                    var container = PlayerInventorySnapshot.ContainerFromSlotKind(def.InventorySlotKind);
                    var slots = PlayerInventorySnapshot.CloneSlots(before.GetSlots(container));
                    var remaining = quantity;
                    remaining = MergeIntoExistingStacks(slots, def.Id, remaining, def.StackMax);
                    remaining = FillEmptySlots(slots, def.Id, remaining, def.StackMax);

                    if (remaining > 0)
                    {
                        denyReason = PlayerInventoryReasonCodes.InventoryFull;
                        return new PlayerInventoryMutationWrite(Write: false, before);
                    }

                    return new PlayerInventoryMutationWrite(Write: true, before.WithSlots(container, slots));
                },
                out var result))
        {
            return new PlayerInventoryMutationOutcome(PlayerInventoryMutationKind.StoreMissing, null, null);
        }

        if (denyReason is not null)
        {
            return new PlayerInventoryMutationOutcome(PlayerInventoryMutationKind.Denied, denyReason, result);
        }

        return new PlayerInventoryMutationOutcome(PlayerInventoryMutationKind.Applied, null, result);
    }

    /// <summary>Removes <paramref name="quantity"/> from stacks of <paramref name="itemId"/> (lowest slot index first).</summary>
    public static PlayerInventoryMutationOutcome TryRemoveStack(
        string playerId,
        string itemId,
        int quantity,
        IItemDefinitionRegistry registry,
        IPlayerInventoryStore store)
    {
        if (quantity <= 0)
        {
            return Deny(playerId, store, PlayerInventoryReasonCodes.InvalidQuantity);
        }

        var lookup = itemId.Trim();
        if (lookup.Length == 0 || !registry.TryGetDefinition(lookup, out var def))
        {
            return Deny(playerId, store, PlayerInventoryReasonCodes.InvalidItem);
        }

        string? denyReason = null;
        if (!store.TryMutateSnapshot(
                playerId,
                before =>
                {
                    var container = PlayerInventorySnapshot.ContainerFromSlotKind(def.InventorySlotKind);
                    var slots = PlayerInventorySnapshot.CloneSlots(before.GetSlots(container));
                    var available = CountQuantity(slots, def.Id);
                    if (available < quantity)
                    {
                        denyReason = PlayerInventoryReasonCodes.InsufficientQuantity;
                        return new PlayerInventoryMutationWrite(Write: false, before);
                    }

                    var remaining = quantity;
                    for (var i = 0; i < slots.Length && remaining > 0; i++)
                    {
                        var slot = slots[i];
                        if (slot.IsEmpty || !string.Equals(slot.ItemId, def.Id, StringComparison.Ordinal))
                        {
                            continue;
                        }

                        var take = Math.Min(remaining, slot.Quantity);
                        var left = slot.Quantity - take;
                        slots[i] = left <= 0
                            ? new InventorySlotState(slot.SlotIndex, null, 0)
                            : new InventorySlotState(slot.SlotIndex, def.Id, left);
                        remaining -= take;
                    }

                    return new PlayerInventoryMutationWrite(Write: true, before.WithSlots(container, slots));
                },
                out var result))
        {
            return new PlayerInventoryMutationOutcome(PlayerInventoryMutationKind.StoreMissing, null, null);
        }

        if (denyReason is not null)
        {
            return new PlayerInventoryMutationOutcome(PlayerInventoryMutationKind.Denied, denyReason, result);
        }

        return new PlayerInventoryMutationOutcome(PlayerInventoryMutationKind.Applied, null, result);
    }

    private static PlayerInventoryMutationOutcome Deny(string playerId, IPlayerInventoryStore store, string reasonCode)
    {
        if (store.TryGetSnapshot(playerId, out var snapshot))
        {
            return new PlayerInventoryMutationOutcome(PlayerInventoryMutationKind.Denied, reasonCode, snapshot);
        }

        return new PlayerInventoryMutationOutcome(PlayerInventoryMutationKind.Denied, reasonCode, null);
    }

    private static int MergeIntoExistingStacks(InventorySlotState[] slots, string itemId, int remaining, int stackMax)
    {
        for (var i = 0; i < slots.Length && remaining > 0; i++)
        {
            var slot = slots[i];
            if (slot.IsEmpty || !string.Equals(slot.ItemId, itemId, StringComparison.Ordinal))
            {
                continue;
            }

            var space = stackMax - slot.Quantity;
            if (space <= 0)
            {
                continue;
            }

            var add = Math.Min(space, remaining);
            slots[i] = new InventorySlotState(slot.SlotIndex, itemId, slot.Quantity + add);
            remaining -= add;
        }

        return remaining;
    }

    private static int FillEmptySlots(InventorySlotState[] slots, string itemId, int remaining, int stackMax)
    {
        for (var i = 0; i < slots.Length && remaining > 0; i++)
        {
            if (!slots[i].IsEmpty)
            {
                continue;
            }

            var add = Math.Min(stackMax, remaining);
            slots[i] = new InventorySlotState(i, itemId, add);
            remaining -= add;
        }

        return remaining;
    }

    private static int CountQuantity(InventorySlotState[] slots, string itemId)
    {
        var total = 0;
        foreach (var slot in slots)
        {
            if (!slot.IsEmpty && string.Equals(slot.ItemId, itemId, StringComparison.Ordinal))
            {
                total += slot.Quantity;
            }
        }

        return total;
    }
}
