-- Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved.
--
-- this file licensed under the 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 class = require 'mc.class'
local singleton = require 'mc.singleton'
local log = require 'mc.logging'
local skynet = require 'skynet'
local cjson = require 'cjson'
local c_inter_node_cable_configuration = require 'ub_cable.class.inter_node_cable_configuration'
local c_node_port_addr = require 'ub_cable.class.node_port_addr'
local event = require 'ub_cable.infrastructure.event'
local def = require 'ub_cable.infrastructure.def'
local debounce = require 'mc.debounce.debounce'
local client = require 'pcie_device.client'
local context = require 'mc.context'
local mdb = require 'mc.mdb'
local mdb_service = require 'mc.mdb.mdb_service'
local utils_core = require 'utils.core'

local ub_cable_detection = class()

local MATCH_UBPORT_PATH = '^/bmc/kepler/Chassis/.*/UBInterfaceCards/UBInterfaceCard_.*/Ports'
local UB_FAULT_LED_ON<const> = 1
local UB_FAULT_LED_OFF<const> = 0

function ub_cable_detection:ctor(bus)
    self.bus = bus
    self.cable_cfg_list = {}
    self.node_port_addr_list = {}
    self.cur_event_list = {}            -- key是事件标识 component name 拼接告警码，value是告警信息参数
    self.debounce_vals = {}             -- key是事件标识，value是事件防抖对象
    self.debounce_event_list = {}       -- key是事件标识，value是防抖后的告警是否触发 0表示未触发，1表示触发
    self.event_key_node_port_addr_list = {} -- key是事件标识，value是转接网口对象
    self.guid_map = {} -- key是node id/guid/macro id，value是对应转接网口对象信息
    self.guid_node_id_map = {} -- key是guid, value是node id
    self.node_id_guid_map = {} -- key是node id, value是guid
end

function ub_cable_detection:init()
    local ok, ret = pcall(function()
        self:start()
    end)
    if not ok then
        log:error('init error, %s', ret)
    end
    -- 监听datasync组件消息
    self:listen_inter_node_update()
end

function ub_cable_detection:on_dump(path)
    log:notice('[ub_cable_detection] on_dump')
    log:notice(self.guid_node_id_map)
    log:notice(self.node_id_guid_map)
    log:notice(self.guid_map) --本端信息
    for _, port_obj in pairs(self.node_port_addr_list) do
        log:notice("src_info=%s target_info=%s peer_info=%s", port_obj.src_info, port_obj.target_info, port_obj.peer_info)
    end
end

function ub_cable_detection:update_guid_map(port_obj)
    -- 本端 GUID_MacroId为key对应端口信息，用来匹配实际端口信息
    if not port_obj:get_prop('NodeId') or port_obj:get_prop('NodeId') == 0 or port_obj:get_prop('NodeId') == 255 then
        return
    end
    local key = string.format("%s_%s_%s", port_obj:get_prop('NodeId'), port_obj.src_guid, port_obj:get_prop('MacroId'))
    self.guid_map[key] = {
        node_id = port_obj:get_prop('NodeId'),
        cpu_id = port_obj:get_prop('CPUId'),
        die_id = port_obj:get_prop('DieId'),
        macro_id = port_obj:get_prop('MacroId'),
        slot_id = port_obj:get_prop('InterfaceDeviceSlotId'),
        port_id = port_obj:get_prop('InterfaceDevicePortId'),
        guid = port_obj.src_guid
    }
end

function ub_cable_detection:update_guid_node_id_map(guid, node_id)
    if not guid or not node_id or node_id == 0 or node_id == 255 then
        return
    end
    self.guid_node_id_map[guid] = node_id
    self.node_id_guid_map[node_id] = guid
end

