-- Copyright (c) 2025 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 log = require 'mc.logging'
local hypercard_smbus_cfg = require 'protocol.smbus_hypercard'
local skynet = require 'skynet'
local c_tasks = require 'mc.orm.tasks'
local cjson = require 'cjson'
local event = require 'infrastructure.event'
local add_event = require 'add_event'

local DPU_CARD_SUBJECT_TYPE <const> = 0x08
local EVENT_DPU_MAINCHIP_FAULT_WARNING_KEY_ID <const> = 'PCIeCard.DPUMainChipFaultWarning'
local EVENT_DPU_MAINCHIP_FAULT_CRITICAL_KEY_ID <const> = 'PCIeCard.DPUMainChipFaultCritical'
local EVENT_DPU_FAULT_WARNING_KEY_ID <const> = 'PCIeCard.DPUFaultWarning'
local EVENT_DPU_FAULT_CRITICAL_KEY_ID <const> = 'PCIeCard.DPUFaultCritical'
local bmc_error_code_list <const> = {
    [0x01] = 'BoardVoltageAbnormalWarning',
    [0x02] = 'DPUBootFailWarning',
    [0x04] = 'DPUThermalTripErrorCritial',
    [0x08] = 'DPUResetWarning',
    [0x10] = 'DPUMCEErrorCritial'
}
local bmc_error_key_id_list <const> = {
    [0x01] = EVENT_DPU_FAULT_WARNING_KEY_ID,
    [0x02] = EVENT_DPU_FAULT_WARNING_KEY_ID,
    [0x04] = EVENT_DPU_FAULT_CRITICAL_KEY_ID,
    [0x08] = EVENT_DPU_FAULT_WARNING_KEY_ID,
    [0x10] = EVENT_DPU_FAULT_CRITICAL_KEY_ID
}
local dpu_error_code_list <const> = {
    [0x01] = 'DDRUncorrectedECCErrorCritical',
    [0x02] = 'DDRCorrectableECCErrorWarning',
    [0x04] = 'DPUSRAM2bitECCCritical',
    [0x08] = 'DPUMEPErrorWarning',
    [0x10] = 'DPUMEErrorWarning',
    [0x20] = 'DPUDPEErrorWarning',
    [0x40] = 'DPUPCIECorrectableErrorWarning',
    [0x80] = 'DPUPCIEUncorrectedErrorCritical'
}
local dpu_error_key_id_list <const> = {
    [0x01] = EVENT_DPU_MAINCHIP_FAULT_CRITICAL_KEY_ID,
    [0x02] = EVENT_DPU_MAINCHIP_FAULT_WARNING_KEY_ID,
    [0x04] = EVENT_DPU_MAINCHIP_FAULT_CRITICAL_KEY_ID,
    [0x08] = EVENT_DPU_MAINCHIP_FAULT_WARNING_KEY_ID,
    [0x10] = EVENT_DPU_MAINCHIP_FAULT_WARNING_KEY_ID,
    [0x20] = EVENT_DPU_MAINCHIP_FAULT_WARNING_KEY_ID,
    [0x40] = EVENT_DPU_MAINCHIP_FAULT_WARNING_KEY_ID,
    [0x80] = EVENT_DPU_MAINCHIP_FAULT_CRITICAL_KEY_ID
}
local hypercard_obj = {}
hypercard_obj.__index = hypercard_obj
local function get_smbus_cfg()
    return hypercard_smbus_cfg.new()
end

local PCIECARD_INTF <const> = 'bmc.kepler.Systems.PCIeDevices.PCIeCard'

