if not lib then return end

local Inventory = {}

Inventory.Dumpsters = lib.array:new(218085040, 666561306, -58485588, -206690185, 1511880420, 682791951)

if shared.networkdumpsters then
    -- Make sure dumpsters are frozen to ensure persistent position across clients
    SetInterval(function()
        local objects = GetGamePool('CObject')

        for i = 1, #objects do
            local object = objects[i]
            local state = Entity(object).state

            if state.isDumpster == nil then
                local model = GetEntityModel(object)
                local isDumpster = Inventory.Dumpsters:includes(model)

                state.isDumpster = isDumpster

                if isDumpster then
                    FreezeEntityPosition(object, true)
                end
            end
        end
    end, 3000)
end

function Inventory.OpenDumpster(entity)
    if shared.networkdumpsters then
        local coords = GetEntityCoords(entity)
        client.openInventory('dumpster', coords)
        return
    end

    local netId = NetworkGetEntityIsNetworked(entity) and NetworkGetNetworkIdFromEntity(entity)

    if not netId then
        local coords = GetEntityCoords(entity)
        entity = GetClosestObjectOfType(coords.x, coords.y, coords.z, 0.1, GetEntityModel(entity), true, true, true)
        netId = entity ~= 0 and NetworkGetNetworkIdFromEntity(entity)
    end

    if netId then
        client.openInventory('dumpster', 'dumpster' .. netId)
    end
end

local Utils = require 'modules.utils.client'
local Vehicles = lib.load('data.vehicles')
local backDoorIds = { 2, 3 }

function Inventory.CanAccessTrunk(entity)
    if cache.vehicle or not NetworkGetEntityIsNetworked(entity) then return end

    local vehicleHash = GetEntityModel(entity)
    local vehicleClass = GetVehicleClass(entity)
    local checkVehicle = Vehicles.Storage[vehicleHash]

    if (checkVehicle == 0 or checkVehicle == 1) or (not Vehicles.trunk[vehicleClass] and not Vehicles.trunk.models[vehicleHash]) then return end

    ---@type number | number[]
    local doorId = checkVehicle and 4 or 5

    if not Vehicles.trunk.boneIndex?[vehicleHash] and not GetIsDoorValid(entity, doorId --[[@as number]]) then
        if vehicleClass ~= 11 and (doorId ~= 5 or GetEntityBoneIndexByName(entity, 'boot') ~= -1 or not GetIsDoorValid(entity, 2)) then
            return
        end

        if vehicleClass ~= 11 then
            doorId = backDoorIds
        end
    end

    local min, max = GetModelDimensions(vehicleHash)
    local offset = (max - min) * (not checkVehicle and vec3(0.5, 0, 0.5) or vec3(0.5, 1, 0.5)) + min
    offset = GetOffsetFromEntityInWorldCoords(entity, offset.x, offset.y, offset.z)

    if #(GetEntityCoords(cache.ped) - offset) < 1.5 then
        return doorId
    end
end

function Inventory.OpenTrunk(entity)
    ---@type number | number[] | nil
    local door = Inventory.CanAccessTrunk(entity)

    if not door then return end

    local coords = GetEntityCoords(entity)

    TaskTurnPedToFaceCoord(cache.ped, coords.x, coords.y, coords.z, 0)

    if not client.openInventory('trunk', { netid = NetworkGetNetworkIdFromEntity(entity), entityid = entity, door = door }) then return end

    if type(door) == 'table' then
        for i = 1, #door do
            SetVehicleDoorOpen(entity, door[i], false, false)
        end
    else
        SetVehicleDoorOpen(entity, door --[[@as number]], false, false)
    end
end

if shared.target then
    exports.ox_target:addModel(Inventory.Dumpsters, {
        icon = 'fas fa-dumpster',
        label = locale('search_dumpster'),
        onSelect = function(data) return Inventory.OpenDumpster(data.entity) end,
        distance = 2
    })

    exports.ox_target:addGlobalVehicle({
        icon = 'fas fa-truck-ramp-box',
        label = locale('open_label', locale('storage')),
        distance = 1.5,
        canInteract = Inventory.CanAccessTrunk,
        onSelect = function(data)
            return Inventory.OpenTrunk(data.entity)
        end
    })
end

