-- 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.

-- Description: 负责监听BIOS上报的PCIe设备信息并完成加载
local class = require 'mc.class'
local log = require 'mc.logging'
local cmn = require 'common'
local def = require 'biz_topo.def'
local c_load_info = require 'biz_topo.class.load_info'
local object_manage = require 'mc.mdb.object_manage'
local m_error = require 'pcie_device.errors'
local imu_cmd = require 'imu'
local utils_core = require 'utils.core'
local c_device_service = require 'device.device_service'
local enums = require 'mc.ipmi.enums'
local channel_type = enums.ChannelType
local app_ipmi = require 'pcie_device.ipmi.ipmi'
local bs = require 'mc.bitstring'
local ctx = require 'mc.context'
local cjson = require 'cjson'
local plug_record_mgmt = require 'plug_record_mgmt'

local UINT8_MAX<const> = 0xff
local UINT16_MAX<const> = 0xffff
local UINT32_MAX<const> = 0xffffffff

local OBJGRP_PATH_PREFIX<const> = '/bmc/kepler/ObjectGroup/'
local CSR_DIR<const> = "/opt/bmc/sr"
local FLASH_ERROR_ID<const> = "fffffffe"
local FLASH_ERROR_AUX_ID<const> = "ffffffff"
local DEFAULT_RESERVERD1 = 0x00

local FAULT_DIAGNOSIS_SERVICE<const> = "bmc.kepler.fault_diagnosis"
local IPMI_SET_MULTI_SYSTEM_PCIE_ADDR_INFO<const> = "/bmc/kepler/IpmiCmds/30/92/SetMultiSystemPcieAddrInfo"
local INTERFACE_CMD_INFO<const> = "bmc.kepler.CmdInfo"

---@class DeviceLoader @PCIe设备加载
local c_device_loader = class()

-- 从持久化数据中加载PCIe设备
function c_device_loader:load_device_by_per_info(position)
    local pcie_biz_connectors = self.biz_topo:get_objs('PCIeBizConnector', position)
    for _, c in pairs(pcie_biz_connectors) do
        -- 加载的设备类型合法性校验
        local type = nil
        if c.ref_pcie_addr_info then
            type = def.com_type_to_type[c.ref_pcie_addr_info:get_prop('ComponentType')]
        end
        if not type or not self.persistent_load_info[type] then
            log:debug('[BizTopoLoader] Load failed, invalid type, type=%s, position=%s', type,
                position)
            goto next
        end

        -- 从持久化数据中查找该槽位的PCIe设备记录，找到则进行加载
        local record = self.persistent_load_info[type][c.ref_pcie_addr_info:get_prop('SlotID')]
        if not record then
            goto next
        end

        log:debug('[BizTopoLoader] load_device_by_per_info record.DevBus=%s', record.DevBus)
        c.ref_pcie_addr_info:update_device_bdf(record.DevBus, record.DevDevice, record.DevFunction)
        c.ref_pcie_addr_info:update_multihost_presence(record.MultihostPresence or 1)
        c.ref_pcie_addr_info:update_vid_did(record.ID >> 16 & UINT16_MAX, record.ID & UINT16_MAX)

        -- 从持久化数据中加载卡时，跳过插入日志记录
        plug_record_mgmt.get_instance():skip_card_record_prepare(type, record.SlotID)

        self:task_load_unload_device(c_load_info.new(type, record.SlotID, record.ID, record.AuxID),
            record.MultihostPresence, position, type)

        ::next::
    end
end

-- 递归获取设备BDF
local function get_topo_device_bdf(sys_id, bus, sbdf)
    local payload = {
        system_id = 1,
        is_local = false,
        cpu_id = sbdf.socket,
        bus_num = sbdf.bus,
        device_num = sbdf.device,
        function_num = sbdf.func,
        address = imu_cmd.pci_info_address_list.CLASS_CODE_INFO_ADDR,
        read_length = 4
    }
    local info = imu_cmd.get_info_from_pmu(sys_id, bus, payload)

    -- 递归终止条件1：未获取到信息或该节点为EP节点
    if info == nil or #info < 4 then
        return
    end

    -- 递归终止条件2：该节点为OCP卡设备节点
    if info[imu_cmd.addr_detail.class_code_info_addr.CLASS_CODE] == 
        imu_cmd.pci_info_base_class_code.CLASS_CODE_NET_CARD then
        return sbdf
    end

    -- 在SecBus - SubBus之间递归查找下级设备
    payload.address = imu_cmd.pci_info_address_list.BASE_ADDR_2_ADDR
    info = imu_cmd.get_info_from_pmu(sys_id, bus, payload)
    if info == nil or #info < 4 then
        return
    end

    local sec_bus = info[imu_cmd.addr_detail.base_addr_2_addr.SEC_BUS]
    local sub_bus = info[imu_cmd.addr_detail.base_addr_2_addr.SUB_BUS]

    local sub_sbdf = {
        socket = sbdf.socket
    }
    for bus_number = sec_bus, sub_bus do
        for device_number = imu_cmd.bdf_range.MIN_DEVICE, imu_cmd.bdf_range.MAX_DEVICE do
            for func_number = imu_cmd.bdf_range.MIN_FUNCTION, imu_cmd.bdf_range.MAX_FUNCTION do
                sub_sbdf.bus = bus_number
                sub_sbdf.device = device_number
                sub_sbdf.func = func_number
                if get_topo_device_bdf(sys_id, bus, sub_sbdf) then
                    return sub_sbdf
                end
            end
        end
    end
end

local function get_pcie_vid_did_info(sys_id, bus, pcie_info)
    local payload = {
        system_id = 1,
        is_local = false,
        cpu_id = pcie_info.bus_info.socket_id,
        bus_num = pcie_info.bus_info.bus,
        device_num = pcie_info.bus_info.device,
        function_num = pcie_info.bus_info.func,
        address = imu_cmd.pci_info_address_list.VID_DID_INFO_ADDR,
        read_length = 4
    }
    local info = imu_cmd.get_info_from_pmu(sys_id, bus, payload)
    if info == nil or #info < 4 then
        error(m_error.get_pci_info_failed())
    end
    local device_id = ((info[4] & 0xff) << 8) + (info[3] & 0xff)
    local vendor_id = ((info[2] & 0xff) << 8) + (info[1] & 0xff)
    return vendor_id, device_id