function ub_cable_detection:parse_name(sync_data)
    local ok, ret = pcall(function()
        -- ret格式样例：[[63,[["Name",{"PropertyValue":"NodePortAddr_0_0_6_1_3_2_8000B417B30000000000000200A008CC","Expired":"false"}]]]]
        local prop = sync_data[1][2][1][1]
        local value = sync_data[1][2][1][2].PropertyValue
        log:debug('prop = %s, value = %s', prop, value)
        -- prop = Name, value = NodePortAddr_0_0_2_1_1_2_8000B417B30000000000000200A008CC
        local result = {}
        if prop == 'Name' and #value > 0 then
            for part in string.gmatch(value, "[^_]+") do
                table.insert(result, part)
            end
            if not next(result) then
                return
            end
        end
        if not result[def.COMPONENT_NAME_INDEX_NODEID] or tonumber(result[def.COMPONENT_NAME_INDEX_NODEID]) == 0 or tonumber(result[def.COMPONENT_NAME_INDEX_NODEID]) == 255 then
            return
        end
        local key = string.format("%s_%s_%s", result[def.COMPONENT_NAME_INDEX_NODEID],
            result[def.COMPONENT_NAME_INDEX_GUID], result[def.COMPONENT_NAME_INDEX_MACROID])
        self.guid_map[key] = {
            node_id = tonumber(result[def.COMPONENT_NAME_INDEX_NODEID]),
            cpu_id = tonumber(result[def.COMPONENT_NAME_INDEX_CPUID]),
            die_id = tonumber(result[def.COMPONENT_NAME_INDEX_DIEID]),
            macro_id = tonumber(result[def.COMPONENT_NAME_INDEX_MACROID]),
            slot_id = tonumber(result[def.COMPONENT_NAME_INDEX_SLOTID]),
            port_id = tonumber(result[def.COMPONENT_NAME_INDEX_PORTID]),
            guid = result[def.COMPONENT_NAME_INDEX_GUID]
        }

        self:update_guid_node_id_map(result[def.COMPONENT_NAME_INDEX_GUID], tonumber(result[def.COMPONENT_NAME_INDEX_NODEID]))
    end)
    if not ok then
        log:error('parse_name error=%s', ret)
    end
end

function ub_cable_detection:get_props_rpc(identifier, object_name, interface)
    local data_sync_obj = mdb.get_object(self.bus, "/bmc/kepler/Managers/1/DataSync", "bmc.kepler.Managers.DataSync")
    local ok, ret
    for _, port_obj in pairs(self.node_port_addr_list) do
        ok, ret = pcall(data_sync_obj.GetProperties, data_sync_obj, context.new(), identifier, port_obj.component_object_name, interface)
        if ok then
            log:debug('get_props_rpc identifier=%s object_name=%s interface=%s', identifier, port_obj.component_object_name, interface)
            log:debug(cjson.encode(ret))
            if not next(ret) then
                return
            end
            self:parse_name(ret)
            log:debug(self.node_id_guid_map)
            log:debug(self.guid_map)

            -- 更新完guid_map 更新对端的信息。兼容4节点8节点，没插的节点白名单不检测。
            port_obj:update_target_info(self.cable_cfg_list, self.node_id_guid_map)
        else
            log:error("get_props_rpc err=%s", ret)
        end
    end
end

-- 节点之间信息同步
function ub_cable_detection:listen_inter_node_update()
    -- NodePortAddr_CPUId_DieId_MacroId_InterfaceDeviceSlotId_InterfaceDevicePortId_ NodeId_GUID
    -- 前半段是静态配置，后半段是代码设置
    -- 获取同步的ComponentName后，更新 guid_map/guid_node_id_map
    client:SubscribeDataSyncPropertyUpdated(function(ctx, identifier, object_name, interface)
        -- 获取 DataSync 对象，调用rpc方法
        if interface ~= "bmc.kepler.Systems.Component" then
            return
        end
        -- 实测发现数据同步更新不全，需要主动查询所有Component的对象属性。
        self:get_props_rpc(identifier, object_name, interface)
    end)
end

function ub_cable_detection:update_peer_info(port_obj, peer_info)
    if not peer_info or not peer_info.remote_link_status_list or #peer_info.remote_link_status_list == 0 then
        -- 本端返回建链，但是没有读到对端信息。imu信息返回有问题。
        log:error("CPUId=%s DieId=%s MacroId=%s, peer info read failed.", port_obj:get_prop('CPUId'),
            port_obj:get_prop('DieId'), port_obj:get_prop('MacroId'))
        return
    end

    local peer_guid = peer_info.remote_link_status_list[1].remote_port_guid
    local peer_macro_id = peer_info.remote_link_status_list[1].remote_port_id --是对端的MacroId
    local peer_node_id = self.guid_node_id_map[peer_guid]
    local map_key = string.format("%s_%s_%s", peer_node_id, peer_guid, peer_macro_id)

    if not peer_node_id or not self.guid_map[map_key] then
        -- 找不到对端的guid对应的节点ID，说明没有信息同步过来。告警条件不足。
        port_obj:clear_peer_info()
    end

    port_obj:update_peer_info(self.guid_map[map_key])
