-- 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 org_freedesktop_dbus = require 'sd_bus.org_freedesktop_dbus'
local log = require 'mc.logging'
local class = require 'mc.class'
local singleton = require 'mc.singleton'
local c_biz_topo = require 'biz_topo.biz_topo'
local c_topo_monitor = require 'biz_topo.topo_monitor'
local c_device_loader = require 'biz_topo.device_loader'
local cmn = require 'common'
local signal_mgr = require 'biz_topo.signal_mgr'
local ctx = require 'mc.context'
local ipmi_error = require 'pcie_device.errors'
local def = require 'biz_topo.def'
local app_ipmi = require 'pcie_device.ipmi.ipmi'
local bs = require 'mc.bitstring'
local signal = require 'mc.signal'
local c_topo_reader = require 'biz_topo.topo_reader'
local client = require 'pcie_device.client'
local skynet_queue = require 'skynet.queue'
local mdb = require 'mc.mdb'
local c_device_service = require 'device.device_service'

---@class BizTopoService @业务Topo服务
---@field private biz_topo BizTopo
local c_biz_topo_service = class()

local MAX_CPU_SOCKET_COUNT<const> = 32
local IPMI_TYPE_TRANS_DEV_TYPE<const> = {
    [cmn.ipmi_type.IPMI_PCIE_CARD_TYPE] = def.device_type.PCIE_CARD,
    [cmn.ipmi_type.IPMI_OCP_CARD_TYPE] = def.device_type.OCP_CARD
}
local INTERFACE_BIOS<const> = "bmc.kepler.Systems.Bios"

function c_biz_topo_service:on_add_object(class_name, object, position)
    self.biz_topo:on_add_object(class_name, object, position,
        self.on_add_object_complete_signal, self.bus)
end

function c_biz_topo_service:on_delete_object(class_name, object, position)
    self.biz_topo:on_delete_object(class_name, object, position)
end

function c_biz_topo_service:update_bcu_src_conn_ready()
    cmn.skynet.fork(function()
        local retry = 30 -- 如果psr分发完毕后30秒没有serdes分发，就认为是没有配serdes的机型
        while retry > 0 do
            if next(self.biz_topo.src_biz_connector_container) then
                return -- 有serdes不需要额外更新
            end
            retry = retry - 1
            cmn.skynet.sleep(100)
        end
        self.bcu_src_conn_ready = true
        self.signal_mgr:get('pcie_container_complete'):emit()
        self.signal_mgr:get('src_biz_connector_complete'):emit()
    end)
end

function c_biz_topo_service:on_add_object_complete(position)
    self.on_add_object_complete_signal:emit(position)
    -- PSR加载完成
    if self.biz_topo.psr_position == position then
        log:notice("[BizTopoService] psr_add_complete")
        self.biz_topo.psr_add_complete = true
        self:update_bcu_src_conn_ready()
    end

    -- UbcConfig分发完成
    if self.biz_topo.ubc_config_position == position then
        log:notice("[BizTopoService] ubc_config_add_complete")
        self.biz_topo.ubc_config_add_complete = true
    end
    --源端业务连接器所在组分发完成
    if self.biz_topo.src_biz_connector_container[position] then
        self.biz_topo.src_biz_connector_container[position] = def.ON_ADD_STATUS["ON_ADD_COMPLETE"]
        local item = 0
        for _, v in pairs(self.biz_topo.src_biz_connector_container) do
            if v ~= def.ON_ADD_STATUS["ON_ADD_COMPLETE"] then
                return
            end
            item = item + 1
        end
        --需要BusinessTopoNode对象管理的所有基础板源端业务连接器都分发完全
        if item ~= self.biz_topo.monitor_bcu_num then
           return 
        end
        self.bcu_src_conn_ready = true
    end

    if self.biz_topo.ubc_config_add_complete and self.biz_topo.psr_add_complete and
        self.bcu_src_conn_ready then
        self.signal_mgr:get('pcie_container_complete'):emit()
        self.signal_mgr:get('src_biz_connector_complete'):emit()
    end

    -- PCIe业务连接器所在组件分发完成
    if self.biz_topo.pcie_container[position] then
        self.signal_mgr:get('pcie_container_complete'):listen(position)
        self.biz_topo.pcie_container[position] = nil
    end

    self:create_topo_monitor(position)
    self:monitor_ocp_presence()
