-- 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 ipmi = require 'ipmi'
local bs = require 'mc.bitstring'
local log = require 'mc.logging'
local enums = require 'ipmi.enums'
local vrd_enums = require 'vrd.enum.vrd_enums'
local vrd_power_service = require 'vrd.chip.power.vrd_power_service'
local multihost_common = require 'multihost.common'
local client = require 'general_hardware.client'
local channel_type = enums.ChannelType
local comp_code = ipmi.types.Cc
local MAX_RETRY<const> = 5
local reqGetVrdInfo = bs.new('<<0xDB0700:3/unit:8, 0x37, CpuId>>')
local rspGetVrdInfo = bs.new('<<0xDB0700:3/unit:8, CpuId, PowerNum, InfoSize, Info/binary>>')
local data_parser_per_power = bs.new(
    '<<type, dieid, volt_low, volt_high, cur_low, cur_mid, cur_high, temp:16, info/binary>>')

local vrd_mgmt = class()

function vrd_mgmt:set_vrd_to_invalid()
    self.mds_obj.Status = 1
    local powers = vrd_power_service.get_instance():get_vrd_powers_by_cpu(self.SystemId, self.CpuId)
    for _, power in pairs(powers) do
        power:set_power_to_invalid()
    end
end

function vrd_mgmt:reset_vrd_info()
    local powers = vrd_power_service.get_instance():get_vrd_powers_by_cpu(self.SystemId, self.CpuId)
    for _, power in pairs(powers) do
        power:reset_power()
    end
    self.mds_obj.Status = 0
end

local function check_vrd_type(type)
    if type == vrd_enums.POWER_TYPE.SERDES or type == vrd_enums.POWER_TYPE.IO_DVDD09 then
        return true
    end
    if type <= vrd_enums.POWER_TYPE.HBM and type >= vrd_enums.POWER_TYPE.CORE then
        return true
    end
    return false
end

local function check_vrd_dieid(dieid)
    if dieid == vrd_enums.POWER_DIEID.D0_NOT_EXIST or dieid == vrd_enums.POWER_DIEID.D1_NOT_EXIST then
        return true
    end
    if dieid <= vrd_enums.POWER_DIEID.TD and dieid >= vrd_enums.POWER_DIEID.NA then
        return true
    end
    return false
end

local function parse_vrd_rsp_info(payload)
    local vrd_power_info_table = {}
    local vrd_power_info = {}
    if not payload then
        log:info('get_vrd_info_from_imu payload is nil')
        return comp_code.ResponseError
    end
    local rsp = rspGetVrdInfo:unpack(payload)
    if rsp == nil then
        log:info('get_vrd_info_from_imu rsp is nil')
        return comp_code.ResponseError
    end
    local power_num, info_size, power_info = rsp.PowerNum, rsp.InfoSize, rsp.Info
    local i = 0
    log:debug('power_num, info_size, power_info_size', power_num, info_size, #power_info)
    while i < power_num and power_info ~= nil and #power_info ~= 0 do
        local data_per_power = data_parser_per_power:unpack(power_info)
        if not data_per_power then
            log:info('get_vrd_info_from_imu data_per_power is nil')
            return comp_code.ResponseError
        end
        local type, dieid, volt_low, volt_high, cur_low, cur_mid, cur_high, tmp_power_info =
            data_per_power.type, data_per_power.dieid, data_per_power.volt_low,
            data_per_power.volt_high, data_per_power.cur_low, data_per_power.cur_mid,
            data_per_power.cur_high, data_per_power.info
        if dieid ~= 0xff and not check_vrd_type(type) and not check_vrd_dieid(dieid) then
            log:info('type(%s) or dieid(%s) error', type, dieid)
            return comp_code.ResponseError
        else
            local voltage = tonumber(string.format('%d.%02d', volt_high, volt_low))
            local amps = tonumber(string.format('%d%d.%02d', cur_high, cur_mid, cur_low))
            local temperature = data_per_power.temp
            vrd_power_info = {
                Type = type,
                DieId = dieid,
                Voltage = voltage,
                CurrentAmps = amps,
                TemperatureCelsius = temperature
            }
            table.insert(vrd_power_info_table, vrd_power_info)
        end
        power_info = tmp_power_info
        i = i + 1
    end
    return comp_code.Success, vrd_power_info_table
end

function vrd_mgmt.get_vrd_info_from_single_host_imu(bus, channel, cpu_id)
    -- ipmb消息CpuId为0、1对应CPU1、CPU2
    local data = reqGetVrdInfo:pack({CpuId = cpu_id})

    local ok, cc, payload = pcall(ipmi.request, bus, {channel_type.CT_ME:value(), channel},
        {DestNetFn = 0x30, Cmd = 0x98, Payload = data})
    if not ok then
        log:debug('Send request by ipmi fail, err_message: %s', cc)
        return
    end

    if cc == comp_code.Success then
        local cc, vrd_power_info_table = parse_vrd_rsp_info(payload)
        return cc, vrd_power_info_table
    end
    return cc
end

function vrd_mgmt.get_cpu_obj_with_cpuid(id)
    local ok, objs = pcall(client.GetCPUObjects, client)
    if not ok then
        log:error('Get obj failed')
        return nil
    end
    for _, obj in pairs(objs) do
        -- CpuId匹配对象
        if id == obj.PhysicalId then
            return obj
        end
    end
    return nil
end

function vrd_mgmt.get_vrd_info_from_imu(bus, system_id, cpu_id)
    local cpu_obj = vrd_mgmt.get_cpu_obj_with_cpuid(cpu_id)
    if cpu_obj == nil then
        log:error('get cpu obj failed')
        return nil, nil
    end
    local channel = multihost_common:get_channel_id(system_id, cpu_obj.LogicalId)
    -- multihost场景下只获取ipmb消息中第一个cpu数据
    if channel ~= 0 then
        cpu_id = cpu_obj.LogicalId
    end
    return vrd_mgmt.get_vrd_info_from_single_host_imu(bus, channel, cpu_obj.LogicalId)
end

function vrd_mgmt:update_vrd_info(bus)
    self.retry = self.retry + 1
    local cc, vrd_power_info_table = vrd_mgmt.get_vrd_info_from_imu(bus, self.SystemId, self.CpuId)
    if cc and vrd_power_info_table then
        -- 重置retry count和状态
        self.retry = 0
        local max_temp = 0
        local power
        for _, v in ipairs(vrd_power_info_table) do
            power = vrd_power_service.get_instance():get_vrd_power(self.SystemId,  self.CpuId, v.Type, v.DieId)
            if not power then
                log:info('get vrd power failed, CpuId:%s, type:%s, DieId:%s', self.CpuId, v.Type, v.DieId)
            else
                power:update_info(v.Voltage, v.CurrentAmps, v.TemperatureCelsius)
            end
            max_temp = v.TemperatureCelsius > max_temp and v.TemperatureCelsius or max_temp
        end
        self.mds_obj.VrdTemperatureCelsius = max_temp
        self.mds_obj.Status = 0
    elseif cc == comp_code.CommandNotAvailable then
        self:reset_vrd_info()
    elseif self.mds_obj.Status == 0 and self.retry >= MAX_RETRY then
        log:debug('unable to get voltage and temperature in 3 times, set vrd values to invalid')
        self:set_vrd_to_invalid()
    end
end

function vrd_mgmt:ctor(obj, position)
    self.SystemId = obj.SystemId
    self.CpuId = obj.CpuId
    self.mds_obj = obj
    self.retry = 0
    self.position = position
end

return  vrd_mgmt