function hypercard_obj:update_alarm_status(device_name, error_message)
    local alarm_exist = 0
    local ok, alarm_list = event.get_latest_alarm_list()
    if not ok then
        log:debug("get latest alarm list failed, error: %s", alarm_list)
        return alarm_exist
    end

    -- 根据 ComponentName、MessageArgs 匹配告警
    for _, alarm in pairs(alarm_list) do
        if not alarm.ComponentName or alarm.ComponentName ~= device_name then
            log:debug('alarm.ComponentName not match') -- 不是当前对象的告警
            goto continue
        end
        if not alarm.MessageArgs or alarm.MessageArgs == "" then
            log:debug('alarm.MessageArgs empty')
            goto continue
        end
        local is_ok, alarm_msg_args = pcall(cjson.decode, alarm.MessageArgs)
        if not is_ok then
            log:debug('alarm.MessageArgs decode failed, err: %s', alarm_msg_args)
            goto continue
        end
        if alarm_msg_args[#alarm_msg_args] == error_message then
            alarm_exist = 1
            break
        end
        ::continue::
    end

    return alarm_exist
end

function hypercard_obj:alarm_task(reg_val, error_code_list, error_key_id_list)
    for key, value in pairs(error_code_list) do
        if (reg_val & key ~= key) and (self:update_alarm_status(self.obj.DeviceName, value) == 0) then
            local param = { self.obj.SlotID, self.obj.Name, value }
            add_event.generate_error_code('true', error_key_id_list[key], self.obj.DeviceName,
                DPU_CARD_SUBJECT_TYPE, param, 'q05')
        elseif (reg_val & key == key) and (self:update_alarm_status(self.obj.DeviceName, value) ~= 0) then
            local param = { self.obj.SlotID, self.obj.Name, value }
            add_event.generate_error_code('false', error_key_id_list[key], self.obj.DeviceName,
                DPU_CARD_SUBJECT_TYPE, param, 'q05')
        end
    end
end

local static_prop_map = {
    CPLDProtocolVersion = function(object, version)
        object.obj.CPLDProtocolVersion = version
    end,
    BOM = function(object, data)
        log:debug('set BOM to %s', data)
        object.obj.BOM = data
    end,
    LogicVersion = function(object, data)
        log:debug('set LogicVersion to %s', data)
        object.obj.LogicVersion = data
    end,
    BIOSVersion = function(object, data)
        object.obj.BIOSVersion = data
    end,
    IMUVersion = function(object, data)
        object.obj.IMUVersion = data
    end,
    MemorySizeGiB = function(object, data)
        object.obj.MemorySizeGiB = data
    end,
    DiskCapacityGiB = function(object, data)
        object.obj.DiskCapacityGiB = data
    end,
    ManagerFirmwareVersion = function(object, data)
        log:debug('set ManagerFirmwareVersion to %s', data)
        object.obj.ManagerFirmwareVersion = data
    end,
    PowerState = function(object, data)
        log:debug('set PowerState to %s', data)
        -- bit3:当前DPU是否上电的状态指示， 1 - DPU上电状态 0 - DPU下电状态
        if data & 0x8 == 8 then
            object.obj.PowerState = 'On'
        else
            object.obj.PowerState = 'Off'
        end
    end
}

local dynamic_prop_map = {
    SerialDirection = function(object, data)
        -- SerialDirection实际方向：0：串口关闭 1：串口连接到管理控制单元，如IMU、MCP ； 2：串口连接到系统控制单元，如SCP；3：串口连接到计算处理单元，如AP',
        -- 取寄存器低两位
        local value = data & 0x3
        if value == 0 then
            -- 00 - NCSI选通SCP串口
            object.obj.SerialDirection = 2
        elseif value == 1 then
            -- 01 - NCSI选通IMU串口
            object.obj.SerialDirection = 1
        elseif value == 2 then
            -- 10 - NCSI选通N2串口（Default）
            object.obj.SerialDirection = 3
        elseif value == 3 then
            -- 11 - NCSI串口关闭
            object.obj.SerialDirection = 0
        end
    end,
    PowerWatts = function(object, data)
        object.obj.PowerWatts = data
    end,
    AmbientTemperatureCelsius = function(object, data)
        object.obj.Inlet1TemperatureCelsius = data
    end,
    NetworkAdapterChipTemperatureCelsius = function(object, data)
        object.obj.NetworkAdapterChipTemperatureCelsius = data
    end,
    NICSFPMaxTemperatureCelsius = function(object, data)
        object.obj.NICSFPMaxTemperatureCelsius = data
    end,
    BMCAlertStatus = function(object, data)
        object.bmc_warning_reg = data[2]
    end,
    DPUAlertStatus = function(object, data)
        object.dpu_warning_reg = data[2]
    end
}

function hypercard_obj:get_cpld_check_result()
    -- 已经无法通信场景下，不冗余产生cpld自检失败告警
    if self.obj.HeartBeatLoss == 1 then
        log:debug('heart beat loss, get_cpld_check_result return')
        return true
    end
    self.smbus:CardInfo(self, 'CPLDCheckWrite', { { 0x55 } })
    local res = self.smbus:CardInfo(self, 'CPLDCheckRead')
    if not res or res ~= 0xAA then
        log:debug('cpld check failed, read data is %s', res)
        return false
    end

    self.smbus:CardInfo(self, 'CPLDCheckWrite', { { 0xAA } })
    res = self.smbus:CardInfo(self, 'CPLDCheckRead')
    if not res or res ~= 0x55 then
        log:debug('cpld check failed, read data is %s', res)
        return false
    end
    return true
end

function hypercard_obj:start_cpld_check_task()
    self.cpld_check_fail_cnt = 0
    local result
    local task = c_tasks.get_instance():new_task('cpld_check'):loop(function()
        result = self:get_cpld_check_result()
        if not result then
            self.cpld_check_fail_cnt = self.cpld_check_fail_cnt + 1
        else
            self.cpld_check_fail_cnt = 0
            self.obj.Health = 0
        end
        -- 连续3次cpld自检失败后，产生告警
        if self.cpld_check_fail_cnt > 2 then
            self.obj.Health = 1
        end
    end):set_timeout_ms(300000)
    self.update_tasks['cpld_check'] = task
end

function hypercard_obj:update_static_property()
    log:debug('start to update static property')
    for property_name, update_func in pairs(static_prop_map) do
        local ok, value = pcall(function()
            return self.smbus:CardInfo(self, property_name)
        end)
        log:debug('pro is %s, ret is %s, value is %s', property_name, ok, value)

        if ok and update_func then
            update_func(self, value)
        end
        skynet.sleep(100)
    end
end

function hypercard_obj:update_dynamic_property()
    for property_name, update_func in pairs(dynamic_prop_map) do
        local ok, value = pcall(function()
            return self.smbus:CardInfo(self, property_name)
        end)
        log:debug('pro is %s, ret is %s, value is %s', property_name, ok, value)

        if ok and update_func then
            update_func(self, value)
        end
        skynet.sleep(100)
    end
end

function hypercard_obj:start_update_property_task()
    local task = c_tasks.get_instance():new_task('update_static_property'):loop(function()
        if self.obj.HeartBeatLoss == 1 then
            log:debug('heart beat loss, update_static_property return')
            return
        end
        self:update_static_property()
    end):set_timeout_ms(60000)
    self.update_tasks['update_static_property'] = task

    task = c_tasks.get_instance():new_task('update_dynamic_property'):loop(function()
        if self.obj.HeartBeatLoss == 1 then
            log:debug('heart beat loss, update_dynamic_property return')
            return
        end
        self:update_dynamic_property()
    end):set_timeout_ms(5000)
    self.update_tasks['update_dynamic_property'] = task
end

-- 心跳丢失时,停止所有属性的轮询获取,并将功耗、温度恢复为默认值
function hypercard_obj:start_update_heartbeat_status_task()
    local ok
    local status
    local task = c_tasks.get_instance():new_task('update_heartbeat_status'):loop(function()
        ok, status = self.smbus:CardInfo(self, 'PowerState')
        if not ok then
            self.obj.SystemLoadedStatus = 255
            self.obj.HeartBeatLoss = 1
            self.obj.PowerWatts = 0
            self.obj.Inlet1TemperatureCelsius = 0xfe
            self.obj.NetworkAdapterChipTemperatureCelsius = 0xfe
        else
            self.obj.SystemLoadedStatus = status
            self.obj.HeartBeatLoss = 0
        end
    end):set_timeout_ms(5000)
    self.update_tasks['update_heartbeat_status'] = task
end

function hypercard_obj:stop()
    for name, task in pairs(self.update_tasks) do
        if task and task.stop then
            task:stop()
            log:notice('task %s has stopped', name)
        end
    end
end

function hypercard_obj:start_alarm_notify_task()
    local task = c_tasks.get_instance():new_task('alarm_notify_task'):loop(function()
        self:alarm_task(self.bmc_warning_reg, bmc_error_code_list, bmc_error_key_id_list)
        self:alarm_task(self.dpu_warning_reg, dpu_error_code_list, dpu_error_key_id_list)
    end):set_timeout_ms(5000)
    self.update_tasks['alarm_notify_task'] = task
end

function hypercard_obj:start()
    self:start_update_heartbeat_status_task()
    self:start_update_property_task()
    self:start_cpld_check_task()
    self:start_alarm_notify_task()
end

function hypercard_obj.new(object, position)
    return setmetatable({
        BMCChip = object['RefBMCChip'],
        CPLDChip = object['RefCPLDChip'],
        obj = object,
        bmc_warning_reg = 0xFF,
        dpu_warning_reg = 0xFF,
        smbus = get_smbus_cfg(),
        cpld_check_fail_cnt = 0,
        pciecard = object[PCIECARD_INTF],
        update_tasks = {}
    }, hypercard_obj)
end

-- 重启时，需要先写DPU_RESET位（bit4)为1 , 10ms后写0
function hypercard_obj:reset_sdi_card()
    log:notice('reset_sdi_card start.')
    local bit = 4
    local ret
    -- 先写1
    -- 先获取初始值，基于初始值修改bit4，其他bit保持不变
    local power_value = self.smbus:CardInfo(self, 'PowerState')
    power_value = power_value | (1 << bit)
    ret = self.smbus:CardInfo(self, 'SetPowerStatus', { { power_value } })
    if not ret then
        log:error('reset_sdi_card write failed, err: %s', ret)
        return false
    end
    log:notice('current reset_sdi_card: %s, reset_sdi_card write DPU_RESET 1 successfully.', power_value)

    -- 10ms后写0
    skynet.sleep(1)
    power_value = self.smbus:CardInfo(self, 'PowerState')
    power_value = power_value & (0 << bit)
    ret = self.smbus:CardInfo(self, 'SetPowerStatus', { { power_value } })
    if not ret then
        log:error('reset_sdi_card write failed, err: %s', ret)
        return false
    end

    log:notice('current reset_sdi_card: %s, reset_sdi_card write DPU_RESET 0 successfully.', power_value)
    return true