end

local function get_pcie_sub_vid_did_info(sys_id, bus, pcie_info)
    local payload = {
        system_id = 1,
        is_local = false,
        cpu_id = pcie_info.bus_info.socket_id,
        bus_num = pcie_info.bus_info.bus,
        device_num = pcie_info.bus_info.device,
        function_num = pcie_info.bus_info.func,
        address = imu_cmd.pci_info_address_list.SUBVID_SUBDID_INFO_ADDR,
        read_length = 4
    }
    local info = imu_cmd.get_info_from_pmu(sys_id, bus, payload)
    if info == nil or #info < 4 then
        error(m_error.get_pci_info_failed())
    end
    local sub_device_id = ((info[4] & 0xff) << 8) + (info[3] & 0xff)
    local sub_vendor_id = ((info[2] & 0xff) << 8) + (info[1] & 0xff)
    return sub_vendor_id, sub_device_id
end

-- 通过BDF查询vid/did/sub_vid/sub_did
local function query_id_from_pmu(sys_id, bus, info)
    if info.type == def.device_type.DISK then
        return
    end

    local err, vid, did, sub_vid, sub_did
    err, vid, did = pcall(get_pcie_vid_did_info, sys_id, bus, info)
    if not err then
        log:error('[BizTopoLoader] Get vid/did from pmu failed, %s', err)
        return
    end
    err, sub_vid, sub_did = pcall(get_pcie_sub_vid_did_info, sys_id, bus, info)
    if not err then
        log:error('[BizTopoLoader] Get subvid/subdid from pmu failed, %s', err)
        return
    end
    info.vid = vid
    info.did = did
    info.sub_vid = sub_vid
    info.sub_did = sub_did
    log:notice('[BizTopoLoader] Get id from PMU, vid:%s, did:%s, sub_vid:%s, sub_did:%s', info.vid,
        info.did, info.sub_vid, info.sub_did)
end

-- 卸载卡
local function unload_device_connector(load_info, connector, addr_multi_presence)
    -- 软件类告警消除需要component存在，卡卸载前需要消除软件类告警
    if connector.Id == FLASH_ERROR_ID and connector.AuxId == FLASH_ERROR_AUX_ID then
        c_device_service.get_instance():clear_flash_checker_event(load_info.slot_id)
    end

    if addr_multi_presence == 0 then
        object_manage.remove_objects(OBJGRP_PATH_PREFIX .. connector.GroupPosition)
        connector.Presence = 0
        log:notice('[BizTopoLoader] Unload %s, Slot=%s', load_info.type, load_info.slot_id)
    end
end

-- 从硬件获取下级设备的slot位置
local function set_slot_from_load_info(connector, slot_id)
    if not slot_id or slot_id < def.PCIE_SLOT_ID_VALUE.MIN_SLOT_ID_VALUE or 
        slot_id > def.PCIE_SLOT_ID_VALUE.MAX_SLOT_ID_VALUE then
        log:error('set pcie decvice slot_id=%s failed, '..
                'correct value %s< slot_id <%s', slot_id, def.PCIE_SLOT_ID_VALUE.MIN_SLOT_ID_VALUE,
                def.PCIE_SLOT_ID_VALUE.MAX_SLOT_ID_VALUE)
        return
    end 
    local ok, err = pcall(function ()
        connector.Slot = slot_id
    end)
    if not ok then
        log:error('set slot from load info failed, %s', err)
    end
end

-- 对比传入的加载信息和Connector对象上已有的信息是否相同，决定PCIe设备的卸载/加载
---@param load_info LoadInfo @传入的PCIe设备加载信息
function c_device_loader:load_unload_device(load_info, position, is_timeout, addr_multi_presence)
    local ok, connector, tc_connector = self.biz_topo:get_mgmt_connector(load_info.type, load_info.slot_id, position)
    if not ok then
        return false, connector
    end
    if tc_connector then
        set_slot_from_load_info(tc_connector, load_info.slot_id)
        if tc_connector.Presence == 1 and tc_connector.LoadStatus == 0 then
            -- 天池加载成功
            log:notice('[BizTopoLoader] TianChiConnector for slot=%s is already loaded, \
                path=%s, Status=%s, Presence=%s, Id-AuxId=%s-%s, skip loading', load_info.slot_id, tc_connector.path,
                tc_connector.LoadStatus, tc_connector.Presence, tc_connector.Id, tc_connector.AuxId)
            return true
        elseif tc_connector.LoadStatus ~= 0 and tc_connector.LoadStatus ~= 255 then
            -- 天池加载出错，用四元组方式加载
            log:notice('[BizTopoLoader] TianChiConnector for slot=%s is not loaded, \
                path=%s, Status=%s, Presence=%s, Id-AuxId=%s-%s, using normal connector', load_info.slot_id,
                tc_connector.path, tc_connector.LoadStatus, tc_connector.Presence, tc_connector.Id, tc_connector.AuxId)
        elseif not is_timeout then
            -- 如果没有超时，就继续等待天池连接器状态就绪
            return false, def.load_error.TIANCHI_CONNECTOR_NOT_READY
        end
    end

    -- 对比Connector上已有属性值，无变化则无需重复加载
    local str_id, str_auxid = self:get_id_auxid_str(connector.Bom, load_info)
    log:notice('[BizTopoLoader] Connector is detected, Slot=%s, path=%s, Id-AuxId=%s-%s, load-Id-AuxId=%s-%s',
        load_info.slot_id, connector.path, connector.Id, connector.AuxId, str_id, str_auxid)
    if connector.Id == str_id and connector.AuxId == str_auxid and connector.Presence == 1 then
        log:notice('[BizTopoLoader] Connector is already loaded, Slot=%s', load_info.slot_id)
        return true
    end

    -- 兼容multihost卸载卡
    unload_device_connector(load_info, connector, addr_multi_presence)

    -- 有变化先卸载，再加载
    if load_info.id ~= UINT32_MAX then -- 0xffffffff表示没有卡在位
        object_manage.remove_objects(OBJGRP_PATH_PREFIX .. connector.GroupPosition)
        connector.Presence = 0
        log:notice('[BizTopoLoader] Unload %s, Slot=%s', load_info.type, load_info.slot_id)

        set_slot_from_load_info(connector, load_info.slot_id)
        cmn.skynet.sleep(100 * 1) -- 1s延迟
        connector.Id = str_id
        connector.AuxId = str_auxid

        plug_record_mgmt.get_instance():skip_card_record_process(load_info.type, load_info.slot_id)
        connector.Presence = 1
        log:notice('[BizTopoLoader] Load %s, Slot=%s, path=%s, Id-AuxId=%s-%s', load_info.type,
            load_info.slot_id, connector.path, connector.Id, connector.AuxId)
    else
        -- 卡不在位清除软件告警
        c_device_service.get_instance():clear_flash_checker_event(load_info.slot_id)
    end
    return true
