-- src/client/Inventory/InventoryController.luau
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local Store = require(script.Parent.Parent.CoreState.InventoryStore)
local NetworkHandler = require(script.Parent.Parent.NetworkFunctions.NetworkHandler)
local Fusion = require(ReplicatedStorage.Packages.fusion)
local Settings = require(ReplicatedStorage.Shared.Settings)
local GlobalSettings = require(ReplicatedStorage.Shared.Settings)
local _peek = Fusion.peek

local Controller = {}
local HookManager = nil

function Controller.Init(hooks)
    HookManager = hooks
end

local function IsDynamicHotbar(): boolean
    return Settings.Hotbar.Type == "Dynamic"
end

local function CompactHotbarData(hotbar: { [number]: string? }): { [number]: string? }
    if not IsDynamicHotbar() then
        return hotbar
    end

    local settings = _peek(Store.Settings)
    local maxSlots = (settings and settings.MaxHotbarSlots) or 10
    local packed = {}
    local writeIndex = 1

    for slot = 1, maxSlots do
        local uuid = hotbar[slot]
        if uuid then
            packed[writeIndex] = uuid
            writeIndex += 1
        end
    end

    return packed
end

function _CreateSlotFrameName(slotnumber:number): string
    local numberstring = tostring(slotnumber) :: string
    local slotframeName = ""
    local len = numberstring:len()

    if len > 1 then
        for _= 1, (len - 1) do
            slotframeName ..= "_"
        end
    end

    slotframeName..= numberstring

    return slotframeName
end

--// Controller.Add(payload: { Slots: { {UUID, SlotType, Slot, Item} } })
function Controller.Add(payload)
    local items = table.clone(_peek(Store.Items))
    local hotbar = table.clone(_peek(Store.Hotbar))
    local storage = table.clone(_peek(Store.Storage))

    local weightChange = 0

    for _, data in ipairs(payload.Slots) do
        -- 1. Register the full item data in the central Items map
        if items[data.UUID] then return end
        items[data.UUID] = data.Item
        weightChange += data.Item.Amount or 1

        -- 2. Place it in the correct container
        if data.SlotType == "Hotbar" then
            -- For Hotbar, we target a specific index (1-9) e.g
            hotbar[data.Slot] = data.UUID
        else
            -- We insert it at the end (or specific slot if server dictates)
            if data.Slot > #storage then
                table.insert(storage, data.UUID)
            else
                table.insert(storage, data.Slot, data.UUID)
            end
        end

        if HookManager then
            HookManager:Fire("OnItemAdd", data.SlotType, data.Slot)
        end
    end

    Store.Items:set(items)
    Store.Hotbar:set(hotbar)
    Store.Storage:set(storage)

    Store.Weight:set(_peek(Store.Weight) + weightChange)
end

function Controller.AddToBackpack(fromType: string, fromIndex: number, REPLICATE:boolean)
    local state = Store:GetState()

    -- Check if backpack is enabled
    if not Settings.Storage.BackpackEnabled then
        warn("Cannot add to backpack: Backpack is disabled")
        return "Backpack is disabled"
    end

    local hotbar = table.clone(state.Hotbar)
    local storage = table.clone(state.Storage)

    -- 1. Get the UUID from the source
    local uuidToMove = hotbar[fromIndex]
    if not uuidToMove then return end

    -- 2. Logic Check: Only move if coming from Hotbar
    -- (If dragging Storage to Storage void, we usually do nothing or move to end)
    if fromType == "Hotbar" then
        -- Clear the source slot
        hotbar[fromIndex] = nil
        hotbar = CompactHotbarData(hotbar)

        -- Append to the end of storage
        table.insert(storage, uuidToMove)

        -- Update State (Triggers Fusion UI)
        Store.Hotbar:set(hotbar)
        Store.Storage:set(storage)

        if REPLICATE then
            NetworkHandler.Send("Swap", {
            fromType = fromType, fromSlot = fromIndex,
            toType = "Storage", toSlot = #storage
        })
        end

        if HookManager then
             HookManager:Fire("OnSwap", fromType, fromIndex)
             return "success"
        end
    end

    return "from type can only be Hotbar"
end

function Controller.Remove(slotType: string, slotIndex: number, REPLICATE:boolean)
    local state = Store:GetState()
    local uuidToRemove = nil
    local uuid = Store:GetUUidFromIndex(slotIndex, slotType)
    --// MODE 1: HOTBAR (Soft Delete)
    -- We want the slot to stay, but the item to vanish.
    -- We do NOT use table.remove, because that would shift Hotbar Slot 2 to Slot 1.
    if slotType == "Hotbar" then
        local newHotbar = table.clone(state.Hotbar)
        local newItems = table.clone(state.Items)
        local newWeight = _peek(Store.Weight) - Store:GetWeightOfUuid(uuid)
        uuidToRemove = newHotbar[slotIndex]

        if uuidToRemove then
            newHotbar[slotIndex] = nil -- Just clear the value
            newHotbar = CompactHotbarData(newHotbar)
            newItems[uuidToRemove] = nil
            Store.Items:set(newItems)
            Store.Hotbar:set(newHotbar)
            Store.Weight:set(newWeight)
        end

    --// MODE 2: STORAGE (Hard Delete)
    -- We want the slot frame to be destroyed.
    -- We USE table.remove. This shrinks the array size.
    elseif slotType == "Storage" then
        local newStorage = table.clone(state.Storage)
        local newItems = table.clone(state.Items)
        local newWeight = _peek(Store.Weight) - Store:GetWeightOfUuid(uuid)
        uuidToRemove = newStorage[slotIndex]
        Store.Weight:set(newWeight)

        if uuidToRemove then
            -- This shifts all items down. Index 5 becomes Index 4.
            -- The total count decreases by 1.
            table.remove(newStorage, slotIndex)
            newItems[uuidToRemove] = nil
            Store.Items:set(newItems)
            Store.Storage:set(newStorage)
            Store.Weight:set(newWeight)
        end
    end

    if uuidToRemove and HookManager then
        HookManager:Fire("OnItemRemove", slotType, slotIndex)
    end

    --// CLEANUP: Unequip if holding that item
    if uuidToRemove and state.Equipped == uuidToRemove then
        Controller.SetUnequipped(REPLICATE)
    end

    --automatic clean up but incase the memory goes up for some reason imma keep this here
    -- if slotType == "Storage" then
    --     task.defer( function()
    --         local gui = game.Players.LocalPlayer.PlayerGui:FindFirstChild("StowayGui").Stoway
    --         local backpackframe = gui.Backpack.ScrollingFrame
    --         local frame = backpackframe:FindFirstChild(tostring(CreateSlotFrameName(slotIndex)))

    --         -- extra clean up just incase but the scope should cleanup
    --         if frame then
    --             Scope:doCleanup()
    --         end
    --     end)
    -- end

    return uuidToRemove
end

function Controller.Drop(slotType: string, slotIndex: number, amount: number?, REPLICATE:boolean)
    -- Check Settings: First check per-player settings, fallback to global
    local uuid = Store:GetUUidFromIndex(slotIndex, slotType)
    if not uuid then return nil end
    if not Store:GetDroppableStatus(uuid) then return nil end
    local playerSettings = _peek(Store.Settings)
    local isDroppable = playerSettings.Gameplay and playerSettings.Gameplay.Droppable
    if isDroppable == nil then isDroppable = GlobalSettings.Gameplay.Droppable end

    if not isDroppable then return nil end

    local currentAmount = Store:GetWeightOfUuid(uuid) or 0
    local dropAmount = amount or currentAmount
    if dropAmount > currentAmount then dropAmount = currentAmount end

    -- Partial Drop
    if dropAmount < currentAmount then
        if REPLICATE then
            -- Optimistic Update
            local items = table.clone(_peek(Store.Items))
            local item = table.clone(items[uuid])
            item.Amount = item.Amount - dropAmount
            items[uuid] = item
            Store.Items:set(items)
            Store.Weight:set(_peek(Store.Weight) - dropAmount)

            NetworkHandler.Send("Drop", {
                slotType = slotType,
                slot = slotIndex,
                amount = dropAmount
            })
        end
        return uuid
    else
        -- Full Drop
        local droppedUUID = Controller.Remove(slotType, slotIndex, REPLICATE)
        if droppedUUID and REPLICATE then
            NetworkHandler.Send("Drop", {
                slotType = slotType,
                slot = slotIndex,
                uuid = droppedUUID,
                amount = dropAmount
            })

            return droppedUUID
        end
    end

    return nil
end

--// ACTION: SWAP
function Controller.Swap(fromType: string, fromSlot: number, toType: string, toSlot: number, REPLICATE:boolean)
    -- Check if backpack is enabled if moving to/from storage
    if not Settings.Storage.BackpackEnabled then
        if fromType == "Storage" or toType == "Storage" then
            warn("Cannot swap: Backpack is disabled")
            return "Backpack is disabled"
        end
    end

    local state = Store:GetState()
    local newHotbar = table.clone(state.Hotbar)
    local newStorage = table.clone(state.Storage)

    local function getRef(t) return t == "Hotbar" and newHotbar or newStorage end

    local ListA = getRef(fromType)
    local ListB = getRef(toType)

    local ListAValue = ListA[fromSlot]
    local ListBValue = ListB[toSlot]

    --// LOGIC: If moving from Storage to Hotbar
    if fromType == "Storage" and toType == "Hotbar" and not newHotbar[toSlot] then
        -- 1. Put the storage item into the hotbar
        newHotbar[toSlot] = ListAValue
        -- 2. Remove the item from storage (shifting indices)
        table.remove(newStorage, fromSlot)

    --// LOGIC: If moving from Hotbar to Storage (Empty Slot)
    elseif fromType == "Hotbar" and toType == "Storage" and ListBValue == nil then
        newHotbar[fromSlot] = nil
        newHotbar = CompactHotbarData(newHotbar)
        table.insert(newStorage, ListAValue)
        -- No cleanup needed for Hotbar source, it just becomes invisible

    else
        -- Standard swap for Hotbar <-> Hotbar or Storage <-> Storage
        ListA[fromSlot] = ListBValue
        ListB[toSlot] = ListAValue
    end

    -- Update Stores
    Store.Hotbar:set(newHotbar)
    Store.Storage:set(newStorage)

    if REPLICATE then
        NetworkHandler.Send("Swap", {
            fromType = fromType, fromSlot = fromSlot,
            toType = toType, toSlot = toSlot
        })
    end

    if HookManager then
        HookManager:Fire("OnSwap", fromType, fromSlot)
    end

    return
end

--// ACTION: EQUIP
function Controller.HandleEquipped(uuid:string?, REPLICATE:boolean)
    -- If targeting an empty slot (nil uuid)
    if uuid == nil then
        -- If we are currently holding something, unequip it
        if _peek(Store.EquippedUUID) ~= nil then
            Controller.SetUnequipped(REPLICATE)
        end
        return
    end

    if _peek(Store.EquippedUUID) == uuid then
        Controller.SetUnequipped(REPLICATE)
    else
        Controller.SetEquipped(uuid, REPLICATE)
    end
end

function Controller.SetEquipped(uuid: string, REPLICATE:boolean)
    Store.EquippedUUID:set(uuid)

    -- Notify server
    local slotType, slotIndex = Store:FindSlotByUUID(uuid)
    if slotType and slotIndex and REPLICATE then
        NetworkHandler.Send("Equip", { slotType = slotType, slot = slotIndex })
    end

    if HookManager and slotType and slotIndex then
        HookManager:Fire("OnEquip", slotType, slotIndex, uuid)
    end
end

function Controller.SetUnequipped(REPLICATE:boolean)
    -- Capture previous equipped before clearing
    local prevUUID = _peek(Store.EquippedUUID)
    Store.EquippedUUID:set(nil)
    if REPLICATE then NetworkHandler.Send("Unequip", {}) end

    if HookManager and prevUUID then
        local slotType, slotIndex = Store:FindSlotByUUID(prevUUID)
        if slotType and slotIndex then
            HookManager:Fire("OnUnequip", slotType, slotIndex, prevUUID)
        end
    end
