-- Copyright (c) 2024 Huawei Technologies Co., Ltd.
-- openUBMC is licensed under Mulan PSL v2.
-- You can use this software according to the terms and conditions of the Mulan PSL v2.
-- You may obtain a copy of Mulan PSL v2 at: http://license.coscl.org.cn/MulanPSL2
--
-- THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
-- EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
-- MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
-- See the Mulan PSL v2 for more details.
local custom_msg = require 'messages.custom'
local context = require 'mc.context'
local log = require 'mc.logging'
local mdb_service = require 'mc.mdb.mdb_service'
local cjson = require 'cjson'
local m = {}

local PCIE_DEVICE_PATH_PATTERN<const> = "PCIeDevice_%d_%d+"
local PCIE_FUNCTION_RAS_INTERFACE<const> = 'bmc.kepler.Systems.PCIeDevice.PCIeFunction.RAS'
local PCIE_FUNCTION_INTERFACE<const> = 'bmc.kepler.Systems.PCIeDevice.PCIeFunction'
local PCIECARD_INTERFACE<const> = 'bmc.kepler.Systems.PCIeDevices.PCIeCard'
local BUSCTL_STAND_INTERFACE<const> = 'bmc.kepler.Object.Properties'
local DPUCARD_INTERFACE<const> = 'bmc.kepler.Systems.DPUCard'
local NETWORKADAPTER_INTERFACE<const> = 'bmc.kepler.Systems.NetworkAdapter'
local PCIE_ADDR_INFO_INTERFACE<const> = 'bmc.kepler.Systems.PcieAddrInfo'
local CPU_INTERFACE<const> = 'bmc.kepler.Systems.Processor.CPU'
local PROCESSOR_INTERFACE<const> = 'bmc.kepler.Systems.Processor'
local PCIECARD_COMPONENT_TYPE<const> = 8
local OCPCARD_COMPONENT_TYPE<const> = 83
local PCIEDEVICE_NETWORKADAPTER<const> = 2
local HOTPLUG_STATE_UNINSTALLED<const> = 4
local SYSTEMSTARTUPSTATE_BIOSPOSTCOMPLETE<const> = 254
local HOTPLUG_STATE_REMOVABLE<const> = 1
-- GetPath函数返回的无效路径
local INVALID_PATH<const> = ''

function m.get_pcie_errors(socket_id, pcie_device)
    local pcie_error = {
        FatalErrorCount = 0,
        NonFatalErrorCount = 0,
        BadDLLPCount = 0,
        BadTLPCount = 0,
        UnsupportedRequestCount = 0
    }
    if not pcie_device then
        return pcie_error
    end

    local function_ras_obj
    local function_obj
    local ok, rsp
    local function_path
    local path_collection
    for _, v in ipairs(pcie_device) do
        --仅匹配PCIeDevice对象
        if not string.match(v, PCIE_DEVICE_PATH_PATTERN) then
            goto continue
        end

        function_path = v .. "/PCIeFunctions"
        ok, rsp = pcall(mdb_service.get_sub_paths, bus, function_path, 1, { PCIE_FUNCTION_INTERFACE })
        if not ok then
            log:debug('function path (%s) is invalid, err(%s)', function_path, rsp)
            goto continue
        end
        path_collection = rsp.SubPaths
        if not path_collection then
            goto continue
        end
        for _, path in pairs(path_collection) do
            ok, function_obj = pcall(mdb.get_object, bus, path, PCIE_FUNCTION_INTERFACE)
            if not ok or not function_obj then
                log:debug('get pcie function object fail, path(%s) err(%s)', path, function_obj)
                goto next
            end
            if function_obj.RelatedProcessorId ~= socket_id then
                goto next
            end
            ok, function_ras_obj = pcall(mdb.get_object, bus, path, PCIE_FUNCTION_RAS_INTERFACE)
            if not ok or not function_ras_obj then
                log:debug('get pcie function ras object fail, path(%s) err(%s)', path, function_ras_obj)
                goto next
            end
            pcie_error.FatalErrorCount = pcie_error.FatalErrorCount + function_ras_obj.FatalErrorCount
            pcie_error.NonFatalErrorCount = pcie_error.NonFatalErrorCount + function_ras_obj.NonFatalErrorCount
            pcie_error.BadDLLPCount = pcie_error.BadDLLPCount + function_ras_obj.BadDLLPCount
            pcie_error.BadTLPCount = pcie_error.BadTLPCount + function_ras_obj.BadTLPCount
            pcie_error.UnsupportedRequestCount = pcie_error.UnsupportedRequestCount +
            function_ras_obj.UnsupportedRequestCount
            ::next::
        end
        ::continue::
    end
    return pcie_error