end

function c_device_loader:get_id_auxid_str(bom, load_info)
    if not bom or not load_info or not load_info.id or not load_info.aux_id then
        return '', ''
    end

    local str_id = string.format('%08x', load_info.id)
    local str_auxid = string.format('%08x', load_info.aux_id)
    -- 定制化场景，替换四元组信息，指向定制化csr文件
    str_id, str_auxid = self:handle_pcie_custom_display(str_id, str_auxid)
    local file_path = string.format('%s/%s_%s_%s.sr', CSR_DIR, bom, str_id, str_auxid)
    -- pcie卡在位且sr文件不存在，加载默认错误的csr
    if load_info.type == def.device_type.PCIE_CARD and load_info.id ~= UINT32_MAX and
        not utils_core.is_file(file_path) then
        return FLASH_ERROR_ID, FLASH_ERROR_AUX_ID
    end
    return str_id, str_auxid
end

function c_device_loader:task_load_unload_device(load_info, addr_multi_presence, position, device_type)
    if device_type == def.device_type.DISK then
        return -- nvme不需要四元组加载
    end

    cmn.skynet.fork(function()
        local timeout = 120 -- 加载超时时间120s
        local tc_timeout = 100 -- 等待天池连接器超时时间100s
        local ok, ret
        repeat
            ok, ret = self:load_unload_device(load_info, position, tc_timeout <= 0, addr_multi_presence)
            cmn.skynet.sleep(500) -- 等待5秒
            timeout = timeout - 5
            tc_timeout = tc_timeout - 5
            -- 满足以下3个条件任意之一，停止加载任务
            -- 1.加载成功
            -- 2.没有匹配到业务连接器或指定槽位的PCIeAddrInfo对象
            -- 3.超时
            if timeout <= 0 then
                log:error('[BizTopoLoader] Load %s time out, slot=%s', load_info.type,
                    load_info.slot_id)
                return
            end
            if ok then
                break
            end
            if ret == def.load_error.NO_MATCHING_BUSINESS_CONNECTOR then
                log:error('[BizTopoLoader] No matching BusinessConnector found, position=%s',
                    position)
                return
            end
            if ret == def.load_error.NO_MATCHING_PCIE_ADDR_INFO then
                log:debug('[BizTopoLoader] No matching PCIeAddrInfo found, type=%s, slot=%s',
                    load_info.type, load_info.slot_id)
                return
            end
            if ret == def.load_error.TIANCHI_CONNECTOR_NOT_READY then
                log:debug('[BizTopoLoader] TianChi Connector is not ready, position=%s', position)
            end
        until ok
        plug_record_mgmt.get_instance():skip_card_record_cancel(load_info.type, load_info.slot_id)
    end)
end

-- 获取持久化pcie卡加载信息
local function get_load_info_records(db_handle, tbl)
    local records = {}
    if not tbl then
        return records
    end
    local table_disk = db_handle:select(tbl):where(tbl.Type:eq(def.device_type.DISK)):all()
    local table_pcie = db_handle:select(tbl):where(tbl.Type:eq(def.device_type.PCIE_CARD)):all()
    local table_ocp = db_handle:select(tbl):where(tbl.Type:eq(def.device_type.OCP_CARD)):all()
    for _, v in pairs(table_disk) do
        table.insert(records, v)
    end
    for _, v in pairs(table_pcie) do
        table.insert(records, v)
    end
    for _, v in pairs(table_ocp) do
        table.insert(records, v)
    end
    return records
end

-- 判断是否需要拷贝本地持久化数据
local function need_recover_from_local(local_records)
    -- 如果是有RecoverFlag，就需要还原
    if local_records and local_records[1] and local_records[1].RecoverFlag == 1 then
        return true
    end
    return false
end

-- 更新本地持久化数据到远程持久化
local function recover_remote_db(local_records, remote_db_handle, remote_records)
    -- 删除所有远程持久化数据
    for _, remote_record in pairs(remote_records) do
        remote_record:delete()
    end

    local new_remote_record
    for _, local_record in pairs(local_records) do
        if local_record.DevBus == UINT8_MAX and local_record.DevDevice == UINT8_MAX and
            local_record.DevFunction == UINT8_MAX then
            log:notice('[BizTopoLoader] skip empty record')
            goto next
        end

        new_remote_record = remote_db_handle.PCIeLoadInfo({
            Type = local_record.Type,
            SlotID = local_record.SlotID,
            ID = local_record.ID,
            AuxID = local_record.AuxID,
            Segment = local_record.Segment,
            SocketID = local_record.SocketID,
            DevBus = local_record.DevBus,
            DevDevice = local_record.DevDevice,
            DevFunction = local_record.DevFunction,
            MultihostPresence = local_record.MultihostPresence
        })
        new_remote_record:save()
        log:notice('[BizTopoLoader] Add persistance PCIeLoadInfo, Type=%s, SlotID=%s, DevBus=%s, MultiPresence=%s',
            new_remote_record.Type, new_remote_record.SlotID, new_remote_record.DevBus,
            new_remote_record.MultihostPresence)
        ::next::
        -- 删除本地持久化数据，只用一次。
        local_record:delete()
    end