end

function ub_cable_detection:read_ub_cable_info(imu_cmd)
    local ok, ret = pcall(function()
        local cpu_id, controller_id, direction, port_id, local_info, peer_info
        for _, port_obj in pairs(self.node_port_addr_list) do
            -- 本端数据获取
            cpu_id, controller_id, direction, port_id = port_obj:get_prop('CPUId'), port_obj:get_prop('DieId'), 0,
                def.IMU_CMD_ALL_PORT
            local_info = imu_cmd.get_ub_port_info(self.bus, cpu_id, controller_id, direction, port_id)
            -- 更新所有本端数据
            if local_info and local_info.guid then
                port_obj:update_src_info(local_info)
                self:update_guid_node_id_map(local_info.guid, port_obj:get_prop('NodeId'))
                self:update_guid_map(port_obj)
            end

            -- 如果本端有建链，就需要再查对端信息
            if not port_obj:get_prop('MacroId') then
                return
            end
            local link_status_index = port_obj:get_prop('MacroId') + 1
            if local_info and local_info.link_status_list[link_status_index] == 1 then
                cpu_id, controller_id, direction, port_id = port_obj:get_prop('CPUId'), port_obj:get_prop('DieId'), 1,
                    port_obj:get_prop('MacroId')
                peer_info = imu_cmd.get_ub_port_info(self.bus, cpu_id, controller_id, direction, port_id)
                self:update_peer_info(port_obj, peer_info)
            else
                -- 本端未建链，如果有插错告警就要消除。
                port_obj:clear_peer_info()
            end
        end
    end)
    if not ok then
        log:error('read_ub_cable_info failed, err=%s', ret)
    end
end

-- 如果没有获取到node id，就不用开始线缆检测
function ub_cable_detection:all_node_id_ready()
    for _, port_obj in pairs(self.node_port_addr_list) do
        if not port_obj:node_id_ready() then
            return false
        end
    end
    return true
end

function ub_cable_detection:start()
    local imu_cmd = require 'ub_cable.imu_cmd'
    skynet.fork_loop({count = 0}, function()
        log:info('start fork_loop.')
        skynet.sleep(100 * 60) -- 等待60s
        if not self:all_node_id_ready() then
            goto continue
        end
        -- 读取线缆配置，更新转接口的本端、对端信息
        self:read_ub_cable_info(imu_cmd)
        -- 比较判断出告警
        self:update_ub_cable_event()
        -- 判断故障灯
        self:update_ub_card_led()
        ::continue::
    end)
end

-- 对象分发处理回调函数
function ub_cable_detection:on_add_object(class_name, mds_obj, position)
    local switch = {
        ['InterNodeCableConfiguration'] = function()
            local object = c_inter_node_cable_configuration.new(mds_obj, position)
            table.insert(self.cable_cfg_list, object)
        end,
        ['NodePortAddr'] = function()
            local object = c_node_port_addr.new(mds_obj, position)
            table.insert(self.node_port_addr_list, object)
            self:init_debounce_value(object)
        end
    }
    if switch[class_name] then
        log:notice('Add object %s', mds_obj.name)
        switch[class_name]()
        log:notice('Add object %s successfully', mds_obj.name)
    end
end

-- 对于每个NodePortAddr，初始化防抖对象
function ub_cable_detection:init_debounce_value(obj)
    local event_key
    for event_code, _ in pairs(def.UB_CABLE_EVENTCODE) do
        -- 获取当前告警的唯一标识
        event_key = string.format("%s-%s", obj.component_name, event_code)
        -- 获取当前的告警，如果持久化是已有告警，就初始化为已触发。
        self.debounce_vals[event_key] = debounce[debounce.DEBOUNCED_CONT].new(def.DEFAULT_CONT_NUM,
            self.cur_event_list[event_key] and 1 or 0)
        -- 更新event key和转接网口对象
        self.event_key_node_port_addr_list[event_key] = obj
    end