end

-- PowerOn:1, PowerOff:0
function hypercard_obj:set_dpu_power_state(data)
    local power_value = self.smbus:CardInfo(self, 'PowerState')
    local ret
    if data == 0 then
        -- PowerOff：最后一个Bit，写入1触发一次下电操作，逻辑自动清零，读出值为0

        power_value = power_value | 0x01

        ret = self.smbus:CardInfo(self, 'SetPowerStatus', { { power_value } })
        if not ret then
            log:error('power_state write %s failed, err: %s', data, ret)
            return false
        end
    elseif data == 1 then
        -- PowerOn：倒数第二个Bit，写入1触发一次上电操作， 逻辑自动清零，读出值为0
        power_value = power_value | 0x02
        ret = self.smbus:CardInfo(self, 'SetPowerStatus', { { power_value } })
        if not ret then
            log:error('power_state write %s failed, err: %s', data, ret)
            return false
        end
    else
        log:error('set power state failed')
        return false
    end
    return true
end

-- 待设置的串口方向，0：串口关闭 1：串口连接到管理控制单元，如IMU、MCP ； 2：串口连接到系统控制单元，如SCP；3：串口连接到计算处理单元，如AP',
function hypercard_obj:set_dpu_serial_direction(direction)
    log:notice('set_dpu_serial_direction %s start.', direction)
    local current_direction = self.smbus:CardInfo(self, 'SerialDirection')
    local input

    if direction == 0 then
        -- 无方向  11
        input = current_direction | 0X03
    elseif direction == 2 then
        -- SCP 00
        input = current_direction & 0XFC
    elseif direction == 1 then
        -- IMU 01
        input = (current_direction & 0XFD) | 0X01
    elseif direction == 3 then
        -- 处理器方向 10
        input = (current_direction & 0XFE) | 0X02 -- 最后两位分别置为1、0
    else
        log:error('set dpu serial direction failed.')
        return false
    end
    self.smbus:CardInfo(self, 'SetSerialDirection', { { input } })
    return true
end

return hypercard_obj