end

-- 使用远程持久化数据，初始化PCIe加载信息
function c_device_loader:persistent_load_info_init()
    local remote_db_handle = self.db
    local remote_records = get_load_info_records(remote_db_handle, remote_db_handle.PCIeLoadInfo)

    self.persistent_load_info = {
        [def.device_type.DISK] = {},
        [def.device_type.PCIE_CARD] = {},
        [def.device_type.OCP_CARD] = {}
    }
    for _, record in pairs(remote_records) do
        if self.persistent_load_info[record.Type] then
            self.persistent_load_info[record.Type][record.SlotID] = record
            log:notice(
                '[BizTopoLoader] Get persistance PCIeLoadInfo, Type=%s, SlotID=%s, DevBus=%s',
                record.Type, record.SlotID, record.DevBus)
        end
    end
end

-- 读取持久化PCIe加载信息
function c_device_loader:pcie_load_info_init()
    -- 兼容旧版本使用远程持久化数据
    local local_db_handle = self.reset_local_db
    local local_records = get_load_info_records(local_db_handle, local_db_handle.PCIeLoadInfoBackup)

    local remote_db_handle = self.db
    local remote_records = get_load_info_records(remote_db_handle, remote_db_handle.PCIeLoadInfo)

    -- 判断是否需要更新本地持久化数据到远程持久化
    if need_recover_from_local(local_records) then
        recover_remote_db(local_records, remote_db_handle, remote_records)
    end

    -- 使用远程持久化数据
    self:persistent_load_info_init()
end

function c_device_loader:handle_pcie_custom_display(id, aux_id)
    local quadruple_id = string.format('%s_%s', id, aux_id)
    local db = self.db
    local tbl = db.PCIeCardDisplayCustom
    local records = db:select(tbl):where(tbl.QuadrupleId:eq(quadruple_id)):all()
    for _, record in pairs(records) do
        if record.Enabled then
            log:notice('[handle_pcie_custom_display] %s custom display type to %s', record.QuadrupleId,
            record.CustomType)
            id = record.QuadrupleId
            aux_id = record.CustomType
            return id, aux_id
        end
    end
    log:notice('[handle_pcie_custom_display] %s not custom display', quadruple_id)
    return id, aux_id
end

-- 还原/恢复出厂设置的时候，备份当前pcie卡信息到本地持久化数据。
function c_device_loader:on_recover_cb()
    local local_db_handle = self.reset_local_db
    local local_records = get_load_info_records(local_db_handle, local_db_handle.PCIeLoadInfoBackup)
    local remote_db_handle = self.db
    local remote_records = get_load_info_records(remote_db_handle, remote_db_handle.PCIeLoadInfo)
    -- 删除所有本地持久化数据
    for _, local_record in pairs(local_records) do
        local_record:delete()
    end

    local new_local_record
    for _, remote_record in pairs(remote_records) do
        new_local_record = local_db_handle.PCIeLoadInfoBackup({
            Type = remote_record.Type,
            SlotID = remote_record.SlotID,
            ID = remote_record.ID,
            AuxID = remote_record.AuxID,
            Segment = remote_record.Segment,
            SocketID = remote_record.SocketID,
            DevBus = remote_record.DevBus,
            DevDevice = remote_record.DevDevice,
            DevFunction = remote_record.DevFunction,
            MultihostPresence = remote_record.MultihostPresence,
            RecoverFlag = 1
        })
        new_local_record:save()
        log:notice('[BizTopoLoader] Add PCIeLoadInfoBackup, Type=%s, SlotID=%s, DevBus=%s, MultiPresence=%s',
            new_local_record.Type, new_local_record.SlotID, new_local_record.DevBus,
            new_local_record.MultihostPresence)
    end

    -- 当前没有pcie卡信息就存一条无效数据
    if #remote_records == 0 then
        new_local_record = local_db_handle.PCIeLoadInfoBackup({
            Type = def.device_type.PCIE_CARD,
            SlotID = 0,
            ID = UINT32_MAX,
            AuxID = UINT32_MAX,
            Segment = 0,
            SocketID = 0,
            DevBus = UINT8_MAX,
            DevDevice = UINT8_MAX,
            DevFunction = UINT8_MAX,
            MultihostPresence = 0,
            RecoverFlag = 1
        })
        new_local_record:save()
        log:notice('[BizTopoLoader] Add PCIeLoadInfoBackup empty record.')
    end
end

-- 新增未持久化的记录
local function add_device_load_info(sys_id, tbl, new_pcie_load_info, per_load_info)
    for _, info in pairs(new_pcie_load_info) do
        if not info.completed then
            local record = tbl({
                Type = info.type,
                SlotID = info.slot_id,
                ID = info.id,
                AuxID = info.aux_id,
                Segment = info.bus_info.segment,
                SocketID = info.bus_info.socket_id,
                DevBus = info.bus_info.bus,
                DevDevice = info.bus_info.device,
                DevFunction = info.bus_info.func,
                MultihostPresence = 1 << (sys_id - 1)
            })
            record:save()
            per_load_info[record.SlotID] = record
            log:error('[BizTopoLoader] Add persistance PCIeLoadInfo, Type=%s, SlotID=%s, DevBus=%s, MultiPresence=%s',
                record.Type, record.SlotID, record.DevBus, record.MultihostPresence)
        end
    end
end

