-- 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 class = require 'mc.class'
local event_util = require 'infrastructure.event'
local defs = require 'domain.alarm.defs'
local mc_utils = require 'mc.utils'
local json = require 'cjson'
local skynet = require 'skynet'
local log = require 'mc.logging'
local alarm_manager = require 'domain.alarm.alarm_manager'
local fructl_handler = require 'infrastructure.fructl'
local client = require 'bios.client'
local Alarm = class()

local MAX_COROUTINE_NUM<const> = 50

function Alarm:ctor()
end

-- 1、填充State
-- 2、填充MessageArgs
local AlarmTypes = {
    [defs.AlarmType.PowerLink] = {
        MessageArgs = {
            defs.PowerLinkAlarmArgs.CPU,
            defs.PowerLinkAlarmArgs.Type,
            defs.PowerLinkAlarmArgs.Die,
            defs.PowerLinkAlarmArgs.Channel
        },
        EventMsg = {
            ComponentName = 'PowerSupply',
            State = '',
            EventKeyId = 'PSU.PSULinkAbnormal',
            MessageArgs = '',
            SystemId = '',
            ManagerId = '1',
            ChassisId = '',
            NodeId = ''
        }
    },
    [defs.AlarmType.VRAbnormal] = {
        MessageArgs = {
            defs.VRAbnormalArgs.CPU,
            defs.VRAbnormalArgs.Die,
            defs.VRAbnormalArgs.Type,
        },
        EventMsg = {
            ComponentName = 'CpuBoard',
            State = '',
            EventKeyId = 'CpuBoard.VRPowerRedundancyFailure',
            MessageArgs = '',
            SystemId = '',
            ManagerId = '1',
            ChassisId = '',
            NodeId = ''
        }
    },
    [defs.AlarmType.MemoryLink] = {
        MessageArgs = {
            defs.MemaryLinkAlarmArgs.DimmIndex
        },
        EventMsg = {
            ComponentName = 'Memory',
            State = '',
            EventKeyId = 'Memory.MemoryI2CI3CLinkAbnormal',
            MessageArgs = '',
            SystemId = '',
            ManagerId = '1',
            ChassisId = '',
            NodeId = '',
            SubjectType = '1'
        }
    }
}

function Alarm:args(msg, alarm_type)
    local event_args = msg.args
    local event_type = msg.type
    local args_tbl = {nil}
    for _, v in pairs(alarm_type.MessageArgs) do
        local event_arg = event_args[v]
        if not event_args[v] then
            error(string.format('[bios]build type(%s) alarm, arg(%s) empty', event_type, v))
        end
        table.insert(args_tbl, tostring(event_arg))
    end
    return json.encode(args_tbl)
end

function Alarm:generate_event(msg, event, alarm_mgr)
    local ok, err = pcall(event_util.generate_event, event)
    if ok then
        log:info('[bios]add event: alarm_type(%s) event_type(%s) success', msg.type, msg.state)
        pcall(function()
            alarm_mgr:manager_event(event)
        end)
    else
        log:error('[bios]add event: alarm_type(%s) event_type(%s) fail(err:%s)', msg.type, msg.state, err)
    end
end

function Alarm:event(msg, alarm_type)
    local alarm_mgr = alarm_manager.get_instance()
    if alarm_mgr.coroutine_num < MAX_COROUTINE_NUM then
        alarm_mgr.coroutine_num = alarm_mgr.coroutine_num + 1
        skynet.fork_once(function()
            local event = mc_utils.table_copy(alarm_type.EventMsg)
            event.State = tostring(msg.state)
            local ok, err = pcall(
                self.format_component_name, self, event, msg)
            if not ok then
                log:debug('[bios]get memory component name failed, error %s', err)
                alarm_mgr.coroutine_num = alarm_mgr.coroutine_num - 1
                return
            end
            local message_args = self:args(msg, alarm_type)
            event.MessageArgs = message_args

            self:generate_event(msg, event, alarm_mgr)
            alarm_mgr.coroutine_num = alarm_mgr.coroutine_num - 1
        end)
    else
        log:info("[bios]add event: created too much coroutines, alarm_type(%s) event_type(%s)", msg.type, msg.state)
        error('add event failed')
    end
end

-- msg = {
--  type = defs.AlarmType.MemoryLink,
--  state = true/false,
--  args = {}
-- }
function Alarm:alarm(msg, system_id)
    local alarm_type = AlarmTypes[msg.type]
    if not alarm_type then
        error(string.format('[bios]build alarm, type(%s) invalid', msg.type))
    end

    alarm_type.EventMsg.SystemId = system_id or ''
    self:event(msg, alarm_type)
end

function Alarm:format_component_name(event, msg)
    if event.ComponentName == 'CpuBoard' and event.SystemId == '' then
        event.ComponentName = self:get_cpu_component_name(msg)
        return
    end
    if event.ComponentName ~= 'Memory' then
        return
    end
    local args = msg.args[defs.MemaryLinkAlarmArgs.DimmIndex]
    if type(args) ~= 'string' or not args.format('DIMM') or #args < 7 then
        return args
    end
    local cpu_id = tonumber(args:sub(5, 5), 16)
    local logical_channel_id = tonumber(args:sub(6, 6), 16)
    local dimm_id = tonumber(args:sub(7, 7), 16)
    local physical_channel = self:get_physical_channel(cpu_id, logical_channel_id, dimm_id)
    if not physical_channel then
        error('[bios] input param error, not find physical channel')
    end
    event.ComponentName = string.format('DIMM%1x%1X%1x', cpu_id, physical_channel, dimm_id)
    msg.args[defs.MemaryLinkAlarmArgs.DimmIndex] = event.ComponentName
end

function Alarm:get_cpu_component_name(msg)
    local component_name
    if not msg.args['DeviceLocator'] then
        return 'CpuBoard1'
    end
    client:ForeachProcessorObjects(function(obj)
        if obj.Id and obj.Position then
            if obj.Id == tonumber(string.match(msg.args['DeviceLocator'], 'CPU(%d+)') or 1) then
                component_name = obj.Position
            end
        end
    end)
    return component_name or 'CpuBoard1'
end

function Alarm:get_physical_channel(cpu_id, logical_channel_id, dimm_id)
    local memory_obj = fructl_handler:get_memory_obj()
    for _, v in pairs(memory_obj) do
        if v.CpuId == cpu_id and
            v.LogicalChannelId == logical_channel_id and
            v.DimmId == dimm_id then
            return v.ChannelId
        end
    end
end

return Alarm