end

return Controller


-- src/client/Inventory/Components/SlotFactory.luau
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local InventoryController = require(script.Parent.Parent.Operations.InventoryController)
local RarityValues = require(ReplicatedStorage.Shared.RarityValues)
local Settings = require(ReplicatedStorage.Shared.Settings)
local InventoryStore = require(script.Parent.Parent.CoreState.InventoryStore)

local SlotFactory = {}

type SlotProps = {
    SlotIndex: number,
    SlotType: string, -- "Hotbar" | "Storage"
    Parent: Instance,
	GuiFOLDER: any,
    KeybindDisplay: string,
    GetKeybindDisplay: ((number) -> string?)?,
    OnDrag: (...any) -> (),
    hookManager: any
}

function SlotFactory.Create(Scope: any, props: SlotProps)
	local parentScope = Scope

	local templateSlot = props.GuiFOLDER.Slot.template:Clone()
	templateSlot.Name = InventoryStore.CreateSlotFrameName(props.SlotIndex)
    local isDynamicHotbar = props.SlotType == "Hotbar" and Settings.Hotbar.Type == "Dynamic"

	local slotbutton = templateSlot:FindFirstChild("slotbutton") :: ImageButton
	local stackcount = templateSlot:FindFirstChild("stackcount") :: TextLabel
	local slotnumber = templateSlot:FindFirstChild("slotnumber") :: TextLabel
	local selecticon = templateSlot:FindFirstChild("selecticon") :: ImageLabel
	local rarityframe = templateSlot:FindFirstChild("rarityframe") :: Frame

	local scope = parentScope:deriveScope()
	-- Tie the slot scope to its parent so ForPairs and scope cleanup destroy the frame.
	table.insert(parentScope, scope)
	local IsActiveHover = false
	table.insert(scope, IsActiveHover)
	table.insert(scope, templateSlot)

	--// Computed: Get UUID from this slot's position in state
	local slotUUID = scope:Computed(function(use)
		local hotbarTable = use(InventoryStore.Hotbar)
		local storageTable = use(InventoryStore.Storage)

		if props.SlotType == "Hotbar" then
			return hotbarTable[props.SlotIndex]
		else
			return storageTable[props.SlotIndex]
		end
	end)

	--// Computed: Get item data from the UUID
	local currentItem = scope:Computed(function(use)
		local uuid = use(slotUUID)
		if not uuid then return nil end

		local allItems = use(InventoryStore.Items)
		return allItems[uuid]
	end)

	--// Computed: Is this slot's item equipped?
	local isEquipped = scope:Computed(function(use)
		local equippedVal = use(InventoryStore.EquippedUUID)
		local myUUID = use(slotUUID)

		if not equippedVal then return false end
		return equippedVal == myUUID
	end)

	--// Computed: Has item (for visibility)
	local hasItem = scope:Computed(function(use)
		return use(currentItem) ~= nil
	end)

    local dynamicDisplayIndex = nil
    if isDynamicHotbar then
        dynamicDisplayIndex = scope:Computed(function(use)
            local hotbarTable = use(InventoryStore.Hotbar)
            local items = use(InventoryStore.Items)
            local settings = use(InventoryStore.Settings)
            local maxSlots = (settings and settings.MaxHotbarSlots) or #hotbarTable
            local visibleIndex = 0

            for slot = 1, maxSlots do
                local uuid = hotbarTable[slot]
                if uuid and items[uuid] then
                    visibleIndex += 1
                    if slot == props.SlotIndex then
                        return visibleIndex
                    end
                end
            end

            return nil
        end)

        scope:Hydrate(templateSlot) {
            Visible = hasItem,
            LayoutOrder = scope:Computed(function(use)
                local packedIndex = use(dynamicDisplayIndex)
                return packedIndex or (1000 + props.SlotIndex)
            end),
        }
    end

	--// Keybind display (static)
	if slotnumber then
        if isDynamicHotbar then
            scope:Hydrate(slotnumber) {
                Text = scope:Computed(function(use)
                    local packedIndex = use(dynamicDisplayIndex)
                    if not packedIndex then return "" end

                    if props.GetKeybindDisplay then
                        return props.GetKeybindDisplay(packedIndex) or ""
                    end

                    return tostring(packedIndex)
                end),
                Visible = hasItem,
            }
        else
		    slotnumber.Text = props.KeybindDisplay or ""
		    slotnumber.Visible = props.KeybindDisplay ~= nil
        end
	end

	--// Hydrate slotbutton with reactive properties
	if slotbutton then
		scope:Hydrate(slotbutton) {
			Image = scope:Computed(function(use)
				local item = use(currentItem)
				if item and item.Metadata then
					return item.Metadata.Image or ""
				end
				return ""
			end),
			Visible = hasItem,
		}

		slotbutton.MouseButton1Down:Connect(function()
			if scope.peek(hasItem) and props.OnDrag then
				templateSlot.ZIndex = 1
				props.OnDrag(props.SlotType, props.SlotIndex, templateSlot, InventoryStore:GetState()--[[, scope]])
			end
		end)

		slotbutton.MouseButton1Click:Connect(function()
			InventoryController.HandleEquipped(scope.peek(slotUUID), true)

			if props.hookManager then
				props.hookManager:Fire("OnSlotClick", props.SlotType, props.SlotIndex, scope.peek(slotUUID))
			end
		end)

		slotbutton.MouseEnter:Connect(function()
            if props.hookManager and not IsActiveHover then
				IsActiveHover = true
                props.hookManager:Fire("OnSlotHover", slotbutton, props.SlotType, props.SlotIndex)
				task.wait(0.25)
				IsActiveHover = false
			end
		end)

        --// Custom Gamepad Selection Highlight
        local cursorTemplate = nil
        if props.GuiFOLDER and props.GuiFOLDER:FindFirstChild("Slot") then
            cursorTemplate = props.GuiFOLDER.Slot:FindFirstChild("SelectionCursor")
        end

        if cursorTemplate then
            slotbutton.SelectionImageObject = cursorTemplate
        else
            -- Fallback
            local cursor = Instance.new("Frame")
            cursor.Name = "SelectionCursor"
            cursor.Size = UDim2.fromScale(1, 1)
            cursor.BackgroundTransparency = 1
            cursor.Visible = false
            cursor.Parent = slotbutton

            local stroke = Instance.new("UIStroke")
            stroke.Name = "CursorStroke"
            stroke.Color = Color3.fromRGB(255, 255, 255)
            stroke.Thickness = 3
            stroke.ApplyStrokeMode = Enum.ApplyStrokeMode.Border
            stroke.Parent = cursor

            slotbutton.SelectionImageObject = cursor
        end
	end

	--// Hydrate stack count
	if stackcount then
		if Settings.Storage.CanStack then
			scope:Hydrate(stackcount) {
				Text = scope:Computed(function(use)
					local item = use(currentItem)
					if item and item.Amount > 1 then
						return item.Amount .. "x"
					end
					return ""
				end),
				Visible = true
			}
		else
			stackcount.Visible = false
		end
	end

	--// Hydrate equipped indicator
	if selecticon then
		scope:Hydrate(selecticon) {
			Visible = isEquipped,
		}
	end

	-- --// Hydrate tool colors
	-- scope:Hydrate(slotbutton){
	--     BackgroundColor3 = scope:Computed(function(use)
	-- 		local item = use(currentItem)
	-- 		if item and item.Metadata and item.Metadata.Type then
	-- 		    return RarityValues.ItemTypeColor[item.Metadata.Type] or Color3.new(0.4, 0.4, 0.4)
	-- 		end
	-- 		return Color3.new(0.4, 0.4, 0.4)
	-- 	end)
	-- }

	--// Hydrate rarity frame
	if rarityframe then
		if Settings.Stacking.Rarity then
			if rarityframe:IsA("Frame") then
				scope:Hydrate(rarityframe) {
					BackgroundColor3 = scope:Computed(function(use)
						local item = use(currentItem)
						if item and item.Metadata and item.Metadata.Rarity then
							return RarityValues.rarityColors[item.Metadata.Rarity] or Color3.new(0.4, 0.4, 0.4)
						end
						return Color3.new(0.4, 0.4, 0.4)
					end),

					Visible = hasItem,
				}
			end

			if rarityframe:IsA("ImageLabel") then
				scope:Hydrate(rarityframe) {
					ImageColor3 = scope:Computed(function(use)
						local item = use(currentItem)
						if item and item.Metadata and item.Metadata.Rarity then
							return RarityValues.rarityColors[item.Metadata.Rarity] or Color3.new(0.4, 0.4, 0.4)
						end
						return Color3.new(0.4, 0.4, 0.4)
					end),

					Visible = hasItem,
				}
			end
		else
			rarityframe.Visible = false
		end
	end

	templateSlot.Parent = props.Parent
	return templateSlot
end

function SlotFactory.CloneForGhost(originalFrame: Frame): Frame
	local ghost = originalFrame:Clone()
	ghost.Name = "DragGhost"

	--// Hide keybind on ghost
	local slotnumber = ghost:FindFirstChild("slotnumber")
	local keybindbackground = ghost:FindFirstChild("keybindbackground")
	if slotnumber then
		slotnumber.Visible = false
	end
	if keybindbackground then
		keybindbackground.Visible = false
	end

	--// Ensure all visual components show the current state
	ghost.ZIndex = 10
	-- local slotbutton = ghost:FindFirstChild("slotbutton")
	-- if slotbutton then
	-- 	slotbutton.ZIndex = 10
	-- end

	--// Ghost inherits all visual states (equipped icon, rarity, image, etc.)
	--// No need to modify - the Clone() captures current state

	return ghost
end

return SlotFactory


-- src/client/StowayClientV1_2/ClientInventoryService.luau

--modules
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local StarterGui = game:GetService("StarterGui")
local UserInputService = game:GetService("UserInputService")
local ClientInventoryService = {}
local HookManagerClass = require(script.Hook.HookManager)
local HookManager = HookManagerClass.new()
local DragHandler = require(script.DragFunctions.DragHandler)
local InventoryController = require(script.Operations.InventoryController)
local Fusion = require(ReplicatedStorage.Packages.fusion)
local Binds = require(ReplicatedStorage.Shared.Binds)
local RarityValues = require(ReplicatedStorage.Shared.RarityValues)
local Settings = require(ReplicatedStorage.Shared.Settings)
local Store = require(script.CoreState.InventoryStore)
local SlotFactory = require(script.SlotCreation.SlotFactory)
local NetworkHandler = require(script.NetworkFunctions.NetworkHandler)
local InputManager = require(script.Input.InputManager)
local SelectionManager = require(script.Input.SelectionManager)
local InteractionManager = require(script.Input.InteractionManager)
local ConsoleDropUI = require(script.Input.ConsoleDropUI)

-- BindableEvents
local GetStowayFromServer = ReplicatedStorage:WaitForChild("Remotes").AskForStoway :: RemoteFunction

-- CONSTANTS
local HotbarScope = nil
local StorageScope = nil
local Player = Players.LocalPlayer :: Player
local GUISTORAGE = ReplicatedStorage:FindFirstChild("Gui") -- change to your folder name
local GUIFOLDER : Folder

-- Others
local InventoryGui       --= Player.PlayerGui:WaitForChild("StowayGui", 20).Stoway :: ScreenGui
local DraggingGui        --= Player.PlayerGui:WaitForChild("StowayGui").DraggingGui :: ScreenGui
local BackpackFrame      --= InventoryGui.Backpack :: Frame
local HotbarFrame        --= InventoryGui.Hotbar :: Frame
local ScrollingBackpack : ScrollingFrame --= BackpackFrame.ScrollingFrame :: ScrollingFrame
local SearchFilter      : TextBox --= BackpackFrame.SearchFilter :: TextBox
local WeightLabel       : TextLabel --= BackpackFrame.Weight :: TextLabel
local BackpackStuff     : ImageButton -- Mobile Backpack Button