---@param new_pcie_load_info LoadInfo[] @单次上报的全量PCIe设备加载信息集合
function c_device_loader:device_load_info_persist(sys_id, type, new_pcie_load_info)
    local per_load_info = self.persistent_load_info[type]
    for _, record in pairs(per_load_info) do
        local info = new_pcie_load_info[record.SlotID]
        -- 刷新持久化记录
        if info then
            log:notice('[BizTopoLoader] Update persistance PCIeLoadInfo, Type=%s, SlotID=%s, DevBus=%s',
                record.Type, record.SlotID, record.DevBus)
            record.ID = info.id
            record.AuxID = info.aux_id
            record.Segment = info.bus_info.segment
            record.SocketID = info.bus_info.socket_id
            record.DevBus = info.bus_info.bus
            record.DevDevice = info.bus_info.device
            record.DevFunction = info.bus_info.func
            record.MultihostPresence = record.MultihostPresence | (1 << (sys_id - 1))
            record:save()
            info.completed = true
        else
            -- 对应host的在位设为0
            record.MultihostPresence = record.MultihostPresence & (~(1 << (sys_id - 1)))
            if record.MultihostPresence == 0 then
                -- 删除不存在的持久化记录
                log:notice('[BizTopoLoader] Delete persistance PCIeLoadInfo, Type=%s, SlotID=%s, DevBus=%s',
                    record.Type, record.SlotID, record.DevBus)
                local slot_id = record.SlotID
                record:delete()
                per_load_info[slot_id] = nil
            else
                record:save()
            end
        end
    end

    -- 新增未持久化的记录
    add_device_load_info(sys_id, self.db.PCIeLoadInfo, new_pcie_load_info, per_load_info)
end

-- 解析Bios上报的Device BDF信息
---@return LoadInfo[]
local function parse_pcie_card_bdf_data(data, device_type)
    log:notice('[BizTopoLoader] Parse Card BDF')
    if not data then
        log:error('[BizTopoLoader] The bus info data is in valid.')
    end

    local pcie_card_info = {}
    local index = 1

    -- nvme的SlotID是从0开始。
    if device_type == def.device_type.DISK then
        log:info('parse_pcie_card_bdf_data change index to 0.')
        index = 0
    end

    local segment, socket_id, bus, device, func, ok, info
    -- 5个字节为一组bdf信息
    for i = 1, 255, 5 do
        ok = pcall(function()
            segment, socket_id, bus, device, func = string.unpack('BBBBB', data, i)
        end)
        if not ok then
            break
        end
        -- 只取segmenet为0的
        if segment == 0 then
            info = c_load_info.new(device_type, index, nil, nil,
                {segment, socket_id, bus, device, func})
            pcie_card_info[index] = info
            if socket_id ~= 0xff or bus ~= 0xff or device ~= 0xff or func ~= 0xff then
                log:notice(
                    '[BizTopoLoader] type=%s, slot_id=%d - DeviceSSBDF=[0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x]',
                    info.type, index, segment, socket_id, bus, device, func)
            end
        end
        index = index + 1
    end
    return pcie_card_info
end

local function check_update_multi_presence(sys_id, addr, info)
    if not addr then
        log:error('[BizTopoLoader] check_update_multi_presence, no addr')
        return
    end
    local addr_multi_presence = addr:get_prop('MultihostPresence')
    if info and (info.id ~= UINT32_MAX or info.type == def.device_type.DISK) then
        -- 卡在位
        addr_multi_presence = addr_multi_presence | (1 << (sys_id - 1))
        addr:update_device_bdf(info.bus_info.bus, info.bus_info.device, info.bus_info.func)
    else
        -- 卡不在位
        addr_multi_presence = addr_multi_presence & (~(1 << (sys_id - 1)))
        if addr_multi_presence == 0 then
            -- 全部host不在位，就置为ff
            addr:update_device_bdf(info.bus_info.bus, info.bus_info.device, info.bus_info.func)
        end
    end
    addr:update_multihost_presence(addr_multi_presence)
    addr:update_vid_did(info.vid, info.did)
    return addr_multi_presence
end

-- 判断是否是retimer卡，通过判断reserve1是否为默认值实现，非默认值认为是retimer场景
function c_device_loader:is_retimer_device(pcie_info, sys_id)
    local tbl = self.reset_local_db.SystemsPCIeTopoInfo
    local db_handle = self.reset_local_db
    local topo_info = db_handle:select(tbl):where({
        GlobalSystemId = sys_id,
        LocalSystemId = 0,
        Segment = pcie_info.bus_info.segment,
        SecBus = pcie_info.bus_info.bus,
        SubBus = pcie_info.bus_info.bus
    }):first()

    if topo_info == nil then
        return false
    end

    if topo_info.Reserved1 ~= DEFAULT_RESERVERD1 then
        log:notice('[BizTopoLoader] Ignore JBOG PCIe device. sys_id=%s, BDF = %s:%s.%s',
            sys_id, pcie_info.bus_info.bus, pcie_info.bus_info.device, pcie_info.bus_info.func)
        return true
    end

    return false
end