end

function c_biz_topo_service:create_topo_monitor(position)
    local biz_topo_nodes = self.biz_topo.biz_topo_node_list
    local src_biz_connectors = self.biz_topo.biz_connector_list
    local unit_configurations = self.biz_topo.unit_config_list
    local psr_position, bcu_position
    for _, biz_topo_node in ipairs(biz_topo_nodes) do
        if not src_biz_connectors or not next(src_biz_connectors) or not unit_configurations or
            not next(unit_configurations) then
                log:notice(
                    '[BizTopoService] Object not ready cannot create topo monitor, biz_topo_nodes_length=%s' ..
                        ' src_biz_connector_length=%s unit_configurations_length=%s position=%s',
                    biz_topo_nodes and #biz_topo_nodes or 'nil',
                    src_biz_connectors and #src_biz_connectors or 'nil',
                    unit_configurations and #unit_configurations or 'nil', position)
            return
        end

        bcu_position = biz_topo_node.position
        psr_position = self.biz_topo.psr_position
        log:debug("[BizTopoService] start topo monitor, psr_position: %s, bcu_postion: %s",
                psr_position, bcu_position)
        -- 一个psr对应多个BCU
        if not psr_position or (self.topo_monitors[psr_position] and
            self.topo_monitors[psr_position][bcu_position]) then
            log:debug("[BizTopoService] cannot create topo monitor, psr_position: %s, bcu_postion: %s",
                psr_position, bcu_position)
            goto next
        end

        local topo_monitor = c_topo_monitor.new(self.bus, biz_topo_node, src_biz_connectors, unit_configurations,
            self.biz_topo.pcie_addr_info_list)
        log:notice("[BizTopoService] create topo monitor, psr_position: %s, bcu_postion: %s",
                psr_position, bcu_position)
        
        cmn.skynet.fork(function ()
            topo_monitor:main()
        end)
        
        if not self.topo_monitors[psr_position] then
            self.topo_monitors[psr_position] = {}
        end
        self.topo_monitors[psr_position][bcu_position] = topo_monitor
        ::next::
    end
end

local function get_global_bdf(db, socket_id)
    if socket_id >= MAX_CPU_SOCKET_COUNT then
        return false, 0
    end
    local bus_table = db.CpuBusInfo
    local property = bus_table({SocketID = socket_id})
    if property.BusBaseAddr == nil or property.BusSize == nil then
        return false, 0
    end
    log:info('cpu=0x%x sys_bus=0x%x', socket_id, property.BusBaseAddr)
    return true, property.BusBaseAddr
end

function c_biz_topo_service:update_pcie_addr_obj_bus_arm(obj)
    local controller_type = obj.mds_obj.ControllerType
    -- 仅更新PCIeCore类型控制器直出的PCIe设备对应的PCIeAddrInfo类对象
    if controller_type ~= def.controller_type.PCIE then
        return
    end
    local socket_id = obj.mds_obj.SocketID
    local ret, sys_bus = get_global_bdf(self.db, socket_id)
    if ret ~= true then
        log:error('get obj Bus failed, socket:%u, ret:%d, ', socket_id, ret)
        return
    end

    obj.mds_obj.Bus = sys_bus
    log:debug('obj(%s):socket:%u, bus:%u', obj.mds_obj.Name, socket_id, sys_bus)
end

local function set_bus_size_of_each_cpu(req, db, count)
    local bus_table = db.CpuBusInfo
    local property = nil
    -- 新IPMI 命令字，上报CPU基地址 + Size. 消息长度为CPU个数 * 2 + 1(CPU个数占1个字节)
    for i = 1, count, 1 do
        property = bus_table({SocketID = i - 1})
        property.BusBaseAddr = (req.BusBaseAddr >> ((i - 1) * 8)) & 0xff
        property.BusSize = (req.BusSize >> ((i - 1) * 8)) & 0xff
        property:save()
    end
end

function c_biz_topo_service:set_cpu_arrayed_bus_size(req, data_len, count)
    -- 当前新天池架构不存在老IPMI格式
    if data_len ~= 2 * count + 1 then
        return false
    end
    set_bus_size_of_each_cpu(req, self.db, count)
    -- 更新PcieAddrInfo对象的Bus号
    for _, obj in ipairs(self.biz_topo.pcie_addr_info_list) do
        self:update_pcie_addr_obj_bus_arm(obj)
    end
    log:running(log.RLOG_INFO, 'Update cpu bus info successfully')
    return true
end

function c_biz_topo_service:set_cpu_bus_size(req, context)
    if req then
        return 0x00, req.ManuId
    end

    if not req or req.Tail == nil or req.Tail == '' then
        log:error('enalbe/disable operation failed')
        error(ipmi_error.invalid_command(context.ChanType, app_ipmi.SetCpuBusSize.netfn,
            app_ipmi.SetCpuBusSize.cmd))
    end

    local val = bs.new('<<Count:1/unit:8, Tail/string>>'):unpack(req.Tail)
    if val == nil then
        log:error('bitstring ipmi failed')
        error(ipmi_error.invalid_field(context.ChanType, app_ipmi.SetCpuBusSize.netfn,
            app_ipmi.SetCpuBusSize.cmd))
    end
    local count = val.Count -- 第一字节表示上报CPU个数

    local bs_bus_info = string.format(
        '<<Count:1/unit:8, BusBaseAddr:%d/unit:8, BusSize:%d/unit:8, tail/binary>>', count, count)
    local val_info = bs.new(bs_bus_info):unpack(req.Tail)
    if val_info == nil then
        log:error('bitstring ipmi failed')
        error(ipmi_error.invalid_field(context.ChanType, app_ipmi.SetCpuBusSize.netfn,
            app_ipmi.SetCpuBusSize.cmd))
    end

    local data_len = req.DataLen
    if data_len <= 1 then
        log:running(log.RLOG_INFO, 'Update cpu bus info failed')
        log:error('set cpu bus number failed. cpu_count(%u), data_len(%u).', count, data_len)
        error(ipmi_error.invalid_length(context.ChanType, app_ipmi.SetCpuBusSize.netfn,
            app_ipmi.SetCpuBusSize.cmd))
    end
    if count == 0 then
        log:running(log.RLOG_INFO, 'Update cpu bus info failed')
        log:error('set cpu bus number failed. cpu_count(%u), data_len(%u).', count, data_len)
        error(ipmi_error.invalid_field(context.ChanType, app_ipmi.SetCpuBusSize.netfn,
            app_ipmi.SetCpuBusSize.cmd))
    end
    local ret = self:set_cpu_arrayed_bus_size(val_info, data_len, count)
    if ret ~= true then
        log:running(log.RLOG_INFO, 'Update cpu bus info failed')
        log:error('set cpu bus number failed. cpu_count(%u), data_len(%u).', count, data_len)
        error(ipmi_error.invalid_length(context.ChanType, app_ipmi.SetCpuBusSize.netfn,
            app_ipmi.SetCpuBusSize.cmd))
    end
    return 0x00, req.ManuId
end

local function update_integrated_pcie_addr_object(obj, config)
    -- 更新PCIe控制器的BDF信息
    if config.controller_type ~= 0 then
        return
    end
    local socket = obj.mds_obj.SocketID
    local type = obj.mds_obj.ControllerType
    local index = obj.mds_obj.ControllerIndex

    -- controller_type: PCIe控制器类型，0:PCIeCore，1:NIC，2:SAS，3:SATA，4:ZIP，5:SEC
    -- controller_index: PCIe控制器索引值，CPU内部相同ControllerType的个数索引，从0开始
    if socket == config.socket and type == config.controller_type and index ==
        config.controller_index then
        log:notice('[BizTopoService] update %s(soket=%u,type=%u,index=%u) property:bus=%u',
            obj.mds_obj.name, socket, type, index, config.bus)
        obj.mds_obj.Bus = config.bus
    end
end

local function update_bdf_cfgs(biz_topo, config)
    local obj = biz_topo.bdfconfigs[config.socket]
    if not obj then
        log:error('[BizTopoService] Socket%s, bdfconfig obj not exist.', config.socket)
        return false
    end
    local new_cfgs, cfg = {}
    local update_flag = false
    for k, v in pairs(obj.mds_obj.BDFConfigs) do
        cfg = obj:config_new(table.unpack(v))
        if cfg.ControllerIndex == config.controller_index and cfg.Type == config.controller_type then
            table.insert(new_cfgs, {cfg.PortId, cfg.ControllerIndex, cfg.Segment, config.bus, cfg.Device,
                cfg.Function, cfg.Type})
            if config.bus ~= cfg.Bus then
                update_flag = true
            end
        else
            table.insert(new_cfgs, {cfg.PortId, cfg.ControllerIndex, cfg.Segment, cfg.Bus, cfg.Device,
                cfg.Function, cfg.Type})
        end
    end
    -- 上报的BDF相比本地存在变化才更新
    if update_flag then
        obj.mds_obj.BDFConfigs = new_cfgs
        biz_topo:init_bdf_map(obj)
    end
    return update_flag
end

function c_biz_topo_service:update_pcie_addr_all(req, socket, count)
    local PcieAddrInfo = bs.new('<<ControllerType:1/unit:8, ControllerIndex:1/unit:8, ' ..
                                    'Bus:1/unit:8, Device:1/unit:8, Function:1/unit:8>>')
    local pcie_controller_info = string.format('<<PcieController:%d/PcieAddrInfo, tail/binary>>',
        count)
    local val = bs.new(pcie_controller_info, {PcieAddrInfo = PcieAddrInfo}):unpack(req)
    if val == nil then
        log:error('bitstring ipmi failed')
        return
    end
    local config, controller
    for i = 1, count do
        controller = count == 1 and val.PcieController or val.PcieController[i]
        config = {
            controller_type = controller.ControllerType,
            controller_index = controller.ControllerIndex,
            bus = controller.Bus,
            device = controller.Device,
            func = controller.Function,
            socket = socket
        }
        if not update_bdf_cfgs(self.biz_topo, config) then
            goto next
        end
        for _, obj in ipairs(self.biz_topo.pcie_addr_info_list) do
            update_integrated_pcie_addr_object(obj, config)
        end
        ::next::
    end
end

function c_biz_topo_service:update_integrated_pcie_config(req, context)
    if req.Tail == nil or req.Tail == '' then
        log:error('enalbe/disable operation failed')
        error(ipmi_error.invalid_command(context.ChanType,
            app_ipmi.UpdateIntegratedPcieConfig.netfn, app_ipmi.UpdateIntegratedPcieConfig.cmd))
    end
    local val = bs.new('<<Socket:1/unit:8, Count:1/unit:8, Tail/string>>'):unpack(req.Tail)
    if val == nil then
        log:error('bitstring ipmi failed')
        error(ipmi_error.invalid_length(context.ChanType, app_ipmi.UpdateIntegratedPcieConfig.netfn,
            app_ipmi.UpdateIntegratedPcieConfig.cmd))
    end
    local data_len = req.DataLen

    if req.Tail and #req.Tail ~= data_len then
        log:error('invalid data_len=%s, req.Tail len=%s', data_len, #req.Tail)
        error(ipmi_error.invalid_length(context.ChanType, app_ipmi.UpdateIntegratedPcieConfig.netfn,
            app_ipmi.UpdateIntegratedPcieConfig.cmd))
    end

    local count = val.Count
    -- 每组控制器信息5字节, 控制器组之前两个字节为socket与controller type
    if data_len < count * 5 + 2 then
        log:error('invalid data_len = %u,contrller_cnt = %u', data_len, count)
        error(ipmi_error.invalid_length(context.ChanType, app_ipmi.UpdateIntegratedPcieConfig.netfn,
            app_ipmi.UpdateIntegratedPcieConfig.cmd))
    end
    cmn.skynet.fork(function ()
        self:update_pcie_addr_all(val.Tail, val.Socket, count)
    end)

    return 0x00, req.ManuId
end

-- 根据PCIeAddrInfo对象找到关联的target业务连接器
function c_biz_topo_service:method_get_target_conn_by_addr_obj(addr_obj)
    return self.biz_topo:get_target_conn_by_addr_obj(addr_obj)
end

-- 根据PCIe设备的position查询父级CSR中对应的PCIeAddrInfo对象
function c_biz_topo_service:method_get_pcie_addr_by_pos(pos)
    return self.biz_topo:get_pcie_addr_by_pos(pos)
end

-- 根据PCIe设备的position查询父级CSR中对应的下行连接器的属性
function c_biz_topo_service:method_get_pcie_type_by_pos(pos)
    return self.biz_topo:get_pcie_type_by_pos(pos)
end

function c_biz_topo_service:get_device_present_status(req)
    if not IPMI_TYPE_TRANS_DEV_TYPE[req.DeviceType] then
        log:error('[BizTopoService] ipmi get pcie device present status failed, ' ..
            'invaild type=%s, only apply pcie_card=%s or ocp_card=%s',
            req.DeviceType, cmn.ipmi_type.IPMI_PCIE_CARD_TYPE, cmn.ipmi_type.IPMI_OCP_CARD_TYPE)
        return nil
    end

    local pcie_device_present = 0
    local slot_bit = 0
    local present_device_list = self.device_loader.persistent_load_info[IPMI_TYPE_TRANS_DEV_TYPE[req.DeviceType]]
    for slot_id, _ in pairs(present_device_list) do
        slot_bit = 1 << (slot_id - 1)
        pcie_device_present = pcie_device_present | slot_bit
    end

    return pcie_device_present
end

function c_biz_topo_service:ctor(bus, service_manager, db, reset_local_db)
    self.bus = bus
    self.db = db
    self.service_manager = service_manager
    self.listening = {} -- 信号监听列表
    -- 业务拓扑对象管理
    self.biz_topo = c_biz_topo.new(bus, db)
    -- PCIe设备加载
    self.device_loader = c_device_loader.new(bus, db, self.biz_topo, reset_local_db)
    -- 拓扑检测（多个）
    self.topo_monitors = {}
    -- 信号管理
    self.signal_mgr = signal_mgr.new()
    self.bcu_src_conn_ready = false
    -- 上面的信号机制废弃，使用libmc4lua的signal
    self.on_add_object_complete_signal = signal.new()
    self.monitor_ocp_task = false
end

function c_biz_topo_service:init()
    self.service_manager:register('biz_topo', self)

    -- 信号：监听Bios的属性变化
    self:listen_bios_path()

    -- 信号：监听PCIe业务连接器所在组件分发完成
    self.signal_mgr:register('pcie_container_complete')
    self.signal_mgr:get('pcie_container_complete'):on(function(position)
        self.biz_topo:main()
        self.device_loader:load_device_by_per_info(position)
    end)

    -- 信号：监听计算组件上的业务连接器对象分发完成
    self.signal_mgr:register('src_biz_connector_complete')
    self.signal_mgr:get('src_biz_connector_complete'):on(function(position)
    end)
end

-- 监听bios资源树对象
function c_biz_topo_service:listen_bios_path()
    -- 注册响应函数
    client:OnBiosPropertiesChanged(function(values, path, interface)
        self.device_loader:on_pcie_card_bdf_changed(values, path, interface)
    end)
end

function c_biz_topo_service:get_exu_smc()
    local retry = 3
    local ok, smc
    while retry >= 0 do
        ok, smc = pcall(mdb.get_object, self.bus, '/bmc/kepler/Chip/Smc/Smc_ExpBoardSMC_0101',
            'bmc.kepler.Chip.BlockIO')
        if ok then
            break
        end
        retry = retry - 1
        cmn.skynet.sleep(500)
    end

    if not ok or not smc then
        log:error('[BizTopo]Unable to monitor ocp presence. Cannot get ExpBoardSMC')
        return
    end
    return smc
end

function c_biz_topo_service:unload_ocp(ocp_1_presence, ocp_2_presence)
    local pcie_device_list = c_device_service.get_instance().pcie_device_list
    local ocp_1_raw = string.pack("BBBBB", 0, 255, 255, 255, 255)
    local ocp_2_raw = string.pack("BBBBB", 0, 255, 255, 255, 255)
    local need_unload = false
    for _, obj in pairs(pcie_device_list) do
        if obj:get_prop('DeviceType') ~= def.com_type.OCP_CARD then
            goto next 
        end
        if obj:get_prop('SlotID') == 1 then
            if ocp_1_presence == 0 then
                need_unload = true
            else
                ocp_1_raw = string.pack("BBBBB", obj:get_prop('Segment'), obj:get_prop('SocketID'),
                    obj:get_prop('DevBus'), obj:get_prop('DevDevice'), obj:get_prop('DevFunction'))
            end
        end
        if obj:get_prop('SlotID') == 2 then
            if ocp_2_presence == 0 then
                need_unload = true
            else
                ocp_2_raw = string.pack("BBBBB", obj:get_prop('Segment'), obj:get_prop('SocketID'),
                    obj:get_prop('DevBus'), obj:get_prop('DevDevice'), obj:get_prop('DevFunction'))
            end
        end
        ::next::
    end
    if not need_unload then
        log:debug("[BizTopo] need_unload")
        return
    end
    local raw_data = {
        value = function()
            return ocp_1_raw .. ocp_2_raw
        end
    } 
    local ok, ret = pcall(function()
        return self.device_loader:load_device(1, raw_data, def.device_type.OCP_CARD)
    end)
    if not ok then
        log:error("[BizTopo] unload_ocp error, ret:%s", ret)
    end
end

function c_biz_topo_service:monitor_ocp_presence()
    if self.monitor_ocp_task then
        return
    end
    self.monitor_ocp_task = true
    cmn.skynet.fork(function()
        local smc = self:get_exu_smc()
        if not smc then
            self.monitor_ocp_task = false
            return
        end
        local ocp_card_list = c_device_service.get_instance().ocp_card_list
        local ok, ret, presence_raw, type_raw, ocp_1_presence, ocp_2_presence
        while true do
            if #ocp_card_list == 0 then
                goto next
            end
            ok, ret = pcall(function()
                presence_raw = smc:Read(ctx.new(), 0x8004100, 2)
                type_raw = smc:Read(ctx.new(), 0x8004500, 2)
                if type_raw:byte(1) == 1 and presence_raw:byte(1) == 1 then
                    ocp_1_presence = 1
                else
                    ocp_1_presence = 0
                end

                if type_raw:byte(2) == 1 and presence_raw:byte(2) == 1 then
                    ocp_2_presence = 1
                else
                    ocp_2_presence = 0
                end
            end)
            if not ok then
                log:error('[BizTopo]monitor_ocp_presence err=%s', ret)
                goto next
            end
            log:debug('[BizTopo]monitor_ocp_presence ocp_1=%s ocp_2=%s', ocp_1_presence, ocp_2_presence)
            self:unload_ocp(ocp_1_presence, ocp_2_presence)
            ::next::
            cmn.skynet.sleep(100 * 60) -- 轮询间隔60s
        end
    end)
end

function c_biz_topo_service:main()
    log:notice('[BizTopo] Sub service start.')

    self.device_loader:main()
end

c_biz_topo_service.update_bdf_cfgs = update_bdf_cfgs

return singleton(c_biz_topo_service)