local InputConnections = {}

function SetUpBinds()
    -- Clear old connections
    for _, conn in ipairs(InputConnections) do
        conn:Disconnect()
    end
    table.clear(InputConnections)

    InputManager.Init()

    -- Check if Backpack is globally disabled
    if not Settings.Storage.BackpackEnabled and BackpackFrame then
        BackpackFrame.Visible = false
    end

    -- Backpack
    if InputManager.BackpackAction then
        local bpConn = InputManager.BackpackAction.Activated:Connect(function(active, pressed)
            if pressed then
                if BackpackFrame then BackpackFrame.Visible = not BackpackFrame.Visible end
            end
        end)
        table.insert(InputConnections, bpConn)
    end

    -- Mobile Backpack Button Logic
    if BackpackStuff then
        local function updateBackpackStuffVisibility()
            local isMobile = UserInputService.PreferredInput == Enum.PreferredInput.Touch
            local isEnabled = Settings.Storage.BackpackEnabled

            BackpackStuff.Visible = isMobile and isEnabled

            -- If disabled or not mobile, ensure backpack is closed (optional, but good UX)
            if not (isMobile and isEnabled) and BackpackFrame and BackpackFrame.Visible and UserInputService.PreferredInput == Enum.PreferredInput.Touch then
                -- BackpackFrame.Visible = false -- Maybe don't force close if opened via other means, but safer to leave alone
            end
        end

        updateBackpackStuffVisibility()

        local prefInputConn = UserInputService:GetPropertyChangedSignal("PreferredInput"):Connect(updateBackpackStuffVisibility)
        table.insert(InputConnections, prefInputConn)

        local clickConn = BackpackStuff.Activated:Connect(function()
            if BackpackFrame then
                BackpackFrame.Visible = not BackpackFrame.Visible
            end
        end)
        table.insert(InputConnections, clickConn)
    end

	local currentSettings = Fusion.peek(Store.Settings)
	local maxSlots = currentSettings and currentSettings.MaxHotbarSlots or 10
    local isDynamicHotbar = Settings.Hotbar.Type == "Dynamic"

	for i = 1, maxSlots do
        local action = InputManager.HotbarActions[i]
        if action then
            local conn = action.Activated:Connect(function(active, pressed)
                if pressed then
                    local targetUUID
                    if isDynamicHotbar then
                        targetUUID = Store:GetUUidFromVisibleHotbarIndex(i)
                    else
                        targetUUID = Store:GetUUidFromIndex(i, "Hotbar")
                    end

                    InventoryController.HandleEquipped(targetUUID, true)
                end
            end)
             table.insert(InputConnections, conn)
        end
	end
end

local function GetKeybindDisplay(slotIndex: number): string?
	local bindData = Binds.HotbarSlots[slotIndex]
	if not bindData then return nil end
	return "[" .. bindData.Display .. "]"
end

local function GetSortedIndices(storage: { string }, items: { [string]: any }, sortOrder: string): { number }
	local indices = {}
	for i = 1, #storage do
		table.insert(indices, i)
	end

	if not sortOrder or sortOrder == "None" then
		return indices
	end

	table.sort(indices, function(a, b)
		local itemA = items[storage[a]]
		local itemB = items[storage[b]]
		if not itemA or not itemB then return a < b end

		if sortOrder == "Name" then
			return itemA.Id < itemB.Id
		elseif sortOrder == "Rarity" then
			local priorityA = RarityValues.RaritySortOrder[itemA.Metadata and itemA.Metadata.Rarity] or 99
			local priorityB = RarityValues.RaritySortOrder[itemB.Metadata and itemB.Metadata.Rarity] or 99
			if priorityA == priorityB then return itemA.Id < itemB.Id end
			return priorityA < priorityB
		elseif sortOrder == "ItemType" then
			local typeA = itemA.Metadata and itemA.Metadata.Type or ""
			local typeB = itemB.Metadata and itemB.Metadata.Type or ""
			local priorityA = RarityValues.ItemTypeSortOrder[typeA] or 99
			local priorityB = RarityValues.ItemTypeSortOrder[typeB] or 99
			if priorityA == priorityB then return itemA.Id < itemB.Id end
			return priorityA < priorityB
		end
		return a < b
	end)

	return indices
end

function BuildHooks(HookPresetName: string?)
    HookManager:ClearAll()
    if HookPresetName then
        local hookFolder = GUIFOLDER and GUIFOLDER:FindFirstChild("HookFuncs")
        if hookFolder then
            local module = hookFolder:FindFirstChild(HookPresetName)
            if module and module:IsA("ModuleScript") then
                local preset = require(module)
                HookManager:LoadPreset(preset)
            else
                warn("[Stoway] Hook preset module not found:", HookPresetName)
            end
        else
            warn(`[Stoway] {GUIFOLDER}.HookFuncs folder missing!`)
        end
    end
end

function BuildGui(UIVersion, FolderName, GuiName)
    if not GuiName then return {success = false, reason = `GUI NAME IS BLANK`} end

    GUIFOLDER = GUISTORAGE[UIVersion]:FindFirstChild(FolderName) --and GUISTORAGE[UIVersion]:FindFirstChild(FolderName):Clone()
    if not GUIFOLDER then return {success = false, reason = `No Folder by the name {FolderName}`} end

    InventoryGui = GUIFOLDER:FindFirstChild(GuiName);
    if not InventoryGui then return {success = false, reason = `No GUI BY THAT NAME -> {GuiName}`} end
    InventoryGui = InventoryGui:Clone()

    return xpcall(
        function ()
            DraggingGui = InventoryGui --GUIFOLDER:FindFirstChild("DraggingGui")
            BackpackFrame = InventoryGui.Backpack
            HotbarFrame = InventoryGui.Hotbar
            ScrollingBackpack = BackpackFrame.ScrollingFrame
            SearchFilter = BackpackFrame.SearchFilter
            WeightLabel = BackpackFrame.Weight
            BackpackStuff = InventoryGui:FindFirstChild("BackpackStuff")

            InventoryGui.Parent = Player.PlayerGui

            -- Setup Dynamic Drop Menu Template
            local slotFolder = GUIFOLDER:FindFirstChild("Slot")
            if slotFolder then
                local dropTemplate = slotFolder:FindFirstChild("DropMenu")
                if dropTemplate then
                    ConsoleDropUI.SetTemplate(dropTemplate)
                end
            end

            return {success = true, reason = ""}
        end,

        function (err)
            return {success = false, reason = err}
        end)
end

function BuildHotbar()
	if HotbarScope then
		HotbarScope:doCleanup()
		HotbarScope = nil
	end

    HotbarScope = Store.Scope:deriveScope()
    for i = 1, Fusion.peek(Store.Settings).MaxHotbarSlots do
        SlotFactory.Create(HotbarScope, {
            SlotIndex = i,
            SlotType = "Hotbar",
            Parent = HotbarFrame,
            GuiFOLDER = GUIFOLDER,
            KeybindDisplay = GetKeybindDisplay(i),
            GetKeybindDisplay = GetKeybindDisplay,
            OnDrag = DragHandler.StartDrag, -- Hook up drag handler here for passing scopes
            hookManager = HookManager -- hook manager should refresh or be reloaded per interface refresh
        })
    end
end

function BuildStorage()
    if StorageScope then
		StorageScope:doCleanup()
		StorageScope = nil
	end

    if not Settings.Storage.BackpackEnabled then return end

    StorageScope = Store.Scope:deriveScope()

    -- 1. Create a Search Value
    local SearchText = nil
    if SearchFilter then
        SearchText = StorageScope:Value("")
        SearchFilter:GetPropertyChangedSignal("Text"):Connect(function()
            SearchText:set(SearchFilter.Text:lower())
        end)
    end

	if WeightLabel then
		if Settings.Storage.Limit and Settings.Storage.Limit > 0 then
			StorageScope:Hydrate(WeightLabel) {
				Text = StorageScope:Computed(function(use)
					local weight = use(Store.Weight)
					return weight .. "/" .. use(Store.Settings).Limit
				end),
				Visible = true
			}
		else
			WeightLabel.Visible = false
		end
	end

    -- if you want the storage to be updated and have infinity you have to change it to the below
    -- if WeightLabel then
	-- 	StorageScope:Hydrate(WeightLabel) {
	-- 		Text = StorageScope:Computed(function(use)
	-- 			local weight = use(Store.Weight)
	-- 			local settings = use(Store.Settings)
	-- 			if settings and settings.Limit and settings.Limit > 0 then
	-- 				return weight .. "/" .. settings.Limit
	-- 			else
	-- 				return weight .. "/∞"
	-- 			end
	-- 		end),
	-- 	}
	-- end

    -- 2. Master Sort Calculation (Runs once per update)
    -- This creates a lookup table: {[OriginalIndex] = Rank}
    local SortLookup = nil
    if Settings.Storage.Sorting then
        SortLookup = StorageScope:Computed(function(use)
            local storage = use(Store.Storage)
            local items = use(Store.Items)
            local sortOrder = Settings.Storage.SortOrder or "None"

            local lookup = {}
            local sortedIndices = GetSortedIndices(storage, items, sortOrder)

            for rank, originalIndex in ipairs(sortedIndices) do
                lookup[originalIndex] = rank
            end
            return lookup
        end)
    end

    -- 3. Static Slot Creation
    -- We track raw Storage so slots are only created/deleted on Add/Remove

    StorageScope:ForPairs(Store.Storage, function(_use, scope, storageIndex, uuid)

        local slotFrame = SlotFactory.Create(scope, {
            SlotIndex = storageIndex,
            SlotType = "Storage",
            Parent = ScrollingBackpack,
            GuiFOLDER = GUIFOLDER,
			KeybindDisplay = nil,
            OnDrag = DragHandler.StartDrag,
            hookManager = HookManager
        })

        if SortLookup ~= nil then
        -- 4. Reactive Updates (Filtering and Sorting)
            scope:Hydrate(slotFrame) {
            -- FILTERING
                Visible = scope:Computed(function(use)
                    local search = use(SearchText)
                    if search == "" then return true end

                    local item = use(Store.Items)[uuid]
                    if not item then return false end

                    return item.Id:lower():find(search, 1, true) ~= nil
                end),

            -- SORTING (Lookup is O(1) here, very fast!)
                LayoutOrder = scope:Computed(function(use)
                    local lookup = use(SortLookup)
                    return lookup[storageIndex] or storageIndex
                end)
            }
        end

        return storageIndex, slotFrame
    end)
end

function ClientInventoryService.Reload(config: {UiType: string?, Data: any?}?)
    if not config then return {success = false, reason = "No CONFIG SENT"} end
    local uiType : {FolderName: string, GuiName: string, HookPresetName: string} | nil = config.UiType and Settings.DifferentUIs[config.UiType]
    if not uiType then return {success = false, reason = "Cannot load data without the type of ui to show"} end

    print("[Stoway] Reloading inventory system...")

    -- 1. DESTROY: Unbind all hotkey actions (Handled by SetUpBinds cleanup)
    InputManager.Destroy()
    InteractionManager.Destroy()
    SelectionManager.Destroy()

    -- 2. CLEANUP: Destroy Fusion scopes (UI cleanup)
    if HotbarScope then
        HotbarScope:doCleanup()
        HotbarScope = nil
    end
    if StorageScope then
        StorageScope:doCleanup()
        StorageScope = nil
    end

    -- 3. GUI REFERENCES: Reload UI references (new GUI if provided)
    if InventoryGui then InventoryGui:Destroy(); InventoryGui = nil end
    BuildGui(config.UiType, uiType.FolderName, uiType.GuiName)

    -- 4. HOOKS: Clear old hooks and load new preset (from Name)
    BuildHooks(uiType.HookPresetName)

    -- 5. DATA: Re-initialize store if new data provided
    if config ~= nil and config.Data ~= nil then
        Store.Init(config.Data)
    end

    -- 6. REBUILD: Reinitialize DragHandler with new references
    DragHandler.Destroy()
    DragHandler.Init(DraggingGui, HotbarFrame, BackpackFrame, HookManager, SlotFactory.CloneForGhost)

    InteractionManager.Init(DragHandler, HotbarFrame)
    SelectionManager.Init(HotbarFrame, BackpackFrame, HookManager)

    -- 7. BUILD UI
    BuildHotbar()
    BuildStorage()

    -- 8. REBIND: Set up keybinds again
    SetUpBinds()
    --InventoryController.HandleEquipped(Fusion.peek(Store.EquippedUUID), false)

    return {success = true, reason = "[Stoway] Reload complete!"}