function c_device_loader:load_device(sys_id, raw_data, device_type)
    log:notice('[BizTopoLoader] Bios %s BDF start. sys_id=%s', device_type, sys_id)
    if not raw_data then
        log:error('[BizTopoLoader] %s Raw data is invalid. sys_id=%s', device_type, sys_id)
        return nil
    end

    local data = raw_data:value()
    local pcie_card_info_total = parse_pcie_card_bdf_data(data, device_type)
    local new_pcie_load_info = {}
    local ok, ret, addr_multi_presence
    for _, info in pairs(pcie_card_info_total) do
        -- 支持不加载JBOG中PCIeSwitch下的PCIe设备
        if self:is_retimer_device(info, sys_id) then
            goto continue
        end

        -- 校验总线信息的合法性
        info.vid = UINT16_MAX
        info.did = UINT16_MAX
        info.sub_vid = UINT16_MAX
        info.sub_did = UINT16_MAX
        if (info.bus_info.bus == UINT8_MAX and info.bus_info.device == UINT8_MAX and
            info.bus_info.func == UINT8_MAX) or info.bus_info.bus == 0 then
            info.vid = UINT16_MAX -- 不做处理
        else
            query_id_from_pmu(sys_id, self.bus, info)
        end
        info.id = (info.vid << 16) + info.did
        info.aux_id = (info.sub_vid << 16) + info.sub_did

        -- 更新PCIeAddrInfo对象的DeviceBDF信息
        ok, ret = self.biz_topo:get_pcie_addr_info(info.type, info.slot_id)
        if ok then
            addr_multi_presence = check_update_multi_presence(sys_id, ret.addr, info)
            log:notice('[BizTopoLoader] addr_multi_presence=%s, sys_id=%s', addr_multi_presence, sys_id)
        end

        self:task_load_unload_device(info, addr_multi_presence, nil, device_type)

        if info.id ~= UINT32_MAX or device_type == def.device_type.DISK then
            new_pcie_load_info[info.slot_id] = info
        end
        ::continue::
    end
    -- 持久化加载信息
    self:device_load_info_persist(sys_id, device_type, new_pcie_load_info)
end

-- 响应BIOS上报的BDF信息
function c_device_loader:on_pcie_card_bdf_changed(values, path, interface)
    if not values or
        (not values['PcieCardBDF'] and not values['OCPCardBDF'] and not values['PcieDiskBDF']) then
        return
    end
    local sys_id = tonumber(string.match(path or '', '/bmc/kepler/Systems/(%d+)/Bios'))
    log:notice('[BizTopoLoader] Bios BDF changed. sys_id=%s', sys_id)

    local raw_datas = {
        [def.device_type.DISK] = values['PcieDiskBDF'],
        [def.device_type.PCIE_CARD] = values['PcieCardBDF'],
        [def.device_type.OCP_CARD] = values['OCPCardBDF']
    }
    local ok, ret
    for device_type, raw_data in pairs(raw_datas) do
        ok, ret = pcall(function()
            return self:load_device(sys_id, raw_data, device_type)
        end)
        if not ok then
            log:error("[BizTopoLoader] load_device error, ret:%s, sys_id:%s", ret, sys_id)
        end
    end
end

function c_device_loader:ocp_get_sub_bdf(pcie_addr_info)
    local sbdf = {
        socket = pcie_addr_info:get_prop('SocketID'),
        bus = pcie_addr_info:get_prop('Bus'),
        device = pcie_addr_info:get_prop('Device'),
        func = pcie_addr_info:get_prop('Function')
    }
    return get_topo_device_bdf(1, self.bus, sbdf)
end

local mmio_array = bs.new('<<base:8/unit:8, limit:8/unit:8>>')

local MultiHostPCIeTopoInfo <const> = '<<global_system_id:1/unit:8, local_system_id:1/unit:8, ' ..
    'status:1/unit:8, segment:1/unit:8, socket:1/unit:8, ' ..
    'bus:1/unit:8, device:1/unit:8, func:1/unit:8, group:1/unit:8, component:1/unit:8, slot:1/unit:8, '..
    'vendor_id:2/little-unit:8, dev_id:2/little-unit:8, '..
    'sec_bus:1/unit:8, sub_bus:1/unit:8, bdf_no:1/unit:8, resv1:1/unit:8, '..
    'low_base:4/little-unit:8, low_limit:4/little-unit:8, high_base:8/little-unit:8, high_limit:8/little-unit:8, '..
    'resv2:2/little-unit:8, resv4:4/little-unit:8, update_flag:1/unit:8, mmio_cnt:1/unit:8, '..
    'mmio_range:6/MMIO_ARRAY>>'

local function ocp_gen_systems_pcie_topo_info_ipmi_data(property)
    local mmio_range = {}

    mmio_range[1] = {
        base = property.MMIO0Base,
        limit = property.MMIO0Limit
    }
    mmio_range[2] = {
        base = property.MMIO1Base,
        limit = property.MMIO1Limit
    }
    mmio_range[3] = {
        base = property.MMIO2Base,
        limit = property.MMIO2Limit
    }
    mmio_range[4] = {
        base = property.MMIO3Base,
        limit = property.MMIO3Limit
    }
    mmio_range[5] = {
        base = property.MMIO4Base,
        limit = property.MMIO4Limit
    }
    mmio_range[6] = {
        base = property.MMIO5Base,
        limit = property.MMIO5Limit
    }

    local cfg = {
        global_system_id = property.GlobalSystemId,
        local_system_id = property.LocalSystemId,
        status = 2,
        segment = property.Segment,
        socket = property.Socket,
        bus = property.Bus,
        device = property.Device,
        func = property.Function,
        group = property.GroupId,
        component = property.ComponentType,
        slot = property.Slot,
        vendor_id = property.VendorId,
        dev_id = property.DeviceId,
        sec_bus = property.SecBus,
        sub_bus = property.SubBus,
        bdf_no = property.BDFNo,
        resv1 = property.Reserved1,
        low_base = property.CfgLowBase,
        low_limit = property.CfgLowLimit,
        high_base = property.CfgHighBase,
        high_limit = property.CfgHighLimit,
        resv2 = property.Reserved2,
        resv4 = property.Reserved4,
        update_flag = property.UpdateFlag,
        mmio_cnt = property.MMIOCnt,
        mmio_range = mmio_range
    }

    return bs.new(MultiHostPCIeTopoInfo, {MMIO_ARRAY = mmio_array}):pack(cfg)
end