end

-- 获取当前所有告警后，过滤当前所有transfer port, 
function ub_cable_detection:filter_alarm(alarm)
    if not def.UB_CABLE_EVENTCODE[alarm.EventCode] then
        return false
    end
    if alarm.ComponentName:sub(1, #def.UB_CABLE_COMPONENT_NAME_PREFIX) ~= def.UB_CABLE_COMPONENT_NAME_PREFIX then
        return false
    end
    return true
end

-- 获取当前ComponentName的静态前缀
function ub_cable_detection:get_static_component_name_prefix(component_name)
    local result = {}
    for word in string.gmatch(component_name, "[^_]+") do
        table.insert(result, word)
    end
    local result = table.concat(result, "_", def.STATIC_COMPONENT_NAME_OFFSET, def.STATIC_COMPONENT_NAME_SIZE)
    return result
end

-- 获取当前event仓的告警，更新 self.cur_event_list
function ub_cable_detection:update_latest_alarm_list()
    local ok, alarm_list = pcall(event.get_latest_alarm_list, self.bus)
    if not ok or not alarm_list then
        error(string.format('[event_handler] get_latest_alarm_list failed. err:%s', alarm_list))
    else
        log:debug('[event_handler] get_latest_alarm_list successfully.')
    end
    local event_err_code_list = {}
    local event_key, alarm_msg_args
    -- 通过 ComponentName 和 EventCode 和告警参数组合作为key来区分告警
    for _, alarm in pairs(alarm_list) do
        if not self:filter_alarm(alarm) then
            goto continue
        end
        ok, alarm_msg_args = pcall(cjson.decode, alarm.MessageArgs)
        if not ok then
            log:error('[event_handler] alarm.MessageArgs decode failed, err: %s', alarm_msg_args)
            goto continue
        end
        -- 获取当前告警的唯一标识
        event_key = string.format("%s-%s", self:get_static_component_name_prefix(alarm.ComponentName), alarm.EventCode)
        event_err_code_list[event_key] = alarm_msg_args -- 表示告警已触发，恢复告警需要保存上一次的告警信息参数
        ::continue::
    end
    self.cur_event_list = event_err_code_list
end

-- 对于已初始化的防抖对象，判断是否要触发告警
function ub_cable_detection:scan_node_port_addr_status()
    local dbd_trigger
    for _, port_obj in pairs(self.node_port_addr_list) do
        port_obj:update_cable_status()
        for event_key, status in pairs(port_obj.event_status) do
            if status == true then
                dbd_trigger = self.debounce_vals[event_key]:get_debounced_val(1)
            elseif status == false then
                dbd_trigger = self.debounce_vals[event_key]:get_debounced_val(0)
            end
            -- 防抖对象处理后的值，记录到预期的告警状态里
            self.debounce_event_list[event_key] = dbd_trigger  -- 0表示未触发，1表示触发
        end
    end
end

-- 获取告警参数，调用告警接口. event_key是拼接的component_name和event_code，event_code长度是10
function ub_cable_detection:generate_soft_event(event_key, is_assert)
    if not event_key or #event_key < 9 then
        log:error('[event_handler] generate_soft_event, event_key=%s', event_key)
        return
    end
    local event_code = event_key:sub(#event_key - 9, #event_key)
    local event_key_id = def.UB_CABLE_EVENT_KEY_ID_MAP[event_code]
    local obj = self.event_key_node_port_addr_list[event_key]
    local msg_args
    log:notice('[event_handler] generate_soft_event, event_code=%s event_key_id=%s', event_code, event_key_id)
    if is_assert == 'false' then
        -- 如果是消除告警，就需要使用之前的告警参数
        msg_args = self.cur_event_list[event_key]
    else
        msg_args = obj:get_msg_args(event_code)
    end
    return event.generate_soft_event(obj.component_name, is_assert, event_key_id, msg_args, def.UB_CABLE_SUBJECT_TYPE)
end

function ub_cable_detection:log_led_changed(slot_id, port_id, detection, value)
    if detection ~= value then
        log:notice('set UB interface card %s Port %s fault led from %s to %s', slot_id, port_id, detection, value)
    end
end

function ub_cable_detection:set_ubinterface_fault_led(led_on_port_objs)
    local ubcard_objs = client:GetSystemsBoardObjects()
    if not ubcard_objs then
        log:error('Without UB Interface card can match to set fault led')
        return
    end
    local ok, ret, key_str, ubport_obj, ubport_obj_by_status
    for _, ubcard_obj in pairs(ubcard_objs) do
        ok, ret = pcall(mdb_service.get_sub_paths, self.bus, ubcard_obj.path, 2, {})
        if not ok or not ret or not ret.SubPaths then
            log:error('get UB Port path failed, %s', ret)
            return
        end
        for _, ubport_path in pairs(ret.SubPaths) do
            if utils_core.g_regex_match(MATCH_UBPORT_PATH, ubport_path) then
                ok, ubport_obj = pcall(mdb.get_object, self.bus, ubport_path, 'bmc.kepler.Chassis.UBPort')
                if not ok then
                    log:error('get UB Port object failed, %s', ubport_obj)
                    return
                end
                key_str = string.format("%s_%s", ubcard_obj.Slot, ubport_obj.PortId)
                ok, ubport_obj_by_status = pcall(mdb.get_object, self.bus, ubport_path, 'bmc.kepler.Chassis.UBPort.Status')
                if not ok then
                    log:error('get UB Port object failed, %s', ubport_obj_by_status)
                    return
                end
                if led_on_port_objs[key_str] then
                    self:log_led_changed(ubcard_obj.Slot, ubport_obj.PortId, ubport_obj_by_status.CableDetection, UB_FAULT_LED_ON)
                    ubport_obj_by_status.CableDetection = UB_FAULT_LED_ON
                else
                    self:log_led_changed(ubcard_obj.Slot, ubport_obj.PortId, ubport_obj_by_status.CableDetection, UB_FAULT_LED_OFF)
                    ubport_obj_by_status.CableDetection = UB_FAULT_LED_OFF
                end
            end
        end
        log:info('Without UB Port can match to set fault led')
    end
    log:info('Without UB Interface card can match to set fault led')
end

function ub_cable_detection:update_ub_card_led()
    local led_on_port_objs = {}
    local port_obj, port_obj_key --只需要UB卡槽位号和端口号
    for event_key, v in pairs(self.debounce_event_list) do
        port_obj = self.event_key_node_port_addr_list[event_key]
        port_obj_key = string.format("%s_%s", port_obj:get_prop('InterfaceDeviceSlotId'), port_obj:get_port_id_arg(port_obj:get_prop('InterfaceDevicePortId')))
        if v == 1 then
            led_on_port_objs[port_obj_key] = true
        end
    end
    self:set_ubinterface_fault_led(led_on_port_objs)
end

-- 判断是否需要触发或消除告警
function ub_cable_detection:update_ub_cable_event()
    local ok, resp
    -- 更新防抖告警
    self:scan_node_port_addr_status()
    ok, resp = pcall(function()
        -- 获取event仓记录的当前告警码 更新cur_event_list
        return self:update_latest_alarm_list()
    end)
    if not ok then
        log:error('[event_handler] update_latest_alarm_list , error: %s', resp)
        return
    end
    skynet.sleep(10)  -- 等待100毫秒 减少cpu占用
    for event_key, v in pairs(self.debounce_event_list) do
        -- 找出已触发，但是防抖事件判断要消除的告警
        if v == 0 and self.cur_event_list[event_key] then
            -- 消除告警
            ok, resp = pcall(function()
                log:notice('[event_handler] deassert, event_key: %s', event_key)
                return self:generate_soft_event(event_key, 'false')
            end)
            if not ok then
                log:error('[event_handler] deassert error code failed, error: %s', resp)
            end
        end
        -- 找出未触发，但是防抖事件判断要产生的告警
        if v == 1 and not self.cur_event_list[event_key] then
            -- 触发告警
            ok, resp = pcall(function()
                log:notice('[event_handler] assert, event_key: %s', event_key)
                return self:generate_soft_event(event_key, 'true')
            end)
            if not ok then
                log:error('[event_handler] assert error code failed, error: %s', resp)
            end
        end
    end
end

return singleton(ub_cable_detection)