---@param search 'slots' | 1 | 'count' | 2
---@param item table | string
---@param metadata? table | string
function Inventory.Search(search, item, metadata)
    if not PlayerData.loaded then
        if not coroutine.running() then
            error('player inventory has not yet loaded.')
        end

        repeat Wait(100) until PlayerData.loaded
    end

    if item then
        if search == 'slots' then search = 1 elseif search == 'count' then search = 2 end
        if type(item) == 'string' then item = { item } end
        if type(metadata) == 'string' then metadata = { type = metadata } end

        local items = #item
        local returnData = {}
        for i = 1, items do
            local item = string.lower(item[i])
            if item:sub(0, 7) == 'weapon_' then item = string.upper(item) end
            if search == 1 then
                returnData[item] = {}
            elseif search == 2 then
                returnData[item] = 0
            end
            for _, v in pairs(PlayerData.inventory) do
                if v.name == item then
                    if not v.metadata then v.metadata = {} end
                    if not metadata or table.contains(v.metadata, metadata) then
                        if search == 1 then
                            returnData[item][#returnData[item] + 1] = PlayerData.inventory[v.slot]
                        elseif search == 2 then
                            returnData[item] += v.count
                        end
                    end
                end
            end
        end
        if next(returnData) then return items == 1 and returnData[item[1]] or returnData end
    end
    return false
end

exports('Search', Inventory.Search)

exports('GetPlayerItems', function()
    return PlayerData.inventory
end)

exports('GetPlayerWeight', function()
    return PlayerData.weight
end)

exports('GetPlayerMaxWeight', function()
    return PlayerData.maxWeight
end)

local Items = require 'modules.items.client'

local function assertMetadata(metadata)
    if metadata and type(metadata) ~= 'table' then
        metadata = metadata and { type = metadata or nil }
    end

    return metadata
end

---@param itemName string
---@param metadata? any
---@param strict? boolean Strictly match metadata properties, otherwise use partial matching.
---@return SlotWithItem?
function Inventory.GetSlotWithItem(itemName, metadata, strict)
    local inventory = PlayerData.inventory
    local item = Items(itemName) --[[@as OxClientItem?]]

    if not inventory or not item then return end

    metadata = assertMetadata(metadata)
    local tablematch = strict and table.matches or table.contains

    for _, slotData in pairs(inventory) do
        if slotData and slotData.name == item.name and (not metadata or tablematch(slotData.metadata, metadata)) then
            return slotData
        end
    end
end

exports('GetSlotWithItem', Inventory.GetSlotWithItem)

---@param itemName string
---@param metadata? any
---@param strict? boolean Strictly match metadata properties, otherwise use partial matching.
---@return number?
function Inventory.GetSlotIdWithItem(itemName, metadata, strict)
    return Inventory.GetSlotWithItem(itemName, metadata, strict)?.slot
end

exports('GetSlotIdWithItem', Inventory.GetSlotIdWithItem)

---@param itemName string
---@param metadata? any
---@param strict? boolean Strictly match metadata properties, otherwise use partial matching.
---@return SlotWithItem[]?
function Inventory.GetSlotsWithItem(itemName, metadata, strict)
    local inventory = PlayerData.inventory
    local item = Items(itemName) --[[@as OxClientItem?]]

    if not inventory or not item then return end


    metadata = assertMetadata(metadata)
    local response = {}
    local n = 0
    local tablematch = strict and table.matches or table.contains

    for _, slotData in pairs(inventory) do
        if slotData and slotData.name == item.name and (not metadata or tablematch(slotData.metadata, metadata)) then
            n += 1
            response[n] = slotData
        end
    end

    return response
end

exports('GetSlotsWithItem', Inventory.GetSlotsWithItem)

---@param itemName string
---@param metadata? any
---@param strict? boolean Strictly match metadata properties, otherwise use partial matching.
---@return number[]?
function Inventory.GetSlotIdsWithItem(itemName, metadata, strict)
    local items = Inventory.GetSlotsWithItem(itemName, metadata, strict)

    if items then
        ---@cast items +number[]
        for i = 1, #items do
            items[i] = items[i].slot
        end

        return items
    end
end

---@param itemName string
---@param metadata? any
---@param strict? boolean Strictly match metadata properties, otherwise use partial matching.
---@return number
function Inventory.GetItemCount(itemName, metadata, strict)
    local inventory = PlayerData.inventory
    local item = Items(itemName) --[[@as OxClientItem?]]

    if not inventory or not item then return 0 end

    if not metadata then
        return item.count
    end


    metadata = assertMetadata(metadata)
    local count = 0
    local tablematch = strict and table.matches or table.contains

    for _, slotData in pairs(inventory) do
        if slotData and slotData.name == item.name and (not metadata or tablematch(slotData.metadata, metadata)) then
            count += slotData.count
        end
    end

    return count
end

exports('GetItemCount', Inventory.GetItemCount)


local function openEvidence()
    client.openInventory('policeevidence')
end

local textPrompts = {
    evidence = {
        options = { icon = 'fa-box-archive' },
        message = ('**%s**  \n%s'):format(locale('open_police_evidence'),
            locale('interact_prompt', GetControlInstructionalButton(0, 38, true):sub(3)))
    },
    stash = {
        options = { icon = 'fa-warehouse' },
        message = ('**%s**  \n%s'):format(locale('open_stash'),
            locale('interact_prompt', GetControlInstructionalButton(0, 38, true):sub(3)))
    }
}

Inventory.Evidence = setmetatable(lib.load('data.evidence'), {
    __call = function(self)
        for _, evidence in pairs(self) do
            if evidence.point then
                evidence.point:remove()
            elseif evidence.zoneId then
                exports.ox_target:removeZone(evidence.zoneId)
                evidence.zone = nil
            end

            if client.hasGroup(shared.police) then
                if shared.target then
                    if evidence.target then
                        evidence.zoneId = Utils.CreateBoxZone(evidence.target, {
                            {
                                icon = evidence.target.icon or 'fas fa-warehouse',
                                label = locale('open_police_evidence'),
                                groups = shared.police,
                                onSelect = openEvidence,
                                iconColor = evidence.target.iconColor,
                            }
                        })
                    end
                else
                    evidence.target = nil
                    evidence.point = lib.points.new({
                        coords = evidence.coords,
                        distance = 16,
                        inv = 'policeevidence',
                        marker = client.evidencemarker,
                        prompt = textPrompts.evidence,
                        nearby = Utils.nearbyMarker
                    })
                end
            end
        end
    end
})

Inventory.Stashes = setmetatable(lib.load('data.stashes'), {
    __call = function(self)
        for id, stash in pairs(self) do
            if stash.jobs then stash.groups = stash.jobs end

            if stash.point then
                stash.point:remove()
            elseif stash.zoneId then
                exports.ox_target:removeZone(stash.zoneId)
                stash.zoneId = nil
            end

            if not stash.groups or client.hasGroup(stash.groups) then
                if shared.target then
                    if stash.target then
                        stash.zoneId = Utils.CreateBoxZone(stash.target, {
                            {
                                icon = stash.target.icon or 'fas fa-warehouse',
                                label = stash.target.label or locale('open_stash'),
                                groups = stash.groups,
                                onSelect = function()
                                    exports.ox_inventory:openInventory('stash', stash.name)
                                end,
                                iconColor = stash.target.iconColor,
                            },
                        })
                    end
                else
                    stash.target = nil
                    stash.point = lib.points.new({
                        coords = stash.coords,
                        distance = 16,
                        inv = 'stash',
                        invId = stash.name,
                        marker = client.evidencemarker,
                        prompt = textPrompts.stash,
                        nearby = Utils.nearbyMarker
                    })
                end
            end
        end
    end
})

RegisterNetEvent('ox_inventory:refreshMaxWeight', function(data)
    if data.inventoryId == cache.serverId then
        PlayerData.maxWeight = data.maxWeight
    end

    SendNUIMessage({
        action = 'refreshSlots',
        data = {
            weightData = {
                inventoryId = data.inventoryId,
                maxWeight = data.maxWeight
            }
        }
    })
end)

RegisterNetEvent('ox_inventory:refreshSlotCount', function(data)
    SendNUIMessage({
        action = 'refreshSlots',
        data = {
            slotsData = {
                inventoryId = data.inventoryId,
                slots = data.slots
            }
        }
    })
end)

return Inventory