function c_device_loader:persist_ocp_systems_pcie_topo_info(info)
    -- 先判断是否有数据内容
    local tbl = self.reset_local_db.SystemsPCIeTopoInfo
    local db_handle = self.reset_local_db
    local topo_info = db_handle:select(tbl):where({
        LocalSystemId = 0,
        Segment = info.bus_info.segment,
        Bus = info.bus_info.bus,
        Device = info.bus_info.device,
        Function = info.bus_info.func
    }):first()
    -- 如果有数据库且Vid Did一致不用继续操作
    if topo_info ~= nil and topo_info.VendorId == info.vendor_id and topo_info.DeviceId == info.device_id then
        return
    end

    -- 没有数据库或Vid Did不一致的情况，新建数据库内容
    local property = tbl({GlobalSystemId = 1, LocalSystemId = 0,
        Segment = info.bus_info.segment, Socket = info.bus_info.socket_id,
        Bus = info.bus_info.bus, Device = info.bus_info.device, Function = info.bus_info.func})

    property.VendorId = info.vendor_id
    property.DeviceId = info.device_id
    -- 以下几个值默认不为0，参考带内数据构造
    property.GroupId = property.GroupId == 0 and 1 or property.GroupId
    property.ComponentType = property.ComponentType == 0 and 255 or property.ComponentType
    property.Slot = property.Slot == 0 and 255 or property.Slot
    property:save()

    -- 向sdk发送ipmi
    local data = ocp_gen_systems_pcie_topo_info_ipmi_data(property)
    local req = {
        ManufactureId = 0xdb0700,
        SubCommand = 0x17,
        ActionType = 0x14,
        Length = 0,
        Data = data
    }

    local context = cjson.encode({ChanType = channel_type.CT_ME:value(), Instance = 0})

    local pay_load = bs.new(app_ipmi.SetFDMInfoMultiSystemPCIeAddrInfo.decode):pack(req)
    
    for retry_times = 1, 3 do
        local err, processor_resp = self.bus:pcall(FAULT_DIAGNOSIS_SERVICE, IPMI_SET_MULTI_SYSTEM_PCIE_ADDR_INFO,
            INTERFACE_CMD_INFO, 'Process', 'a{ss}ayay', ctx.get_context(), pay_load, context)
        if err or not processor_resp then
            log:info('[DeviceLoador] Process failed err info: %s, retry times = %s', err, retry_times)
        else
            -- 只在OCP卡从不在位到在位，且持久化数据出现改变时打印一次
            log:notice('[DeviceLoador] Supplement alarm database list, BDF = %s:%s.%s, vid = %s, did = %s',
                info.bus_info.bus, info.bus_info.device, info.bus_info.func, info.vendor_id, info.device_id)
            return
        end
    end
end

-- 处理PCIe信息的公共逻辑
function c_device_loader:process_pci_info(connector, is_load)
    local type = def.com_type_to_type[connector.ref_pcie_addr_info:get_prop('ComponentType')]
    -- 先获取槽位号，判断该槽位上是否有插过卡，若没有则去查询IMU
    local slot_id = connector.ref_pcie_addr_info:get_prop('SlotID')
    local sbdf
    if not self.persistent_load_info[type] or not self.persistent_load_info[type][slot_id] then
        -- OCP卡情况，试图从IMU捞取OCP卡信息
        sbdf = self:ocp_get_sub_bdf(connector.ref_pcie_addr_info)
        if not sbdf then
            log:info('[BizTopoLoader] persistent_load_info empty for ocp card hot plug.')
            return
        else -- 获取到ocp设备时更新pcie_addr_info
            connector.ref_pcie_addr_info:set_prop('DevBus', sbdf.bus)
            connector.ref_pcie_addr_info:set_prop('DevDevice', sbdf.device)
            connector.ref_pcie_addr_info:set_prop('DevFunction', sbdf.func)
        end
    else
        sbdf = {
            socket = connector.ref_pcie_addr_info:get_prop('SocketID'),
            bus = connector.ref_pcie_addr_info:get_prop('DevBus'),
            device = connector.ref_pcie_addr_info:get_prop('DevDevice'),
            func = connector.ref_pcie_addr_info:get_prop('DevFunction')
        }
    end

    -- 一次性获取所有PCIe属性
    local socket_id = connector.ref_pcie_addr_info:get_prop('SocketID')
    local bus = connector.ref_pcie_addr_info:get_prop('DevBus')
    local device = connector.ref_pcie_addr_info:get_prop('DevDevice')
    local fun = connector.ref_pcie_addr_info:get_prop('DevFunction')
    local segment = connector.ref_pcie_addr_info:get_prop('Segment')
    local component_type = connector.ref_pcie_addr_info:get_prop('ComponentType')
    local hot_plug_filter_key = string.format("%s_%s", slot_id, component_type)
    if self.hot_plug_filter[hot_plug_filter_key] then
        -- 已经在处理的卡不重复处理。
        log:debug('[BizTopoLoader] hot_plug_filter skip, slot_id=%s, type=%s', slot_id, type)
        return
    end
    self.hot_plug_filter[hot_plug_filter_key] = true

    local info = c_load_info.new(def.device_type.OCP_CARD, slot_id, UINT32_MAX, UINT32_MAX,
        {segment, socket_id, bus, device, fun})
    info.vid = UINT16_MAX
    info.did = UINT16_MAX
    info.sub_vid = UINT16_MAX
    info.sub_did = UINT16_MAX

    if is_load then -- 加载场景，需要查询四元组信息
        log:debug('[process_pci_info] OCP card load, query vid/did, slot_id=%s', slot_id)
        if (info.bus_info.bus == UINT8_MAX and info.bus_info.device == UINT8_MAX and
            info.bus_info.func == UINT8_MAX) or info.bus_info.bus == 0 then
            info.vid = UINT16_MAX -- 不做处理
        else
            query_id_from_pmu(1, self.bus, info)
            -- 持久化SystemsPCIeTopoInfo数据
            self:persist_ocp_systems_pcie_topo_info(info)
        end
        info.id = (info.vid << 16) + info.did
        info.aux_id = (info.sub_vid << 16) + info.sub_did
    end

    local addr_multi_presence = check_update_multi_presence(1, connector.ref_pcie_addr_info, info)
    self:task_load_unload_device(info, addr_multi_presence, nil, def.device_type.OCP_CARD)
    -- 持久化数据操作，更新查到的四元组信息
    self:device_load_info_persist_ocp(1, def.device_type.OCP_CARD, slot_id, info)