end

local NetworkHandlers = {
    Init = function(payload)
        Store.Init(payload)
    end,

    Correction = function(payload)
        -- Server sent a correction, re-sync state
        Store.Init(payload)
    end,

    Add = function(payload)
        InventoryController.Add(payload)
    end,

    Remove = function(payload)
        -- Server removed an item, sync locally
        InventoryController.Remove(payload.SlotType, payload.Slot, false)
    end,

    Swap = function(payload)
        -- Server confirmed swap (client already applied optimistically)
        -- Could use for rollback if needed
        InventoryController.Swap(payload.From.Type, payload.From.Slot, payload.To.Type, payload.To.Slot, false)
    end,

    Equip = function(payload)
        InventoryController.SetEquipped(payload.UUID, false)
    end,

    UpdateMeta = function(payload)
        -- Update item metadata
        local items = Fusion.peek(Store.Items)
        local item = items[payload.UUID]
        if item then
            local newItems = table.clone(items)
            local newItem = table.clone(item)
            newItem.Metadata = newItem.Metadata or {}
            for k, v in pairs(payload.Updates or {}) do
                if k == "Amount" then
                    newItem.Amount = v
                else
                    newItem.Metadata[k] = v
                end
            end
            newItems[payload.UUID] = newItem
            Store.Items:set(newItems)
            Store.Weight:set(payload.NewWeight)
            local slottype, index = Store:FindSlotByUUID(payload.UUID)
            HookManager:Fire("OnItemAdd", slottype, index)
        end
    end,

    UpdateSettings = function(payload)
        Store.Settings:set(payload)
    end,

    Reload = function(payload)
        -- Server triggered reload (e.g., player became mod)
        -- payload.GuiName: Name of new GUI to load from ReplicatedStorage
        -- payload.HookPresetName: Name of ModuleScript in ReplicatedStorage.HookFuncs
        -- payload.Data: Optional new inventory data
        ClientInventoryService.Reload(payload)
    end,
}

function LoadInitialData()
    local ReturnData = GetStowayFromServer:InvokeServer()
    local uiType : {FolderName: string, GuiName: string, HookPresetName: string} | nil = ReturnData.data.Settings.UiType and Settings.DifferentUIs[ReturnData.data.Settings.UiType]

    if not ReturnData.success then return warn(ReturnData.reason) end
    if not uiType then return warn("Cannot load data without the type of ui to show") end

    Store.Init(ReturnData.data)

    BuildGui(ReturnData.data.Settings.UiType, uiType.FolderName, uiType.GuiName)
    BuildHooks(uiType.HookPresetName)

    -- Wire up NetworkHandler with our handlers (like server pattern)

    return true
end

function ClientInventoryService.Init()
    -- 1. Load Data and Build base UI
    LoadInitialData()
    InventoryController.Init(HookManager)

    -- 2. Build rest of the UI
    StarterGui:SetCoreGuiEnabled(Enum.CoreGuiType.Backpack, false)
    StarterGui:SetCoreGuiEnabled(Enum.CoreGuiType.PlayerList, false)

    DragHandler.Init(DraggingGui, HotbarFrame, BackpackFrame, HookManager, SlotFactory.CloneForGhost)

    InteractionManager.Init(DragHandler, HotbarFrame)
    SelectionManager.Init(HotbarFrame, BackpackFrame, HookManager)

    BuildHotbar()
    BuildStorage()
    SetUpBinds()
    NetworkHandler.Init(NetworkHandlers)
end

ClientInventoryService.Hooks = HookManager

return ClientInventoryService


-- src/server/StowayServerV1.2/Core/InventoryState.luau
-- Pure data container for player inventory state
-- Includes per-player settings (defaults from Config, modifiable at runtime)

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local InventoryState = {}
InventoryState.__index = InventoryState

local Settings = require(ReplicatedStorage.Shared.Settings)

--// Types
export type ItemInstance = {
	UUID: string,
	Id: string,
	Amount: number,
	Metadata: { [string]: any }?
}

export type PlayerSettings = {
	Limit: number,             -- Weight limit (0 = infinite)
	CanStack: boolean,         -- Allow stacking
	MaxStackSize: number,      -- Max items per stack
	MaxHotbarSlots: number,    -- Number of hotbar slots
	Droppable: boolean,        -- Allow dropping
	UiType: string, 		   -- Interface version to load (default, admin, mod, developer, content, spec, etc)
}

export type InventoryStateType = {
	Player: Player,
	Items: { [string]: ItemInstance },      -- UUID -> Item
	ItemsByID: { [string]: { string } },    -- ItemId -> Array of UUIDs (for stacking lookup)
	Hotbar: { [number]: string? },          -- Slot -> UUID (Static: nil holes allowed)
	Storage: { string },                     -- Array of UUIDs (Dynamic: packed, no holes)
	EquippedItemUUID: string?,              -- UUID of currently equipped item
	Weight: number,                          -- Total item count (sum of all stack amounts)
	Settings: PlayerSettings,               -- Per-player settings (modifiable!)
}

--// Constructor
function InventoryState.new(player: Player): InventoryStateType
	local self = setmetatable({}, InventoryState)
	self.Player = player
	self.Items = {}
	self.ItemsByID = {}
	self.Hotbar = {}
	self.Storage = {}
	self.EquippedItemUUID = nil
	self.Weight = 0
	self.died = false

	-- Per-player settings (defaults from Config)
	self.Settings = {
		Droppable = Settings.Gameplay.Droppable,
		Limit = Settings.Storage.Limit,
		CanStack = Settings.Storage.CanStack,
		MaxStackSize = Settings.Storage.MaxStackSize,
		MaxHotbarSlots = Settings.Hotbar.MaxSlots,
		UiType = "Default"
	}

	return self
end

--// Getters (No logic, just data access)
function InventoryState:GetItem(uuid: string): ItemInstance?
	return self.Items[uuid]
end

function InventoryState:GetHotbarUUID(slot: number): string?
	return self.Hotbar[slot]
end

function InventoryState:GetStorageUUID(index: number): string?
	return self.Storage[index]
end

function InventoryState:IsEquipped(uuid: string): boolean
	return self.EquippedItemUUID == uuid
end

return InventoryState


-- src/server/StowayServerV1.2/Core/LimitChecker.luau
-- Pre-operation validation for inventory weight/limit (Amount-based)
-- Uses state.Settings (per-player) instead of global Settings
-- Limit = 0 means INFINITE (no weight checking)

local LimitChecker = {}

local SlotManager = require(script.Parent.SlotManager)

--// Check if weight system is enabled (Limit > 0)
function LimitChecker.IsEnabled(state: any): boolean
	return (state.Settings.Limit or 0) > 0
end

--// Get total weight (sum of all item amounts)
function LimitChecker.GetCurrentWeight(state: any): number
	return state.Weight or 0
end

--// Get remaining capacity (returns math.huge if Limit = 0)
function LimitChecker.GetRemainingCapacity(state: any): number
	if not LimitChecker.IsEnabled(state) then
		return math.huge -- Infinite
	end
	return math.max(0, state.Settings.Limit - LimitChecker.GetCurrentWeight(state))
end

--// Check if we can add amount to inventory
function LimitChecker.CanAddAmount(state: any, amount: number): boolean
	if not LimitChecker.IsEnabled(state) then
		return true -- No limit, always allowed
	end
	return LimitChecker.GetRemainingCapacity(state) >= amount
end

--// Check if inventory is completely full
function LimitChecker.IsFull(state: any): boolean
	if not LimitChecker.IsEnabled(state) then
		return false -- Never full if infinite
	end
	return LimitChecker.GetRemainingCapacity(state) == 0
end

--// Check if hotbar has space (for slot placement)
function LimitChecker.HasHotbarSpace(state: any): boolean
	return SlotManager.FindEmptyHotbarSlot(state) ~= nil
end

--// Add weight (call after adding items) - always track for stats
function LimitChecker.AddWeight(state: any, amount: number)
	state.Weight = (state.Weight or 0) + amount
end

--// Remove weight (call after removing items) - always track for stats
function LimitChecker.RemoveWeight(state: any, amount: number)
	state.Weight = math.max(0, (state.Weight or 0) - amount)
end

return LimitChecker


-- src/server/StowayServerV1.2/Core/SlotManager.luau
-- Single source of truth for slot-level operations (Static Hotbar / Dynamic Storage)
-- Uses state.Settings (per-player) instead of global Settings

local SlotManager = {}
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Settings = require(ReplicatedStorage.Shared.Settings)

--// ═══════════════════════════════════════════════════════════════════════════
--// HOTBAR (Static Logic)
--// ═══════════════════════════════════════════════════════════════════════════

function SlotManager.SetHotbarSlot(state: any, slot: number, uuid: string)
	state.Hotbar[slot] = uuid
end

function SlotManager.ClearHotbarSlot(state: any, slot: number)

	state.Hotbar[slot] = nil -- Hole preserved (Static)
end

function SlotManager.FindEmptyHotbarSlot(state: any): number?
	local maxSlots = state.Settings.MaxHotbarSlots
	for i = 1, maxSlots do
		if not state.Hotbar[i] then
			return i
		end
	end
	return nil
end

function SlotManager.GetHotbarItemCount(state: any): number
	local count = 0
	local maxSlots = state.Settings.MaxHotbarSlots
	for i = 1, maxSlots do
		if state.Hotbar[i] then
			count += 1
		end
	end
	return count
end

function SlotManager.IsDynamicHotbarEnabled(): boolean
	return Settings.Hotbar.Type == "Dynamic"
end

-- Dynamic hotbar keeps server slot data packed (no holes) while preserving item order.
function SlotManager.CompactHotbar(state: any): boolean
	if not SlotManager.IsDynamicHotbarEnabled() then
		return false
	end

	local maxSlots = state.Settings.MaxHotbarSlots
	local writeIndex = 1
	local changed = false
	local packedHotbar = {}

	for readIndex = 1, maxSlots do
		local uuid = state.Hotbar[readIndex]
		if uuid then
			if readIndex ~= writeIndex then
				changed = true
			end
			packedHotbar[writeIndex] = uuid
			writeIndex += 1
		end
	end

	if changed then
		state.Hotbar = packedHotbar
	end

	return changed
end

--// ═══════════════════════════════════════════════════════════════════════════
--// STORAGE (Dynamic Logic)
--// ═══════════════════════════════════════════════════════════════════════════

function SlotManager.AppendStorage(state: any, uuid: string)
	table.insert(state.Storage, uuid)
end

function SlotManager.RemoveStorageAt(state: any, index: number)
	table.remove(state.Storage, index) -- Shifts items up (Dynamic)
end

function SlotManager.SwapStorageIndices(state: any, i: number, j: number)
	state.Storage[i], state.Storage[j] = state.Storage[j], state.Storage[i]
end

function SlotManager.GetStorageItemCount(state: any): number
	return #state.Storage
end

--// ═══════════════════════════════════════════════════════════════════════════
--// UTILITY
--// ═══════════════════════════════════════════════════════════════════════════

function SlotManager.FindSlotByUUID(state: any, uuid: string): (string?, number?)
	-- Check Hotbar
	for i, v in state.Hotbar do
		if v == uuid then
			return "Hotbar", i
		end
	end

	-- Check Storage
	for i, v in state.Storage do
		if v == uuid then
			return "Storage", i
		end
	end

	return nil, nil