end

function m.get_members(slot, pcie_cards, ocp_cards)
    local uri_prefix = '/redfish/v1/Chassis/' .. slot .. '/PCIeDevices/'
    local members = {}

    if pcie_cards then
        for _, v in ipairs(pcie_cards) do
            local obj
            local ok, rsp = pcall(function()
                obj = mdb.get_object(bus, v, PCIECARD_INTERFACE)
            end)
            -- 对于有多个资源树路径的卡需要跳过查询失败的路径
            if not ok then
                log:debug('node id(%s) is invalid, err(%s)', v, rsp)
            else
                members[#members + 1] = {['@odata.id'] = uri_prefix .. (obj.NodeID or '')}
            end
        end
    end

    local ok, obj
    if ocp_cards then
        for _, v in ipairs(ocp_cards) do
            ok, obj = pcall(mdb.get_object, bus, v, PCIECARD_INTERFACE)
            if ok then
                members[#members + 1] = {['@odata.id'] = uri_prefix .. (obj.NodeID or '')}
            end
        end
    end

    return members
end

function m.get_count(pcie_cards, ocp_cards)
    local count = 0
    if pcie_cards then
        count = count + #pcie_cards
    end

    if ocp_cards then
        count = count + #ocp_cards
    end

    return count
end

function m.get_ExtendCardInfo(NetworkAdapters, SlotID, pf_mac_info)
    if not NetworkAdapters or not SlotID or not pf_mac_info then
        return {}
    end
    local list = {}

    for _, path in ipairs(NetworkAdapters) do
        local obj = mdb.get_object(bus, path, 'bmc.kepler.Systems.NetworkAdapter')
        if obj.ParentCardSlotId == SlotID then
            local PfMacInfoArr = {}
            for index, item in ipairs(pf_mac_info) do
                PfMacInfoArr[index] = {
                    ["Port"] = item[1] + 1,
                    ["PfId"] = item[2],
                    ["PermanentMac"] = item[3]
                }
            end
            list[#list + 1] = {
                ["ProductName"] = obj.Name,
                ["BoardId"] = string.lower(obj.BoardIDHex) ~= '0xffff' and obj.BoardIDHex or cjson.null,
                ["BoardName"] = obj.Name,
                ["Slot"] = obj.SlotNumber,
                ["PfMacInfo"] = PfMacInfoArr,
                ["Model"] = obj.Model,
                ["PCBVersion"] = obj.PCBVersion,
                ["DeviceId"] = obj.DeviceID ~= '0xFFFF' and obj.DeviceID or cjson.null,
                ["VendorId"] = obj.VendorID ~= '0xFFFF' and obj.VendorID or cjson.null,
                ["SubsystemId"] = obj.SubsystemDeviceID ~= '0xFFFF' and obj.SubsystemDeviceID or cjson.null,
                ["SubsystemVendorId"] = obj.SubsystemVendorID ~= '0xFFFF' and obj.SubsystemVendorID or cjson.null,
                ["Manufacturer"] = obj.Manufacturer,
                ["ChipManufacturer"] = obj.ChipManufacturer,
                ["Description"] = obj.ModelDescription,
            }
            break
        end
    end
    return list
end

local function get_table_len(tbl)
    if type(tbl) ~= 'table' then
        return
    end
    local count = 0
    for _, _ in pairs(tbl) do
        count = count + 1
    end
    return count
end

function m.is_dpu_card(node_id)
    -- node_id为空时应该返回404，而不应该返回400
    if not node_id then
        log:error('node_id is nil')
        return false
    end

    local filter = {NodeID = node_id}
    local ok, rsp = pcall(mdb_service.get_path, bus, PCIECARD_INTERFACE, cjson.encode(filter), true)
    -- 不是PCIe卡代表资源路径错误，返回404，不应该返回400
    if not ok then
        log:error('node id(%s) is invalid, err(%s)', node_id, rsp.message)
        return false
    end

    if rsp.Path == INVALID_PATH then
        log:error('Get invalid path')
        return false
    end

    -- 如果是PCIe卡，但是不是DPU卡，则返回400，表示不支持这个操作
    ok, rsp = pcall(mdb_service.get_object, bus, rsp.Path, {DPUCARD_INTERFACE})
    if not ok then
        log:error('Get DPUCard object failed')
        error(custom_msg.OperationFailed())
    end
    local count = get_table_len(rsp.Object)
    if not count or count == 0 then
        log:error('%s is not dpucard', node_id)
        error(custom_msg.OperationFailed())
    end
    return true
end

function m.get_device_class(function_class)
    if function_class == 0 then
        return "Other"
    elseif function_class == 1 then
        return "MassStorageController"
    elseif function_class == 2 then
        return "NetworkController"
    elseif function_class == 3 then
        return "DisplayController"
    elseif function_class == 4 then
        return "UnclassifiedDevice"
    elseif function_class == 5 then
        return "IntelligentController"
    elseif function_class == 6 then
        return "ProcessingAccelerators"
    elseif function_class == 7 then
        return "Other"
    elseif function_class == 8 then
        return "ProcessingAccelerators"
    elseif function_class == 9 then
        return "ProcessingAccelerators"
    elseif function_class == 10 then
        return "IntelligentController"
    elseif function_class == 11 then
        return "IntelligentController"
    else
        return "Other"
    end
end

local function get_valid_slot(obj)
    if not obj or not obj.SlotID or obj.SlotID == 0 or obj.SlotID == 255 then
        return cjson.null
    end
    return obj.SlotID
end

local function get_valid_slot_label(obj)
    local slot = get_valid_slot(obj)
    if slot == cjson.null then
        return cjson.null
    end
    return "Slot " .. slot
end

local function get_slot_links(chassis_id, pcie_addr_obj, pcie_cards, ocp_cards)
    local uri_prefix = '/redfish/v1/Chassis/' .. chassis_id .. '/PCIeDevices/'
    local members = {}
    local obj, ok
    if pcie_addr_obj.ComponentType == PCIECARD_COMPONENT_TYPE and pcie_cards then
        for _, v in ipairs(pcie_cards) do
            ok, obj = pcall(mdb.get_object, bus, v, PCIECARD_INTERFACE)
            if ok and pcie_addr_obj.SlotID == obj.SlotID then
                members[#members + 1] = {['@odata.id'] = uri_prefix .. (obj.NodeID or '')}
                return members
            end
        end
    end

    if pcie_addr_obj.ComponentType == OCPCARD_COMPONENT_TYPE and ocp_cards then
        for _, v in ipairs(ocp_cards) do
            ok, obj = pcall(mdb.get_object, bus, v, PCIECARD_INTERFACE)
            if ok and pcie_addr_obj.SlotID == obj.SlotID then
                members[#members + 1] = {['@odata.id'] = uri_prefix .. (obj.NodeID or '')}
                return members
            end
        end
    end

    return nil
end

function m.get_slot_count(pcie_addr_infos)
    local count = 0
    if pcie_addr_infos then
        local ok, obj
        for _, v in ipairs(pcie_addr_infos) do
            ok, obj = pcall(mdb.get_object, bus, v, PCIE_ADDR_INFO_INTERFACE)
            if ok and (obj.ComponentType == PCIECARD_COMPONENT_TYPE or obj.ComponentType == OCPCARD_COMPONENT_TYPE) then
                count = count + 1
            end
        end
    end
    return count
end

function m.get_logical_physical_tab(cpu_list)
    if not cpu_list or next(cpu_list) == nil then
        return
    end
    local ok, obj
    local system_id, logical_id, physical_id
    local tab = {}
    for _, path in ipairs(cpu_list) do
        ok = pcall(function()
            obj = mdb.get_object(bus, path, PROCESSOR_INTERFACE)
        end)
        if ok then
            system_id = obj.SystemId
        else
            goto continue
        end    
        ok = pcall(function()
            obj = mdb.get_object(bus, path, CPU_INTERFACE)
        end)
        if ok then
            logical_id, physical_id = obj.LogicalId, obj.PhysicalId
        else
            goto continue
        end   
        if not tab[system_id] then
            tab[system_id] = {}
        end
        tab[system_id][#(tab[system_id]) + 1] = {LogicalId = logical_id, PhysicalId = physical_id}
        ::continue::
    end
    for _, v in pairs(tab) do
        table.sort(v, function(a, b) return a['LogicalId'] < b['LogicalId'] end)
    end
    return tab
end

function m.get_slot_members(chassis_id, pcie_addr_infos, pcie_cards, ocp_cards, cpu_list)
    local members = {}
    if pcie_addr_infos then
        local ok, obj, obj_tbl, pcie_links
        local cpu_tab = m.get_logical_physical_tab(cpu_list) 
        for _, v in ipairs(pcie_addr_infos) do
            ok, obj = pcall(mdb.get_object, bus, v, PCIE_ADDR_INFO_INTERFACE)
            if ok and (obj.ComponentType == PCIECARD_COMPONENT_TYPE or obj.ComponentType == OCPCARD_COMPONENT_TYPE) then
                obj_tbl = {
                    ['PCIeType'] = obj.PCIeType,
                    ['Lanes'] = obj.Lanes,
                    ['SlotType'] = obj.SlotType,
                    ['Location'] = {
                        ['PartLocation'] = {
                            ['ServiceLabel'] = get_valid_slot_label(obj),
                            ['LocationOrdinalValue'] = get_valid_slot(obj),
                            ['LocationType'] = "Slot"
                        }
                    }
                }
                ok, pcie_links = pcall(function()
                    return get_slot_links(chassis_id, obj, pcie_cards, ocp_cards)
                end)
                if ok and pcie_links then
                    obj_tbl['Links'] = {
                        ['PCIeDevice'] = pcie_links
                    }
                end             
                local processor_links = {}
                local system_id = v:match("/bmc/kepler/Systems/(%w+)/")             
                for key, system_cpu_tab in pairs(cpu_tab) do                 
                    if tostring(key) == tostring(system_id) then
                        local socket_id = obj.SocketID
                        if socket_id == 0xfd then                       
                            for i = 1, math.min(2, #system_cpu_tab) do
                                processor_links[#processor_links + 1] = {
                                    ['@odata.id'] = '/redfish/v1/Systems/' .. key .. '/Processors/' .. system_cpu_tab[i].PhysicalId
                                }
                            end
                        elseif socket_id == 0xfc then                            
                            for i = 1, math.min(4, #system_cpu_tab) do
                                processor_links[#processor_links + 1] = {
                                    ['@odata.id'] = '/redfish/v1/Systems/' .. key .. '/Processors/' .. system_cpu_tab[i].PhysicalId
                                }
                            end
                        else                       
                            for _, v1 in pairs(system_cpu_tab) do
                                if v1.LogicalId == socket_id then
                                    processor_links[#processor_links + 1] = {
                                        ['@odata.id'] = '/redfish/v1/Systems/' .. key .. '/Processors/' .. v1.PhysicalId
                                    }
                                end
                            end
                        end
                    end
                end
                obj_tbl['Links'] = obj_tbl['Links'] or {}
                obj_tbl['Links']['Processors'] = processor_links
                members[#members + 1] = obj_tbl
            end
        end
    end
    return members
end

 function m.check_start_removing_device(DeviceName, FunctionClass, TargetReadyToRemove)
    if TargetReadyToRemove == nil then
        return false
    end
    local networkadapter_path = bus:call('bmc.kepler.maca', '/bmc/kepler/MdbService', 'bmc.kepler.Mdb', 'GetPath', 
    'a{ss}ssb', context.new(), "bmc.kepler.Systems.NetworkAdapter", 
    cjson.encode({DeviceLocator = DeviceName}), false)
    if networkadapter_path == '' then
        error(custom_messages.PropertyModificationNotSupported('ReadyToRemove'))
    end
    local obj = mdb.get_object(bus, networkadapter_path, NETWORKADAPTER_INTERFACE)
    
    local CurrentReadyToRemove = false
    if FunctionClass ~= PCIEDEVICE_NETWORKADAPTER then
        error(custom_messages.PropertyModificationNotSupported('ReadyToRemove'))
    elseif not obj.HotPluggable then
        error(custom_messages.PropertyModificationNotSupported('ReadyToRemove'))
    elseif obj.AttentionHotPlugState == HOTPLUG_STATE_UNINSTALLED then
        CurrentReadyToRemove = true
    end

    if CurrentReadyToRemove and not TargetReadyToRemove then
        error(custom_messages.PropertyModificationNotSupported('ReadyToRemove'))
    elseif not CurrentReadyToRemove and TargetReadyToRemove then
        local bios = mdb.get_object(bus, '/bmc/kepler/Systems/' .. obj.SystemID .. '/Bios', 'bmc.kepler.Systems.Bios')
        if bios.SystemStartupState ~= SYSTEMSTARTUPSTATE_BIOSPOSTCOMPLETE or obj.AttentionHotPlugState ~= HOTPLUG_STATE_REMOVABLE then
            error(custom_messages.PropertyModificationNotSupported('ReadyToRemove'))
        end
        return true
    else
        return false
    end
end

function m.get_function_member_and_count(chassisid, id, func_paths)
    local uri_prefix = '/redfish/v1/Chassis/' .. chassisid .. '/PCIeDevices/' .. id .. '/PCIeFunctions/'
    local functions_infos = {
        Count = 0,
        Members = {}
    }

    if not func_paths or next(func_paths) == nil then
        functions_infos.Count = 1
        functions_infos.Members[1] = { ["@odata.id"] = uri_prefix .. "1" }
        return functions_infos
    end

    for i = 1, #func_paths do
        functions_infos.Count = functions_infos.Count + 1
        functions_infos.Members[functions_infos.Count] = { ["@odata.id"] = uri_prefix .. i }
    end

    return functions_infos
end

function m.get_expend(id)
    if id == "1" then
        return "get_pcie_funcion_single_id"
    end

    return "get_pcie_funcion_multi_id"
end

function m.get_function_name(device_path, func_id)
    local sub_objs = mdb.get_sub_objects(bus, device_path .. '/PCIeFunctions/', BUSCTL_STAND_INTERFACE)

    local count = 0
    for _, _ in pairs(sub_objs) do
        count = count + 1
    end

    if next(sub_objs) == nil then
        if func_id == "1" then
            return "1"
        end
        return nil
    end

    local ok = (tonumber(func_id) or 0) > 0

    if not ok then
        return nil
    end
    local function_name_list = {}
    for _, obj in pairs(sub_objs) do
        function_name_list[#function_name_list + 1] = obj.ObjectName
    end

    table.sort(function_name_list)

    ok = (tonumber(func_id) or #function_name_list + 1) <= #function_name_list
    if not ok then
        return nil
    end

    return function_name_list[tonumber(func_id)]
end

return m