end

function c_device_loader:device_load_info_persist_ocp(sys_id, type, slot_id, info)
    local per_load_info = self.persistent_load_info[type]
    local record_match
    for _, record in pairs(per_load_info) do
        -- 刷新持久化记录
        if slot_id == record.SlotID and (record.ID ~= info.id or record.AuxID ~= info.aux_id) then
            log:notice('[BizTopoLoader] Update persistance PCIeLoadInfo, Type=%s, SlotID=%s, DevBus=%s, record=%s-%s, info=%s-%s',
                record.Type, record.SlotID, record.DevBus, record.ID, record.AuxID, info.id, info.aux_id)
            record.ID = info.id
            record.AuxID = info.aux_id
            record.MultihostPresence = record.MultihostPresence | (1 << (sys_id - 1))
            record:save()
            record_match = true
        end
    end
    -- 对应AC时环境上没有OCP卡，之后热插入卡，如果不新增数据库，若OS下电，再重启BMC，卡会不加载
    if not record_match then
        local tbl = self.db.PCIeLoadInfo
        local record = tbl({
            Type = info.type,
            SlotID = info.slot_id,
            ID = info.id,
            AuxID = info.aux_id,
            Segment = info.bus_info.segment,
            SocketID = info.bus_info.socket_id,
            DevBus = info.bus_info.bus,
            DevDevice = info.bus_info.device,
            DevFunction = info.bus_info.func,
            MultihostPresence = 1 << (sys_id - 1)
        })
        record:save()
        self.persistent_load_info[type][slot_id] = record
    end
end

function c_device_loader:ocp_load_from_tc()
    local ok, err_info = pcall(function()
        -- 获取所有下行连接器BusinessConnector
        local ocp_biz_connectors = self.biz_topo:get_objs('PCIeBizConnector')
        local tc_connector, mgmt_connector
        for _, connector in pairs(ocp_biz_connectors) do
            -- 检查是否为OCP设备
            if not connector.ref_pcie_addr_info or 
                connector.ref_pcie_addr_info:get_prop('ComponentType') ~= def.com_type.OCP_CARD then
                goto continue
            end
            tc_connector = connector.ref_mgmt_connector_tianchi
            mgmt_connector = connector.ref_mgmt_connector
            if not tc_connector or not tc_connector.Presence then
                log:debug("Failed to get ref_mgmt_connector_tianchi for: %s %s", connector.position, connector.mds_obj.Name)
                goto continue
            end
            if not mgmt_connector or not mgmt_connector.Presence then
                log:debug("Failed to get mgmt_connector for: %s %s", connector.position, connector.mds_obj.Name)
                goto continue
            end
            -- 判断加载/卸载逻辑
            log:info('tc connector Presence = %s, mgmt connector Presence = %s', tc_connector.Presence, mgmt_connector.Presence)
            if tc_connector.Presence == 1 and mgmt_connector.Presence == 0 then
                self.hot_plug_filter = {} -- 每次热插拔清空过滤器
                log:debug('OCP card load process triggered.')
                self:process_pci_info(connector, true)  -- true 表示加载
            elseif tc_connector.Presence == 0 and mgmt_connector.Presence == 1 then
                self.hot_plug_filter = {}
                log:debug('OCP card unload process triggered.')
                self:process_pci_info(connector, false)  -- false 表示卸载
            end
            ::continue::
        end
    end)
    if not ok then
        log:error('ocp_load_from_tc failed, err: %s', err_info)
        return
    end
end

-- 通过报送的slotid和ssbdf映射表加载pcie设备
function c_device_loader:set_device_present_by_map(sys_id, device_type, map_slot_id_to_ssbdf)
    local ok, ret, addr_multi_presence
    local new_pcie_load_info = {}
    log:notice('[BizTopoLoader] Bios BDF report.device_type=%s, sys_id=%s', device_type, sys_id)
    for slot_id, ssbdf in pairs(map_slot_id_to_ssbdf) do
        ok, ret = pcall(function()
            local info = c_load_info.new(device_type, slot_id, nil, nil, ssbdf)
            info.vid = UINT16_MAX
            info.did = UINT16_MAX
            info.sub_vid = UINT16_MAX
            info.sub_did = UINT16_MAX

            query_id_from_pmu(sys_id, self.bus, info)

            info.id = (info.vid << 16) + info.did
            info.aux_id = (info.sub_vid << 16) + info.sub_did

            -- 更新PCIeAddrInfo对象的DeviceBDF信息
            local get_addr_ok, addr_ret = self.biz_topo:get_pcie_addr_info(info.type, info.slot_id)

            if get_addr_ok then
                addr_multi_presence = check_update_multi_presence(sys_id, addr_ret.addr, info) 
                log:notice('[BizTopoLoader] addr_multi_presence=%s, sys_id=%s', addr_multi_presence, sys_id)
            end

            self:task_load_unload_device(info, addr_multi_presence, nil, device_type)
            if info.id ~= UINT32_MAX or device_type == def.device_type.DISK then
                new_pcie_load_info[info.slot_id] = info
            end
        end)
        if not ok then
            log:error("[BizTopoLoader] load_device error, ret:%s, sys_id:%s", ret, sys_id)
        end
    end
    -- 持久化加载信息
    self:device_load_info_persist(sys_id, device_type, new_pcie_load_info)
end

function c_device_loader:ctor(bus, db, biz_topo, reset_local_db)
    self.bus = bus
    self.db = db
    self.reset_local_db = reset_local_db
    self.biz_topo = biz_topo
    self.persistent_load_info = {}
end

function c_device_loader:main()
    self:pcie_load_info_init()
end

-- Test
c_device_loader.parse_pcie_card_bdf_data = parse_pcie_card_bdf_data
c_device_loader.get_pcie_vid_did_info = get_pcie_vid_did_info
c_device_loader.unload_device_connector = unload_device_connector
c_device_loader.get_topo_device_bdf = get_topo_device_bdf

return c_device_loader