end

function SlotManager.GetUUIDFromSlot(state: any, slotType: string, slot: number): string?
	if slotType == "Hotbar" then
		return state.Hotbar[slot]
	elseif slotType == "Storage" then
		return state.Storage[slot]
	end
	return nil
end

return SlotManager


-- src/server/StowayServerV1.2/Debug/ChatCommands.luau
-- Debug chat commands for testing the inventory system

local ChatCommands = {}

function ChatCommands.Register(player: Player, InventoryService: any)
	player.Chatted:Connect(function(msg)
		local args = string.split(msg, " ")
		local cmd = string.lower(args[1])
		local state = InventoryService.GetState(player)

		if not state then return end

		--// ═══════════════════════════════════════════════════════════════════
		--// INVENTORY DISPLAY
		--// ═══════════════════════════════════════════════════════════════════

		if cmd == "/inv" then
			print("═══════════════════════════════════════════════════════════════")
			print("INVENTORY for " .. player.Name)
			print("───────────────────────────────────────────────────────────────")
			print("Weight:", state.Weight, "/", state.Settings.Limit, "(0 = Infinite)")
			print("CanStack:", state.Settings.CanStack, "| MaxStack:", state.Settings.MaxStackSize)
			print("Equipped UUID:", state.EquippedItemUUID or "None")
			print("───────────────────────────────────────────────────────────────")
			print("HOTBAR (max " .. state.Settings.MaxHotbarSlots .. "):")
			for i = 1, state.Settings.MaxHotbarSlots do
				local uuid = state.Hotbar[i]
				if uuid then
					local item = state.Items[uuid]
					if item then
						print("  [" .. i .. "]", item.Id, "x" .. item.Amount, "(UUID:" .. uuid .. ")")
					else
						print("  [" .. i .. "] (ORPHAN UUID:" .. uuid .. ")")
					end
				else
					print("  [" .. i .. "] (empty)")
				end
			end
			print("STORAGE:", "#" .. #state.Storage)
			for i, uuid in ipairs(state.Storage) do
				local item = state.Items[uuid]
				if item then
					print("  [" .. i .. "]", item.Id, "x" .. item.Amount, "(UUID:" .. uuid .. ")")
				else
					print("  [" .. i .. "] (ORPHAN UUID:" .. uuid .. ")")
				end
			end
			print("═══════════════════════════════════════════════════════════════")

		--// ═══════════════════════════════════════════════════════════════════
		--// ADD ITEM
		--// ═══════════════════════════════════════════════════════════════════

		elseif cmd == "/add" then
			-- /add [itemId] [amount?]
			local itemId = args[2]
			local amount = tonumber(args[3])
			if itemId then
				local result = InventoryService.AddItem(player, itemId, amount, nil, true)
				print("[Add]", itemId)
				print("  Success:", result.success)
				print("  Added:", result.addedAmount or "?")
				print("  Overflow:", result.overflow or 0)
				print("  Reason:", result.reason or "OK")
			else
				print("Usage: /add [itemId] [amount?]")
			end

		elseif cmd == "/addback" then
			-- /addback [itemId] [amount?] - Add directly to backpack (bypasses hotbar)
			local itemId = args[2]
			local amount = tonumber(args[3])
			if itemId then
				local result = InventoryService.AddToBackpack(player, itemId, amount, nil, true)
				print("[AddToBackpack]", itemId)
				print("  Success:", result.success)
				print("  Added:", result.addedAmount or "?")
				print("  Overflow:", result.overflow or 0)
				print("  Reason:", result.reason or "OK")
			else
				print("Usage: /addback [itemId] [amount?]")
			end

		elseif cmd == "/stack" then
			-- /stack [fromType] [fromSlot] [toType] [toSlot]
			local fromType = args[2]
			local fromSlot = tonumber(args[3])
			local toType = args[4]
			local toSlot = tonumber(args[5])
			if fromType and fromSlot and toType and toSlot then
				local result = InventoryService.StackTwoSlots(player, fromType, fromSlot, toType, toSlot)
				print("[Stack]", fromType, fromSlot, "->", toType, toSlot)
				print("  Success:", result.success)
				print("  Transferred:", result.transferred or 0)
				print("  From Remaining:", result.fromRemaining or 0)
				print("  To Amount:", result.toAmount or 0)
				print("  Reason:", result.reason or "OK")
			else
				print("Usage: /stack [Hotbar/Storage] [slot] [Hotbar/Storage] [slot]")
			end

		--// ═══════════════════════════════════════════════════════════════════
		--// REMOVE ITEM
		--// ═══════════════════════════════════════════════════════════════════

		elseif cmd == "/remove" then
			-- /remove [uuid] [amount?]
			local uuid = args[2]
			local amount = tonumber(args[3])
			if uuid then
				local result = InventoryService.RemoveItem(player, uuid, amount, true)
				print("[Remove]", uuid)
				print("  Success:", result.success)
				print("  Remaining:", result.remaining or 0)
				print("  Reason:", result.reason or "OK")
			else
				print("Usage: /remove [uuid] [amount?]")
			end

		--// ═══════════════════════════════════════════════════════════════════
		--// SWAP
		--// ═══════════════════════════════════════════════════════════════════

		elseif cmd == "/swap" then
			-- /swap [fromType] [fromSlot] [toType] [toSlot]
            -- Normalize types to TitleCase (Hotbar, Storage)
            local function toTitleCase(str)
                return str:sub(1,1):upper() .. str:sub(2):lower()
            end

			local fromType = args[2] and toTitleCase(args[2])
			local fromSlot = tonumber(args[3])
			local toType = args[4] and toTitleCase(args[4])
			local toSlot = tonumber(args[5])
			if fromType and fromSlot and toType and toSlot then
				local result = InventoryService.SwapSlots(player, fromType, fromSlot, toType, toSlot, true)
				print("[Swap]", fromType, fromSlot, "->", toType, toSlot)
				print("  Success:", result.success)
				print("  Reason:", result.reason or "OK")
			else
				print("Usage: /swap [Hotbar/Storage] [slot] [Hotbar/Storage] [slot]")
			end

		--// ═══════════════════════════════════════════════════════════════════
		--// EQUIP / UNEQUIP
		--// ═══════════════════════════════════════════════════════════════════

		elseif cmd == "/equip" then
			-- /equip [slot] or /equip [type] [slot]
			local slotType = "Hotbar"
			local slot
			if tonumber(args[2]) then
				slot = tonumber(args[2])
			else
				slotType = args[2] or "Hotbar"
				slot = tonumber(args[3])
			end
			if slot then
				local result = InventoryService.EquipSlot(player, slotType, slot, true)
				print("[Equip]", slotType, slot)
				print("  Success:", result.success)
				print("  Equipped UUID:", result.currentUUID or "None")
			else
				print("Usage: /equip [slot] or /equip [type] [slot]")
			end

		elseif cmd == "/unequip" then
			local result = InventoryService.UnequipSlot(player, true)
			print("[Unequip]")
			print("  Success:", result.success)

		--// ═══════════════════════════════════════════════════════════════════
		--// DROP
		--// ═══════════════════════════════════════════════════════════════════

		elseif cmd == "/drop" then
			-- /drop [type] [slot] [amount?]
			local slotType = args[2]
			local slot = tonumber(args[3])
			local amount = tonumber(args[4])
			if slotType and slot then
				local result = InventoryService.DropItem(player, slotType, slot, amount, true)
				print("[Drop]", slotType, slot, amount or "(All)")
				print("  Success:", result.success)
				print("  Remaining:", result.remaining or 0)
			else
				print("Usage: /drop [Hotbar/Storage] [slot] [amount?]")
			end

		--// ═══════════════════════════════════════════════════════════════════
		--// SETTINGS TOGGLE (PER-PLAYER!)
		--// ═══════════════════════════════════════════════════════════════════

		elseif cmd == "/toggle_stack" then
			state.Settings.CanStack = not state.Settings.CanStack
			print("[Settings] CanStack:", state.Settings.CanStack)
            InventoryService.SyncSettings(player)

		elseif cmd == "/toggle_limit" then
			if state.Settings.Limit == 0 then
				state.Settings.Limit = 15
			else
				state.Settings.Limit = 0
			end
			print("[Settings] Limit:", state.Settings.Limit, "(0 = Infinite)")
            InventoryService.SyncSettings(player)

		elseif cmd == "/set_limit" then
			local newLimit = tonumber(args[2]) or 15
			state.Settings.Limit = newLimit
			print("[Settings] Limit set to:", newLimit)
            InventoryService.SyncSettings(player)

		elseif cmd == "/set_max_stack" then
			local newMax = tonumber(args[2]) or 5
			state.Settings.MaxStackSize = newMax
			print("[Settings] MaxStackSize set to:", newMax)
            InventoryService.SyncSettings(player)

		elseif cmd == "/set_hotbar_slots" then
			local newSlots = tonumber(args[2]) or 9
			state.Settings.MaxHotbarSlots = newSlots
			print("[Settings] MaxHotbarSlots set to:", newSlots)
            InventoryService.SyncSettings(player)

		--// ═══════════════════════════════════════════════════════════════════
		--// CLEAR INVENTORY
		--// ═══════════════════════════════════════════════════════════════════

		elseif cmd == "/clear" then
			local uuidsToRemove = {}
			for uuid, _ in pairs(state.Items) do
				table.insert(uuidsToRemove, uuid)
			end
			for _, uuid in pairs(uuidsToRemove) do
				InventoryService.RemoveItem(player, uuid, nil, true)
			end
			print("[Clear] Inventory cleared")

		--// ═══════════════════════════════════════════════════════════════════
		--// HELP
		--// ═══════════════════════════════════════════════════════════════════

		elseif cmd == "/help" then
			print("═══════════════════════════════════════════════════════════════")
			print("INVENTORY DEBUG COMMANDS")
			print("───────────────────────────────────────────────────────────────")
			print("/inv              - Display inventory state")
			print("/add [id] [amt?]  - Add item (hotbar first)")
			print("/addback [id] [amt?] - Add directly to backpack")
			print("/remove [uuid] [amt?] - Remove item by UUID")
			print("/swap [t1] [s1] [t2] [s2] - Swap slots")
			print("/stack [t1] [s1] [t2] [s2] - Stack items together")
			print("/equip [slot]     - Equip from hotbar")
			print("/unequip          - Unequip current")
			print("/drop [type] [slot] [amount?] - Drop item")
			print("/clear            - Clear inventory")
			print("───────────────────────────────────────────────────────────────")
			print("/toggle_stack       - Toggle stacking on/off (per-player)")
			print("/toggle_limit       - Toggle limit (0=infinite / 15)")
			print("/set_limit [n]      - Set weight limit")
			print("/set_max_stack [n]  - Set max stack size")
			print("/set_hotbar_slots [n] - Set hotbar slot count")
			print("/setui [uitype]     - Change UI type and reload client")
			print("═══════════════════════════════════════════════════════════════")

		--// ═══════════════════════════════════════════════════════════════════
		--// SET UI TYPE (RELOAD CLIENT)
		--// ═══════════════════════════════════════════════════════════════════

		elseif cmd == "/setui" then
			-- /setui [uitype]
			local uiType = args[2]
			if uiType then
				local result = InventoryService.ReloadClient(player, {
					configSettingName = uiType,
					RefreshData = true
				})

				if result.success then
					print("[SetUI] Reloading client with UI type:", uiType)
				else
					print("[SetUI] ERROR:", result.reason)
				end
			else
				print("Usage: /setui [uitype]")
				print("  Example: /setui Default")
			end
		end
	end)

	print("[Debug] Chat commands registered for", player.Name, "- Type /help")
end

return ChatCommands


local ReplicatedStorage = game:GetService("ReplicatedStorage")
local AddOperation = {}
local Settings = require(ReplicatedStorage.Shared.Settings)
local SlotManager = require(script.Parent.Parent.Core.SlotManager)
local LimitChecker = require(script.Parent.Parent.Core.LimitChecker)
local StackChecker = require(script.Parent.Parent.Utils.StackChecker)
local MetadataParser = require(script.Parent.Parent.Utils.MetadataParser)
local UUID = require(script.Parent.Parent.Utils.UUID)
local DropOperation = require(script.Parent.DropOperation)
local EquipOperation = require(script.Parent.EquipOperation)

export type AddResult = {
	success: boolean,
	reason: string?,
	addedItems: { { uuid: string, slotType: string, slot: number } }?,
	updatedItems: { [string]: number }?, -- UUID -> New Amount
	addedAmount: number?,   -- How many were actually added
	overflow: number?,       -- How many could NOT be added (for dropping/ignoring)
	equippedUUID: string?   -- UUID of the item that was automatically equipped
}

local function NormalizeMetadataOverrides(metadataOverrides: any?): (any?, number?)
	if metadataOverrides == nil then
		return nil, nil
	end

	if type(metadataOverrides) ~= "table" then
		return nil, nil
	end

	local normalized = table.clone(metadataOverrides)
	local amountFromMeta = normalized.Amount
	if amountFromMeta == nil then
		amountFromMeta = normalized.StackAmount
	end

	if type(amountFromMeta) == "string" then
		amountFromMeta = tonumber(amountFromMeta)
	end

	if type(amountFromMeta) ~= "number" then
		amountFromMeta = nil
	end

	normalized.Amount = nil
	normalized.StackAmount = nil

	return normalized, amountFromMeta
end

--// Create a new item instance
local function CreateItem(state: any, itemId: string, amount: number, metadata: any?): any
	local uuid = UUID.Generate()
	local newAmount = math.min(amount, state.Settings.MaxStackSize)

	local item = {
		UUID = uuid,
		Id = itemId,
		Amount = newAmount,
		Metadata = metadata or {}
	}

	-- Add to Items map
	state.Items[uuid] = item

	-- Update ItemsByID lookup
	if not state.ItemsByID[itemId] then
		state.ItemsByID[itemId] = {}
	end
	table.insert(state.ItemsByID[itemId], uuid)

	-- Update weight
	LimitChecker.AddWeight(state, newAmount)

	return item
end

--// Main Execute function
function AddOperation.Execute(state: any, itemId: string, amount: number?, metadataOverrides: any?): AddResult
	local normalizedOverrides, metadataAmount = NormalizeMetadataOverrides(metadataOverrides)

	-- Prepare metadata (parse from tool + merge overrides)
	-- MetadataParser returns (metadata, defaultAmount) - Amount is excluded from metadata
	local baseMeta, defaultAmount = MetadataParser.FromItemId(itemId)
	local finalMetadata = MetadataParser.Merge(baseMeta, normalizedOverrides)

	-- Use explicit amount first, then metadata-provided amount, then tool default, then 1
	local finalAmount = amount or metadataAmount or defaultAmount or 1
	local equippedUUID = nil

	-- Validate amount
	if finalAmount <= 0 then
		return { success = false, reason = "INVALID_AMOUNT" }
	end

	local addedItems = {}
	local originalAmount = finalAmount

	-- 1. Cap amount to remaining capacity (partial fill)
	local _amount: number = finalAmount
	if LimitChecker.IsEnabled(state) then
		local remaining = LimitChecker.GetRemainingCapacity(state)
		if remaining <= 0 then
			-- Completely full, nothing can be added
			return { success = false, reason = "FULL", addedAmount = 0, overflow = finalAmount }
		end
		-- Cap to what we can fit
		_amount = math.min(_amount, remaining)
	end

	local amountToAdd = _amount -- Track how much we'll try to add after capping

	-- 2. Try stacking first (StackChecker updates weight internally)
	local updatedItems = {}
	if state.Settings.CanStack then
		local stacked, updates = StackChecker.TryStack(state, itemId, _amount, finalMetadata)
		_amount -= stacked
		updatedItems = updates or {}

		if _amount <= 0 then
			local overflow = originalAmount - amountToAdd
			return { success = true, addedItems = addedItems, updatedItems = updatedItems, addedAmount = amountToAdd, overflow = overflow }
		end
	end

	-- 3. Remaining amount after stacking - create new slots

	-- Check for Backpack Disabled Logic
	if Settings.Storage.BackpackEnabled == false then
		-- Try Hotbar first
		while _amount > 0 do
			local slot = SlotManager.FindEmptyHotbarSlot(state)
			if slot then
				local item = CreateItem(state, itemId, _amount, finalMetadata)
				SlotManager.SetHotbarSlot(state, slot, item.UUID)
				table.insert(addedItems, { uuid = item.UUID, slotType = "Hotbar", slot = slot })
				_amount -= item.Amount
			else
				-- Hotbar is FULL.
				-- Logic: Drop currently equipped item (or slot 1), then place new item there.

				-- 1. Determine Target Slot
				local targetSlot = 1
				equippedUUID = (state.EquippedItemUUID ~= nil)

				if equippedUUID then
					local sType, sIndex = SlotManager.FindSlotByUUID(state, state.EquippedItemUUID)
					if sType == "Hotbar" then
						targetSlot = sIndex
					end
				end

				-- 2. Drop existing item in target slot
				local existingUUID = state.Hotbar[targetSlot]
				if existingUUID then
					-- We force drop. DropOperation handles unequip + remove + spawn.
					-- Note: DropOperation might check Droppable settings.
					-- If dropping is disabled but we are forcing a swap, we might need to override or just Remove.
					-- Requirement says "drop that to the world".

					DropOperation.Execute(state, state.Player, "Hotbar", targetSlot, nil, {
						SkipHotbarCompaction = true
					})
				end

				local item = CreateItem(state, itemId, _amount, finalMetadata)
				SlotManager.SetHotbarSlot(state, targetSlot, item.UUID)
				table.insert(addedItems, { uuid = item.UUID, slotType = "Hotbar", slot = targetSlot })

				-- 4. Re-equip new item if previous was equipped
				if equippedUUID then
					EquipOperation.Equip(state, state.Player, item.UUID)
					equippedUUID = item.UUID
				end

				_amount -= item.Amount

				-- Since we are forcing a swap for the remainder, we break loop after one swap?
				-- Usually you pick up one stack/item.
				-- If _amount > maxStack, we might loop.
				-- But for "swap" logic, usually we handle one slot.
				break
			end
		end

	else
		-- Standard Logic (Backpack Enabled)

		-- 3. Try Hotbar first (Static)
		while _amount > 0 do
			local slot = SlotManager.FindEmptyHotbarSlot(state)
			if slot then
				local item = CreateItem(state, itemId, _amount, finalMetadata)
				SlotManager.SetHotbarSlot(state, slot, item.UUID)
				table.insert(addedItems, { uuid = item.UUID, slotType = "Hotbar", slot = slot })
				_amount -= item.Amount
			else
				break
			end
		end

		-- 4. Try Storage (Dynamic Append)
		while _amount > 0 do
			-- If limit is infinite OR we have capacity, allow adding
			if not LimitChecker.IsEnabled(state) or LimitChecker.GetRemainingCapacity(state) > 0 then
				local item = CreateItem(state, itemId, _amount, finalMetadata)
				SlotManager.AppendStorage(state, item.UUID)
				local storageIndex = #state.Storage
				table.insert(addedItems, { uuid = item.UUID, slotType = "Storage", slot = storageIndex })
				_amount -= item.Amount
			else
				break -- No more room
			end
		end
	end

	-- Calculate results
	local addedAmount = amountToAdd - _amount
	local overflow = originalAmount - addedAmount

	return {
		success = addedAmount > 0,
		addedItems = addedItems,
		updatedItems = updatedItems,
		addedAmount = addedAmount,
		overflow = overflow,
		reason = overflow > 0 and "PARTIAL" or nil,
		equippedUUID = equippedUUID
	}
end

return AddOperation


-- src/server/StowayServerV1.2/Operations/DropOperation.luau
-- Handles Drop logic: If equipped → Unequip → Remove from inventory → Spawn in world

local DropOperation = {}

local SlotManager = require(script.Parent.Parent.Core.SlotManager)
local RemoveOperation = require(script.Parent.RemoveOperation)
local EquipOperation = require(script.Parent.EquipOperation)
local ItemSpawner = require(script.Parent.Parent.World.ItemSpawner)

export type DropResult = {
	success: boolean,
	reason: string?,
	droppedItem: any?,
	remaining: number?,
	UUID: string?,
	hotbarCompacted: boolean?
}

--// Main Execute function
function DropOperation.Execute(state: any, player: Player, slotType: string, slot: number, amount: number?, options: any?): DropResult
	-- Get item
	local uuid = SlotManager.GetUUIDFromSlot(state, slotType, slot)
	if not uuid then
		return { success = false, reason = "SLOT_EMPTY" }
	end

	local item = state.Items[uuid]
	if not item then
		return { success = false, reason = "ITEM_NOT_FOUND" }
	end

	if not state.Settings.Droppable then
		return { success = false, reason = "INVENTORY_NOT_Droppable" }
	end

	if not item.Metadata.Droppable then
		return { success = false, reason = "ITEM_NOT_Droppable" }
	end

	-- Cache item data before removal
	local itemId = item.Id
	local itemAmount = item.Amount
	local itemMetadata = item.Metadata
	local dropAmount = amount or itemAmount

	if dropAmount > itemAmount then dropAmount = itemAmount end
	if dropAmount <= 0 then return {success = false, reason = "INVALID_AMOUNT"} end

	-- If equipped, unequip first (per spec: equipped items go through unequip flow)
	-- Only unequip if we are dropping the WHOLE item (or at least, the amount that exists)
	if state.EquippedItemUUID == uuid and (itemAmount - dropAmount) <= 0 then
		EquipOperation.Unequip(state, player)
	end

	-- Remove from inventory
	local removeResult = RemoveOperation.Execute(state, uuid, dropAmount, options)
	if not removeResult.success then
		return { success = false, reason = removeResult.reason }
	end

	-- Spawn in world
	ItemSpawner.DropTool(player, itemId, dropAmount, itemMetadata)

	return {
		success = true,
		droppedItem = {
			Id = itemId,
			Amount = dropAmount,
			Metadata = itemMetadata
		},
		remaining = removeResult.remaining,
		UUID = uuid,
		hotbarCompacted = removeResult.hotbarCompacted
	}
end

return DropOperation


-- src/server/StowayServerV1.2/Operations/RemoveOperation.luau
-- Handles full Remove logic flow: Validate → Reduce/Destroy → Shift if Storage

local RemoveOperation = {}

local SlotManager = require(script.Parent.Parent.Core.SlotManager)
local LimitChecker = require(script.Parent.Parent.Core.LimitChecker)

export type RemoveResult = {
	success: boolean,
	reason: string?,
	slotType: string?,
	slot: number?,
	remaining: number?,
	hotbarCompacted: boolean?
}

export type RemoveOptions = {
	SkipHotbarCompaction: boolean?
}

--// Destroy item completely from inventory
local function DestroyItem(state: any, uuid: string, itemId: string, itemAmount: number, options: RemoveOptions?): RemoveResult
	-- Find which slot contains this item
	local slotType, slot = SlotManager.FindSlotByUUID(state, uuid)

	-- Unequip if currently equipped
	if state.EquippedItemUUID == uuid then
		state.EquippedItemUUID = nil
	end

	-- Remove weight
	LimitChecker.RemoveWeight(state, itemAmount)

	-- Remove from Items map
	state.Items[uuid] = nil

	-- Remove from ItemsByID lookup
	local uuids = state.ItemsByID[itemId]
	if uuids then
		for i, id in ipairs(uuids) do
			if id == uuid then
				table.remove(uuids, i)
				break
			end
		end
		if #uuids == 0 then
			state.ItemsByID[itemId] = nil
		end
	end

	-- Remove from slot
	local hotbarCompacted = false
	if slotType == "Hotbar" and slot then
		SlotManager.ClearHotbarSlot(state, slot) -- Preserves hole (Static)
		if not (options and options.SkipHotbarCompaction) then
			hotbarCompacted = SlotManager.CompactHotbar(state)
		end
	elseif slotType == "Storage" and slot then
		SlotManager.RemoveStorageAt(state, slot) -- Shifts items (Dynamic)
	end

	return { success = true, slotType = slotType, slot = slot, remaining = 0, hotbarCompacted = hotbarCompacted }
end

--// Main Execute function
function RemoveOperation.Execute(state: any, uuid: string, amount: number?, options: RemoveOptions?): RemoveResult
	local item = state.Items[uuid]
	if not item then
		return { success = false, reason = "ITEM_NOT_FOUND" }
	end

	-- Default: remove all if no amount specified
	local _amount = amount or item.Amount

	-- Validate amount
	if _amount <= 0 then
		return { success = false, reason = "INVALID_AMOUNT" }
	end

	-- Cap to item amount (per spec: if more than stack, remove all)
	if _amount > item.Amount then
		_amount = item.Amount
	end

	-- Reduce or Destroy
	if item.Amount > _amount then
		item.Amount -= _amount
		LimitChecker.RemoveWeight(state, _amount :: number)
		local slotType, slot = SlotManager.FindSlotByUUID(state, uuid)
		return { success = true, slotType = slotType, slot = slot, remaining = item.Amount, hotbarCompacted = false }
	else
		-- Full removal
		return DestroyItem(state, uuid, item.Id, item.Amount, options)
	end
end

--// Remove by slot (convenience wrapper)
function RemoveOperation.ExecuteBySlot(state: any, slotType: string, slot: number, amount: number?, options: RemoveOptions?): RemoveResult
	local uuid = SlotManager.GetUUIDFromSlot(state, slotType, slot)
	if not uuid then
		return { success = false, reason = "SLOT_EMPTY" }
	end
	return RemoveOperation.Execute(state, uuid, amount, options)
end

return RemoveOperation


local ReplicatedStorage = game:GetService("ReplicatedStorage")
-- src/server/StowayServerV1.2/Operations/SwapOperation.luau
-- Handles full Swap logic flow: Validate → Execute (preserve equipped) → Return delta
-- Uses state.Settings (per-player) for bounds checking

local SwapOperation = {}

local Settings = require(ReplicatedStorage.Shared.Settings)
local SlotManager = require(script.Parent.Parent.Core.SlotManager)

export type SlotRef = {
	type: "Hotbar" | "Storage",
	slot: number
}

export type SwapResult = {
	success: boolean,
	reason: string?,
	hotbarCompacted: boolean?
}

--// Validate swap request
local function ValidateSwap(state: any, from: SlotRef, to: SlotRef): (boolean, string?)
	local maxHotbar = state.Settings.MaxHotbarSlots

	-- Check if backpack is enabled
	if not Settings.Storage.BackpackEnabled then
		if from.type == "Storage" or to.type == "Storage" then
			return false, "BACKPACK_DISABLED"
		end
	end

	-- Validate Hotbar bounds
	if from.type == "Hotbar" then
		if from.slot < 1 or from.slot > maxHotbar then
			return false, "INVALID_FROM_SLOT"
		end
	end

	if to.type == "Hotbar" then
		if to.slot < 1 or to.slot > maxHotbar then
			return false, "INVALID_TO_SLOT"
		end
	end

	-- Storage source MUST exist (can't swap from nothing)
	if from.type == "Storage" then
		if not state.Storage[from.slot] then
			return false, "FROM_SLOT_EMPTY"
		end
	end

	return true, nil
end

--// Main Execute function
function SwapOperation.Execute(state: any, from: SlotRef, to: SlotRef): SwapResult
	-- Validate
	local valid, reason = ValidateSwap(state, from, to)
	if not valid then
		return { success = false, reason = reason }
	end

	-- Get items
	local fromUUID = SlotManager.GetUUIDFromSlot(state, from.type, from.slot)
	local toUUID = SlotManager.GetUUIDFromSlot(state, to.type, to.slot)
	local hotbarCompacted = false

	-- Execute based on combination
	if from.type == "Hotbar" and to.type == "Hotbar" then
		-- Static ↔ Static: Simple swap (holes preserved)
		state.Hotbar[from.slot] = toUUID
		state.Hotbar[to.slot] = fromUUID

	elseif from.type == "Storage" and to.type == "Storage" then
		-- Dynamic ↔ Dynamic: Reorder (both must exist or we'd error above)
		state.Storage[from.slot] = toUUID
		state.Storage[to.slot] = fromUUID

	elseif from.type == "Hotbar" and to.type == "Storage" then
		-- Hotbar → Storage
		if toUUID then
			-- Both exist: Swap
			state.Hotbar[from.slot] = toUUID
			state.Storage[to.slot] = fromUUID
		else
			-- Storage slot empty: Append to storage
			if fromUUID then
				table.insert(state.Storage, fromUUID)
				state.Hotbar[from.slot] = nil
				hotbarCompacted = SlotManager.CompactHotbar(state)
			end
		end

	elseif from.type == "Storage" and to.type == "Hotbar" then
		-- Storage → Hotbar
		if toUUID then
			-- Both exist: Swap
			state.Storage[from.slot] = toUUID
			state.Hotbar[to.slot] = fromUUID
		else
			-- Hotbar slot empty: Move & shift storage
			state.Hotbar[to.slot] = fromUUID
			table.remove(state.Storage, from.slot)
		end
    else
        return { success = false, reason = "INVALID_SLOT_TYPE_COMBINATION" }
	end

	-- Note: EquippedItemUUID doesn't change - it follows the UUID, not the slot
	return { success = true, hotbarCompacted = hotbarCompacted }
end

--// Convenience wrapper with raw parameters
function SwapOperation.ExecuteRaw(state: any, fromType: string, fromSlot: number, toType: string, toSlot: number): SwapResult
	return SwapOperation.Execute(state,
		{ type = fromType :: "Hotbar" | "Storage", slot = fromSlot },
		{ type = toType :: "Hotbar" | "Storage", slot = toSlot }
	)
end

return SwapOperation


-- src/server/StowayServerV1.2/Replication/Replicator.luau
-- Centralized delta replication dispatcher

local Replicator = {}

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Actions = require(script.Parent.Actions)

-- Remote setup
local RemotesFolder = ReplicatedStorage:FindFirstChild("Remotes")
if not RemotesFolder then
	RemotesFolder = Instance.new("Folder")
	RemotesFolder.Name = "Remotes"
	RemotesFolder.Parent = ReplicatedStorage
end

local function GetRemote(name: string, class: string): any
	local remote = RemotesFolder:FindFirstChild(name)
	if not remote then
		remote = Instance.new(class)
		remote.Name = name
		remote.Parent = RemotesFolder
	end
	return remote
end

-- Main event for inventory updates
local InventoryUpdateRemote = GetRemote("InventoryUpdate", "RemoteEvent")

--// Send delta update to player
function Replicator.Send(player: Player, action: string, payload: any)
	InventoryUpdateRemote:FireClient(player, action, payload)
end

--// Convenience methods for each action type
function Replicator.SendInit(player: Player, state: any)
	local payload = Actions.Init(state)
	Replicator.Send(player, Actions.Types.INIT, payload)
end

function Replicator.SendAdd(player: Player, addedItems: any, items: any)
	local payload = Actions.Add(addedItems, items)
	Replicator.Send(player, Actions.Types.ADD, payload)
end

function Replicator.SendRemove(player: Player, slotType: string, slot: number, uuid: string)
	local payload = Actions.Remove(slotType, slot, uuid)
	Replicator.Send(player, Actions.Types.REMOVE, payload)
end

function Replicator.SendSwap(player: Player, fromType: string, fromSlot: number, toType: string, toSlot: number)
	local payload = Actions.Swap(fromType, fromSlot, toType, toSlot)
	Replicator.Send(player, Actions.Types.SWAP, payload)
end

function Replicator.SendEquip(player: Player, uuid: string?)
	local payload = Actions.Equip(uuid)
	Replicator.Send(player, Actions.Types.EQUIP, payload)
end

function Replicator.SendUpdateSettings(player: Player, Settings: any?)
	if player and Settings then
		local payload = Actions.UpdateSettings(Settings)
		Replicator.Send(player, Actions.Types.UPDATE_SETTINGS, payload)
	end
end

function Replicator.SendReload(player: Player, configSettingName: string, data: any)
	if player and configSettingName then
		local playload = Actions.Reload(configSettingName, data)
		Replicator.Send(player, Actions.Types.RELOAD, playload)
	end
end

function Replicator.SendUpdateMeta(player: Player, uuid: string, updates: { [string]: any }, newWeight)
	local payload = Actions.UpdateMeta(uuid, updates, newWeight)
	Replicator.Send(player, Actions.Types.UPDATE_META, payload)
end

--// Initial Sync Request Handling
local InitialSyncRemote = GetRemote("AskForStoway", "RemoteFunction")

function Replicator.RegisterInitialSyncCallback(callback: (Player) -> any)
	InitialSyncRemote.OnServerInvoke = callback
end

--// Get remote for external access (e.g., InitialSync)
function Replicator.GetRemotesFolder(): Folder
	return RemotesFolder
end

return Replicator


-- src/server/StowayServerV1.2/Utils/MetadataParser.luau
-- Parses metadata from Tool attributes
-- Note: "Amount" is extracted separately and excluded from metadata

local MetadataParser = {}

local ServerStorage = game:GetService("ServerStorage")

local ToolStorage = ServerStorage:FindFirstChild("Tools") :: Folder?

-- Fields that should NOT be in metadata (they're used as item properties)
local EXCLUDED_FIELDS = {
	Amount = true,  -- Amount is item.Amount, not metadata
	StackAmount = true, -- Legacy drop attribute, normalize to Amount
}

--// Parse metadata from a Tool's attributes (excludes Amount and other reserved fields)
function MetadataParser.FromTool(tool: Tool): ({ [string]: any }?, number?)
	if not tool or not tool:IsA("Tool") then
		return nil, nil
	end

	local attributes = tool:GetAttributes()
	local metadata = {}
	local amount = attributes.Amount
	if amount == nil then
		-- Backward compatibility for older dropped pickups.
		amount = attributes.StackAmount
	end

	if type(amount) == "string" then
		amount = tonumber(amount)
	end

	if type(amount) ~= "number" then
		amount = nil
	end
	if tool.TextureId ~= "" then metadata.Image = tool.TextureId end

	for key, value in pairs(attributes) do
		if not EXCLUDED_FIELDS[key] then
			metadata[key] = value
		end
	end

	-- Return nil if no metadata keys, otherwise return the table
	local finalMeta = next(metadata) and metadata or nil
	return finalMeta, amount
end

--// Get metadata from a tool template by itemId
--// Returns: metadata, defaultAmount
function MetadataParser.FromItemId(itemId: string): ({ [string]: any }?, number?)
	if not ToolStorage then
		warn("[MetadataParser] Tools folder not found in ServerStorage")
		return nil, nil
	end

	local toolTemplate = ToolStorage:FindFirstChild(itemId)
	if toolTemplate and toolTemplate:IsA("Tool") then
		return MetadataParser.FromTool(toolTemplate)
	end

	return nil, nil
end

--// Merge base metadata with overrides
function MetadataParser.Merge(base: { [string]: any }?, overrides: { [string]: any }?): { [string]: any }
	local result = {}

	-- Copy base
	if base then
		for k, v in pairs(base) do
			result[k] = v
		end
	end

	-- Apply overrides
	if overrides then
		for k, v in pairs(overrides) do
			result[k] = v
		end
	end

	return result
end

return MetadataParser


-- src/server/StowayServerV1.2/Utils/StackChecker.luau
-- Validates if items can stack and attempts stacking
-- Uses state.Settings for CanStack and MaxStackSize (per-player)
-- Uses global Settings.Stacking for item-level rules (RequiredFields, Blacklist)

local StackChecker = {}
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Settings = require(ReplicatedStorage.Shared.Settings) -- For Stacking rules
local LimitChecker = require(script.Parent.Parent.Core.LimitChecker)

--// Check if two items can stack based on ID and metadata
function StackChecker.CanStack(item1: any, item2: any): boolean
	-- Must have same ID
	if item1.Id ~= item2.Id then
		return false
	end

	local meta1 = item1.Metadata or {}
	local meta2 = item2.Metadata or {}

	-- Check required fields match (global rule)
	for _, field in ipairs(Settings.Stacking.RequiredFields) do
		if meta1[field] ~= meta2[field] then
			return false
		end
	end

	-- Check blacklist (certain rarities cannot stack) (global rule)
	local rarity = meta1.Rarity
	if rarity and Settings.Stacking.Blacklist[rarity] then
		return false
	end

	-- Deep equality check on remaining metadata
	for k, v in pairs(meta1) do
		if meta2[k] ~= v then
			return false
		end
	end
	for k, v in pairs(meta2) do
		if meta1[k] ~= v then
			return false
		end
	end

	return true
end

--// Attempt to stack amount into existing items, returns amount stacked and updated items
function StackChecker.TryStack(state: any, itemId: string, amount: number, metadata: any?): (number, {[string]: number})
	-- Check per-player CanStack setting
	if not state.Settings.CanStack then
		return 0, {}
	end

	local totalStacked = 0
	local updatedItems = {}
	local existingUUIDs = state.ItemsByID[itemId]

	if not existingUUIDs then
		return 0, {}
	end

	local newItemProxy = { Id = itemId, Metadata = metadata or {} }
	local maxStackSize = state.Settings.MaxStackSize

	for _, uuid in ipairs(existingUUIDs) do
		if amount <= 0 then break end

		local existingItem = state.Items[uuid]
		if existingItem and StackChecker.CanStack(newItemProxy, existingItem) then
			local space = maxStackSize - existingItem.Amount
			if space > 0 then
				local toAdd = math.min(amount, space)
				existingItem.Amount += toAdd
				amount -= toAdd
				totalStacked += toAdd
				updatedItems[uuid] = existingItem.Amount

				-- Update weight for stacked amount
				LimitChecker.AddWeight(state, toAdd)
			end
		end
	end

	return totalStacked, updatedItems
end

return StackChecker


-- src/server/StowayServerV1.2/Utils/UUID.luau
-- UUID generation utility

local UUID = {}

local HttpService = game:GetService("HttpService")

function UUID.Generate(): string
	return HttpService:GenerateGUID(false)
end

return UUID


-- ServerScriptService/init.server.luau

--// Stoway V1 Initialization
-- We are transitioning to the new V1 architecture.
-- The old StowayServer code is replaced by this single service entry point.

local InventoryService = require(script["StowayServerV1_2"])

InventoryService.Init()

print("Stoway Inventory System V1 Loaded")


-- ReplicatedStorage/Shared/Binds.lua

local Binds = {}

-- Helper function to apply binds to an action
-- Usage: Binds.ApplyToAction(action, Binds.Global.InventoryActions.Swap)
-- Or:   Binds.ApplyToAction(action, Binds.Global.InventoryActions.Swap, "Console") -- Console only
function Binds.ApplyToAction(action, bindConfig, inputType: ("PC" | "Console")?)
    if not action or not bindConfig then return end

    local function applyBinds(binds)
        if not binds then return end
        for _, b in ipairs(binds) do
            action:AddBind(b.Main, unpack(b.Modifiers or {}))
        end
    end

    if inputType then
        applyBinds(bindConfig[inputType])
    else
        applyBinds(bindConfig.PC)
        applyBinds(bindConfig.Console)
    end
end

-- Hotbar Slots Configuration
-- PC: Standard number keys
-- Console: Combo binds (e.g., R1 + Button)
Binds.HotbarSlots = {
    [1] = {
        Display = "1",
        PC = { {Main = Enum.KeyCode.One} },
        Console = { {Main = Enum.KeyCode.ButtonX, Modifiers = {Enum.KeyCode.ButtonR1}} } -- R1 + X
    },
    [2] = {
        Display = "2",
        PC = { {Main = Enum.KeyCode.Two} },
        Console = { {Main = Enum.KeyCode.ButtonY, Modifiers = {Enum.KeyCode.ButtonR1}} } -- R1 + Y
    },
    [3] = {
        Display = "3",
        PC = { {Main = Enum.KeyCode.Three} },
        Console = { {Main = Enum.KeyCode.ButtonB, Modifiers = {Enum.KeyCode.ButtonR1}} } -- R1 + B
    },
    [4] = {
        Display = "4",
        PC = { {Main = Enum.KeyCode.Four} },
        Console = { {Main = Enum.KeyCode.ButtonA, Modifiers = {Enum.KeyCode.ButtonR1}} } -- R1 + A
    },
    [5] = {
        Display = "5",
        PC = { {Main = Enum.KeyCode.Five} },
        Console = { {Main = Enum.KeyCode.ButtonX, Modifiers = {Enum.KeyCode.ButtonL1}} } -- L1 + X
    },
    [6] = {
        Display = "6",
        PC = { {Main = Enum.KeyCode.Six} },
        Console = { {Main = Enum.KeyCode.ButtonY, Modifiers = {Enum.KeyCode.ButtonL1}} } -- L1 + Y
    },
    [7] = {
        Display = "7",
        PC = { {Main = Enum.KeyCode.Seven} },
        Console = { {Main = Enum.KeyCode.ButtonB, Modifiers = {Enum.KeyCode.ButtonL1}} } -- L1 + B
    },
    [8] = {
        Display = "8",
        PC = { {Main = Enum.KeyCode.Eight} },
        Console = { {Main = Enum.KeyCode.ButtonA, Modifiers = {Enum.KeyCode.ButtonL1}} } -- L1 + A
    },
    [9] = {
        Display = "9",
        PC = { {Main = Enum.KeyCode.Nine} },
        Console = { {Main = Enum.KeyCode.DPadUp, Modifiers = {Enum.KeyCode.ButtonR1}} } -- R1 + DPadUp
    },
    [10] = {
        Display = "0",
        PC = { {Main = Enum.KeyCode.Zero} },
        Console = { {Main = Enum.KeyCode.DPadDown, Modifiers = {Enum.KeyCode.ButtonR1}} } -- R1 + DPadDown
    }
}

-- Global Binds
Binds.Global = {
    Backpack = {
        PC = { {Main = Enum.KeyCode.Tab}, {Main = Enum.KeyCode.B} },
        Console = { {Main = Enum.KeyCode.DPadLeft} }
    },
    SwitchMode = {
        PC = { {Main = Enum.KeyCode.M} }, -- Testing bind
        Console = { {Main = Enum.KeyCode.ButtonL3} } -- Long press logic
    },
    InventoryActions = {
        Swap = {
            Console = { {Main = Enum.KeyCode.ButtonY} }
        },
        Drop = {
            Console = { {Main = Enum.KeyCode.ButtonX} }
        },
        QuickMove = {
            Console = { {Main = Enum.KeyCode.ButtonL1}, Modifiers = {Enum.KeyCode.ButtonR1} }
        },
        Cancel = {
            Console = { {Main = Enum.KeyCode.ButtonB} }
        }
    },
    DropActions = {
        Increase = { Console = { {Main = Enum.KeyCode.DPadUp} } },
        Decrease = { Console = { {Main = Enum.KeyCode.DPadDown} } },
    }
}

return Binds


local rarities = {}

rarities.rarityColors = {
    ["godly"] = Color3.new(0.933333, 0.92549, 0.435294),
    ["lengendary"] = Color3.new(0.639216, 0.278431, 0.278431),
    ["rare"] = Color3.new(0.372549, 0.607843, 0.721569),
    ["uncommon"] = Color3.new(0.588235, 0.72549, 0.509804),
    ["common"] = Color3.new(0.4, 0.4, 0.4);
}

rarities.RaritySortOrder = {
    ["godly"] = 1,
    ["legendary"] = 2,
    ["rare"] = 3,
    ["uncommon"] = 4,
    ["common"] = 5
}

rarities.ItemTypeColor = {
    ["consumable"] = Color3.fromRGB(169, 169, 169),
    ["weapon"] = Color3.fromRGB(115,147,179),
    ["material"] = Color3.fromRGB(255,255,255)
}

-- Same concept for ItemType
rarities.ItemTypeSortOrder = {
    ["weapon"] = 1,
    ["consumable"] = 2,
    ["resource"] = 3
}

return rarities


-- src/server/StowayServerV1.2/Config/Settings.luau
-- Configuration for the Inventory System V1.2

local Settings = {}

--// Types
export type HotbarType = "Static" | "Dynamic"

--// Hotbar Settings
Settings.Hotbar = {
	Type = "Static" :: HotbarType, -- Static: fixed slots with holes, Dynamic: packed array (perplayer setting)
	MaxSlots = 10, -- (perplayer setting)
}

--// Storage/Backpack Settings (Always Dynamic)
Settings.Storage = {
	Limit = 15,             -- Total weight limit (Hotbar + Storage combined) (perplayer setting)
	CanStack = true, -- (perplayer setting)
	Sorting = true, -- (global setting)
	MaxStackSize = 5, -- (perplayer setting)
	BackpackEnabled = true, -- (global setting) can disable all backpack functions
	SortOrder = "None" -- Name, Rarity, ItemType, None (global setting)
}

--// Stacking Rules
Settings.Stacking = {
	-- Fields that MUST match for items to stack
	RequiredFields = { "Rarity", "Type" }, -- (global setting)
	-- Rarities that can NEVER stack
	Blacklist = { -- (global setting)
		["Legendary"] = true,
		["Mythic"] = true,
		["Special"] = true,
	},

	Rarity = true -- (global setting) turned the rarity frame invisible (does not stop stacking with rarities/or sorting with rarities)
}

--// Gameplay Settings
Settings.Gameplay = { -- (global settings)
	Droppable = true,
	DropDistance = 10,
	MouseScroll = true, -- must hover mouse over hotbar
}

Settings.DifferentUIs = {
	Default = {
		FolderName = "StowayGui",
		GuiName = "Stoway",
		HookPresetName = "DefaultHooks"
	},

	Admin = {
		FolderName = "StowayAdmin",
		GuiName = "AdminGui",
		HookPresetName = "AdminHooks"
	},

	Grass = {
        FolderName = "StowayGrass",
    	GuiName = "GrassUi",
    	HookPresetName = "GrassHooks"
	},

	Metal = {
        FolderName = "StowayMetal",
    	GuiName = "MetalGUI",
    	HookPresetName = "MetalHooks"
	}
}

return Settings


-- ReplicatedStorage/Shared/Types.lua
local Types = {}

Types.HotbarType = { Static = "Static", Dynamic = "Dynamic" }
Types.CarryingType = { Inventory = "Inventory", Backpack = "Backpack" }
Types.SortingType = {Name = "Name", Rarity = "Rarity", ItemType = "ItemType", SearchBar = "SearchBar"}

export type ItemData = {
	ID: string,
	Description: string,
	Image: string,
	Amount: number, -- Current stack size of this specific tool instance/group
	IsGamepass: boolean,
	Rarity: string?,
	Droppable: boolean?,
	Type: string, -- e.g., "Weapon", "Potion", "Resource"
}

export type DragStateTable = {
	ID: number,
	SourceSlot: any,
	MouseOffset: Vector2
}

return Types


--!strict

local Packages = script.Parent
local PlayerModulePackage = require(Packages.PlayerModule)

local module = {}
local patched = PlayerModulePackage.getCopy(true) :: any
local modifiers = require(patched.Modifiers) :: any

-- Adjustments

for _, modifier in script.Modifiers:GetChildren() do
	modifiers.add(modifier)
end

-- Public

function module.get(): ModuleScript
	return patched
end

function module.getCopy(): ModuleScript
	return module.get():Clone()
end

module.replace = PlayerModulePackage.replace

return module