-- 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 skynet = require 'skynet'
local ipmi_req = require 'power_mgmt.ipmi.ipmi'
local msg = require 'power_mgmt.ipmi.ipmi_message'
local psu_service = require 'psu_service'
local c_psu_object = require 'device.psu'
local c_psu_slot_object = require 'device.psu_slot'
local log = require 'mc.logging'
local ipmi = require 'ipmi'
local custom_msg = require 'messages.custom'
local comp_code = ipmi.types.Cc
local psu_def = require 'macros.psu_def'
local client = require 'power_mgmt.client'
local system_power = require 'system_power'
local enums = require 'macros.power_mgmt_enums'
local power_mgmt_utils = require 'power_mgmt_utils'

local PS_INFO_MAX_LEN <const> = 128
local PSU_STATUS_MFR_PSON_LOW_MASK <const> = 0x80
local PSM_GET_POWER_SUPPLY_INFO_VOUT_WORK_MODE <const> = 16
local E_OK <const> = nil

local expect_work_mode = {}
local active_work_mode = {}
-- 机框最大电源个数
local max_chassis_psu_num = 0

local power_ipmi = {}

local function get_supply_info(prop_type)
    return ({
        ['string'] = function(prop_val, offset, length, man_id)
            if not prop_val then
                return msg.GetPowerSupplyInfoRsp.new(comp_code.UnspecifiedError, man_id, 0, "0")
            end
            if offset >= #prop_val or length > PS_INFO_MAX_LEN then
                log:error('[power_mgmt] the offset[%#x] or len[%#x] is too large', offset, length)
                return msg.GetPowerSupplyInfoRsp.new(comp_code.ParmOutOfRange, man_id, 0, "0")
            end
            return msg.GetPowerSupplyInfoRsp.new(comp_code.Success, man_id, offset + length >= #prop_val and 1 or 0,
                prop_val:sub(offset + 1, offset + length))
        end,
        ['double'] = function(prop_val, offset, length, man_id)
            if not prop_val then
                return msg.GetPowerSupplyInfoRsp.new(comp_code.UnspecifiedError, man_id, 0, "0")
            end
            return msg.GetPowerSupplyInfoRsp.new(comp_code.Success, man_id, 0, string.pack('d', prop_val))
        end,
        ['uint16'] = function(prop_val, offset, length, man_id)
            if not prop_val or prop_val == 0 then
                return msg.GetPowerSupplyInfoRsp.new(comp_code.UnspecifiedError, man_id, 0, "0")
            end
            return msg.GetPowerSupplyInfoRsp.new(comp_code.Success, man_id, 0, string.pack('H', prop_val))
        end,
        ['byte'] = function(prop_val, offset, length, man_id)
            if not prop_val then
                return msg.GetPowerSupplyInfoRsp.new(comp_code.UnspecifiedError, man_id, 0, "0")
            end
            return msg.GetPowerSupplyInfoRsp.new(comp_code.Success, man_id, 0, string.pack('B', prop_val))
        end
    })[prop_type]
end

local supply_info = {
    [1] = function(psu_service, req)
        local prop_val = psu_service:get_psu_property(req['PsSelector'], "Manufacturer")
        return get_supply_info('string')(prop_val, req['ReadOffset'], req['Length'], req['ManufactureId'])
    end,
    [2] = function(psu_service, req)
        local prop_val = psu_service:get_psu_property(req['PsSelector'], "PowerSupplyType")
        return get_supply_info('byte')(prop_val, req['ReadOffset'], req['Length'], req['ManufactureId'])
    end,
    [3] = function(psu_service, req)
        local prop_val = psu_service:get_psu_property(req['PsSelector'], "Model")
        return get_supply_info('string')(prop_val, req['ReadOffset'], req['Length'], req['ManufactureId'])
    end,
    [4] = function(psu_service, req)
        local prop_val = psu_service:get_psu_property(req['PsSelector'], "FirmwareVersion")
        return get_supply_info('string')(prop_val, req['ReadOffset'], req['Length'], req['ManufactureId'])
    end,
    [5] = function(psu_service, req)
        local prop_val = psu_service:get_psu_property(req['PsSelector'], "Protocol")
        return get_supply_info('byte')(psu_def.PROTOCOL[string.upper(prop_val)],
            req['ReadOffset'], req['Length'], req['ManufactureId'])
    end,
    [6] = function(psu_service, req)
        local prop_val = psu_service:get_psu_property(req['PsSelector'], "Rate")
        return get_supply_info('uint16')(prop_val, req['ReadOffset'], req['Length'], req['ManufactureId'])
    end,
    [7] = function(psu_service, req)
        local prop_val = psu_service:get_psu_property(req['PsSelector'], "InputVoltage")
        return get_supply_info('double')(prop_val, req['ReadOffset'], req['Length'], req['ManufactureId'])
    end,
    [8] = function(psu_service, req)
        local prop_val = psu_service:get_psu_property(req['PsSelector'], "OutputVoltage")
        return get_supply_info('double')(prop_val, req['ReadOffset'], req['Length'], req['ManufactureId'])
    end,
    [9] = function(psu_service, req)
        local prop_val = psu_service:get_psu_property(req['PsSelector'], "InputCurrentAmps")
        return get_supply_info('double')(prop_val, req['ReadOffset'], req['Length'], req['ManufactureId'])
    end,
    [10] = function(psu_service, req)
        local prop_val = psu_service:get_psu_property(req['PsSelector'], "OutputCurrentAmps")
        return get_supply_info('double')(prop_val, req['ReadOffset'], req['Length'], req['ManufactureId'])
    end,
    [11] = function(psu_service, req)
        -- ps_switch
        return msg.GetPowerSupplyInfoRsp.new(comp_code.Success, req['ManufactureId'], 0, string.pack('B', 1))
    end,
    [12] = function(psu_service, req)
        local prop_val = psu_service:get_psu_property(req['PsSelector'], "SerialNumber")
        return get_supply_info('string')(prop_val, req['ReadOffset'], req['Length'], req['ManufactureId'])
    end,
    [13] = function(psu_service, req)
        local prop_val = psu_service:get_psu_property(req['PsSelector'], "EnvTemperatureCelsius")
        return get_supply_info('double')(prop_val, req['ReadOffset'], req['Length'], req['ManufactureId'])
    end,
    [14] = function(psu_service, req)
        local prop_val = psu_service:get_psu_property(req['PsSelector'], "InputPowerWatts")
        return get_supply_info('double')(prop_val, req['ReadOffset'], req['Length'], req['ManufactureId'])
    end,
    [15] = function(psu_service, req)
        local prop_val = psu_service:get_psu_property(req['PsSelector'], "OutputPowerWatts")
        return get_supply_info('double')(prop_val, req['ReadOffset'], req['Length'], req['ManufactureId'])
    end,
    [16] = function(psu_service, req)
        return msg.GetPowerSupplyInfoRsp.new(comp_code.Success, req['ManufactureId'], 0, string.pack('L', 0))
    end,
    [17] = function(psu_service, req)
        local prop_val = psu_service:get_psu_property(req['PsSelector'], "SourceType")
        return get_supply_info('byte')(prop_val, req['ReadOffset'], req['Length'], req['ManufactureId'])
    end,
    [18] = function(psu_service, req)
        local prop_val = (psu_service:get_psu_property(req['PsSelector'], "WorkMode") == 'Enabled') and
            0 or 1
        return msg.GetPowerSupplyInfoRsp.new(comp_code.Success, req['ManufactureId'], 0,
            string.pack('I4', prop_val << (req['PsSelector'] - 1)))
    end,
    [19] = function(psu_service, req)
        local prop_val = psu_service:get_psu_property(req['PsSelector'], "OutputState")
        return get_supply_info('byte')(prop_val, req['ReadOffset'], req['Length'], req['ManufactureId'])
    end,
    [20] = function(psu_service, req)
        -- mfr_status
        local prop_val = psu_service:get_mfr_reg_value(req['PsSelector'])
        if not prop_val then
            return msg.GetPowerSupplyInfoRsp.new(comp_code.InvalidFieldRequest, req['ManufactureId'], 0, "0")
        end
        return msg.GetPowerSupplyInfoRsp.new(comp_code.Success, req['ManufactureId'], 0,
            string.pack('B', prop_val))
    end,
    [21] = function(psu_service, req)
        -- ps_on
        local prop_val = psu_service:get_mfr_reg_value(req['PsSelector'])
        if not prop_val then
            return msg.GetPowerSupplyInfoRsp.new(comp_code.InvalidFieldRequest, req['ManufactureId'], 0, "0")
        end
        return msg.GetPowerSupplyInfoRsp.new(comp_code.Success, req['ManufactureId'], 0,
            string.pack('B', prop_val & PSU_STATUS_MFR_PSON_LOW_MASK > 0 and 1 or 0))
    end,
    [enums.POWER_SUPPLY_INFO_TYPE.SET_GET_POWER_SUPPLY_DEEP_SLEEP_ENABLED_STATUS] = function(psu_service, req)
        if req['ReadOffset'] ~= 0 or req['Length'] ~= 1 then
            return msg.GetPowerSupplyInfoRsp.new(comp_code.ParmOutOfRange, req['ManufactureId'], 0, "0")
        end
        local prop_val = psu_service:get_psu_property(req['PsSelector'], "SleepMode")
        if not prop_val or prop_val == "" then
            return msg.GetPowerSupplyInfoRsp.new(comp_code.DataNotAvailable, req['ManufactureId'], 0, "0")
        end
        return msg.GetPowerSupplyInfoRsp.new(
            comp_code.Success, req['ManufactureId'], 0, string.pack('B', enums.SLEEP_MODE[prop_val]))
    end
}

local function get_power_supply_info(req, ctx)
    if not c_psu_object.collection:find({SlotNumber = req['PsSelector']}) then
        return msg.GetPowerSupplyInfoRsp.new(comp_code.ParmOutOfRange, req['ManufactureId'], 0, "0")
    end
    local resp = (supply_info[req["PsParameter"]] or function() end)(psu_service.get_instance(), req)
    return resp or msg.GetPowerSupplyInfoRsp.new(comp_code.ParmOutOfRange, req['ManufactureId'], 0, "0")
end

local update_work_mode_task_id = {}
local function update_work_mode(ps_id)
    local work_mode = expect_work_mode[ps_id]
    local res = psu_service.get_instance():set_power_work_mode(ps_id, work_mode)
    if res ~= E_OK then
        log:error('[power_mgmt]set single powersupply status: set mode fail')
    end
    active_work_mode[ps_id] = work_mode
    update_work_mode_task_id[ps_id] = nil
    if active_work_mode[ps_id] ~= expect_work_mode[ps_id] then
        update_work_mode_task_id[ps_id] = skynet.fork(update_work_mode, ps_id)
    end
end

local function set_psu_sleep_mode(req, ctx)
    local software_type = power_mgmt_utils.get_instance():get_chassis_type()
    if not software_type then
        ipmi.ipmi_operation_log(ctx, 'Power_Mgmt', "Set ps%s sleep mode failed", req['PsSelector'])
        return msg.SetPowerStatusRsp.new(comp_code.UnspecifiedError, req['ManufactureId'])
    elseif software_type ~= enums.SOFTWARE_TYPE.CABINET then
        ipmi.ipmi_operation_log(ctx, 'Power_Mgmt', "Set ps%s sleep mode failed", req['PsSelector'])
        return msg.SetPowerStatusRsp.new(comp_code.InvalidCommand, req['ManufactureId'])
    end
    if req['Flag'] ~= 0 or req['Offset'] ~= 0 or req['Length'] ~= 1 then
        log:error('Set psu sleep mode failed, request param is incorrect')
        return msg.SetPowerStatusRsp.new(comp_code.ParmOutOfRange, req['ManufactureId'])
    end
    local sleep_mode = string.unpack('B', req.Data)
    if sleep_mode ~= enums.SLEEP_MODE.Normal and sleep_mode ~= enums.SLEEP_MODE.DeepSleep then
        log:error('Set psu sleep mode failed, sleep mode is incorrect')
        return msg.SetPowerStatusRsp.new(comp_code.ParmOutOfRange, req['ManufactureId'])
    end
    local ret = psu_service.get_instance():set_power_sleep_mode(req['PsSelector'], enums.SLEEP_MODE_NUM[sleep_mode])
    if ret == E_OK then
        ipmi.ipmi_operation_log(
            ctx, 'Power_Mgmt', "Set ps%s sleep mode to %s successfully", req['PsSelector'],
                enums.SLEEP_MODE_NUM[sleep_mode]
        )
        return msg.SetPowerStatusRsp.new(comp_code.Success, req['ManufactureId'])
    end
    ipmi.ipmi_operation_log(ctx, 'Power_Mgmt', "Set ps%s sleep mode failed", req['PsSelector'])
    if ret == enums.SLEEP_MODE_RELATED_ERROR.OPERATION_FAILED then
        return msg.SetPowerStatusRsp.new(comp_code.UnspecifiedError, req['ManufactureId'])
    elseif ret == enums.SLEEP_MODE_RELATED_ERROR.ACTION_NOT_SUPPORTED then
        return msg.SetPowerStatusRsp.new(comp_code.DataNotAvailable, req['ManufactureId'])
    elseif ret == enums.SLEEP_MODE_RELATED_ERROR.PROPERTY_VALUE_OUT_OF_RANGE then
        return msg.SetPowerStatusRsp.new(comp_code.ParmOutOfRange, req['ManufactureId'])
    end
end

local function set_single_powersupply_status(req, ctx)
    local len = req.Length
    if len < 4 or #req.Data < 4 then
        log:error('[power_mgmt]set single powersupply status: data len invalid')
        ipmi.ipmi_operation_log(ctx, 'Power_Mgmt',
            "Set PSU active/standy failed, invalid parameters, len = %u", len)
        return msg.SetPowerStatusRsp.new(comp_code.ReqDataLenInvalid, req['ManufactureId'])
    end

    local ps_index = req.PsSelector
    local active_standby_msk = string.unpack('I4', string.sub(req.Data, 1, 4))
    expect_work_mode[ps_index] = ((active_standby_msk >> (ps_index - 1)) & 1)
    local mode = expect_work_mode[ps_index] > 0 and 'Standy' or 'Active'

    local is_ps_exist = c_psu_object.collection:find({SlotNumber = ps_index})
    if not is_ps_exist then
        log:error('[power_mgmt]psu index(%s) is invalid', ps_index)
        ipmi.ipmi_operation_log(ctx, 'Power_Mgmt', "Set PSU %u %s failed", ps_index, mode)
        return msg.SetPowerStatusRsp.new(comp_code.ParmOutOfRange, req['ManufactureId'])
    end

    if not update_work_mode_task_id[ps_index] then
        -- 不存在刷新任务，则创建任务；若已有任务，会反复刷新工作模式
        update_work_mode_task_id[ps_index] = skynet.fork(update_work_mode, ps_index)
    end

    ipmi.ipmi_operation_log(ctx, 'Power_Mgmt', "Send Set PSU %u %s successfully", ps_index, mode)
    return msg.SetPowerStatusRsp.new(comp_code.Success, req['ManufactureId'])
end

local set_status_fun = {
    [PSM_GET_POWER_SUPPLY_INFO_VOUT_WORK_MODE] = function(req, ctx)
        return set_single_powersupply_status(req, ctx)
    end,
    [enums.POWER_SUPPLY_INFO_TYPE.SET_GET_POWER_SUPPLY_DEEP_SLEEP_ENABLED_STATUS] = function(req, ctx)
        return set_psu_sleep_mode(req, ctx)
    end
}

local function set_power_status(req, ctx)
    local ps_param = req.PsParameter
    local set_fun = set_status_fun[ps_param]
    if not set_fun then
        log:error('[power_mgmt]set fun is nil')
        return msg.SetPowerStatusRsp.new(comp_code.InvalidCommand, req['ManufactureId'])
    end

    return set_fun(req, ctx)
end

local function get_max_psu_device_number()
    if max_chassis_psu_num ~= 0 then
        return max_chassis_psu_num
    end
    -- 获取id为1的对象
    local ok, obj = pcall(client.GetChassisChassisObject, client, { ChassisId = 1 })
    local max_psu = 0
    if ok and obj then
        for _, v in pairs(obj.DeviceSpecication or {}) do
            if v[1] == 'Psu' then
                max_psu = v[2]
                break
            end
        end
    end
    if max_psu == 0 then
        -- chassis延时2min启动查询
        -- chassis未获取最大设备数时，使用psu_slot作为最大支持电源数
        max_psu = #c_psu_slot_object.collection.objects
    end
    max_chassis_psu_num = max_psu
    return max_psu
end

local function check_and_get_psu_service(psu_id)
    if psu_id < 1 then
        error(custom_msg.IPMICommandCannotExecute())
    end

    local max_psu = get_max_psu_device_number()

    if psu_id > max_psu then
        error(custom_msg.IPMIOutOfRange())
    end

    return psu_service.get_instance()
end

local power_supplyinfo_arr <const> = {
    { "InputVoltage",      ipmi_req.GetThresholdSensorReadingVoltageInput },
    { "OutputVoltage",     ipmi_req.GetThresholdSensorReadingVoltageOutput },
    { "InputCurrentAmps",  ipmi_req.GetThresholdSensorReadingCurrentInput },
    { "OutputCurrentAmps", ipmi_req.GetThresholdSensorReadingCurrentOutput },
    { "InputPowerWatts",   ipmi_req.GetThresholdSensorReadingPowerInput },
    { "OutputPowerWatts",  ipmi_req.GetThresholdSensorReadingPowerOutput },
}

-- 根据读值单位不对读数放大的属性
-- 对毫安、毫伏既定单位的读值放大1000倍,对瓦特单位的读值读取原数
local const_units = {
    [1] = "InputPowerWatts",
    [2] = "OutputPowerWatts"
}

local function const_unit_check(req, psu_service, selected_arr)
    local reading = math.floor(psu_service:get_psu_property(req['DeviceId'], selected_arr[1]) * 1000) 
    for key, selectMode in ipairs(const_units) do
        if selectMode == selected_arr[1] then
            reading = math.floor(psu_service:get_psu_property(req['DeviceId'], selected_arr[1]))
        end
    end
    return reading
end

local function get_threshold_sensor_reading(req, ctx, selectMode)
    -- 判断psu_service是否存在，DeviceId是否合法
    local service = check_and_get_psu_service(req['DeviceId'])
    if not selectMode then
        log:error('selectMode function is nil')
        error(custom_msg.IPMIOutOfRange())
    end

    local selected_arr = power_supplyinfo_arr[selectMode]
    local rsp = selected_arr[2].rsp.new()

    rsp.CompletionCode = comp_code.Success
    -- Reading按照IPMI接口说明，只能为有符号int类型
    local var = const_unit_check(req, service, selected_arr)
    if var == nil then
        error(custom_msg.IPMIOutOfRange())
    end
    rsp.ManufactureId = req['ManufactureId']
    rsp.Reading = var
    return rsp
end

local function set_psu_fru_product_version_config(req, ctx)
    local config_value = req.ConfigValue
    -- 0:Hardware 1:Software
    if config_value > 1 then
        log:error('optios(%d) is invalid, expect 0 or 1', config_value)
        ipmi.ipmi_operation_log(ctx, 'Power_Mgmt', 'Failed to set PSU FRU configuration (Product Version)')
        error(custom_msg.IPMIInvalidFieldRequest())
    end
    local mode = config_value == 0 and 'Hardware' or 'Software'
    system_power.get_instance():set_psu_fru_config(config_value)
    ipmi.ipmi_operation_log(
        ctx, 'Power_Mgmt', "Successfully set PSU FRU configuration (Product Version) to %s Version", mode
    )
    local rsp = msg.SetPsuFruConfigRsp.new()
    rsp.CompletionCode = comp_code.Success
    rsp.ManufactureId = req['ManufactureId']
    return rsp
end
 
local function get_psu_fru_product_version_config(req, ctx)
    local config_value = system_power.get_instance():get_psu_fru_config()
    local rsp = msg.GetPsuFruConfigRsp.new()
    rsp.CompletionCode = comp_code.Success
    rsp.ManufactureId = req['ManufactureId']
    rsp.ConfigValue = config_value
    return rsp
end

function power_ipmi.register(register_callback)
    register_callback(ipmi_req.GetPowerSupplyInfo, get_power_supply_info)
    register_callback(ipmi_req.SetPowerStatus, set_power_status)
    for index, value in ipairs(power_supplyinfo_arr) do
        register_callback(value[2],
            function(req, ctx) return get_threshold_sensor_reading(req, ctx, index) end)
    end
    
    register_callback(ipmi_req.SetPsuFruConfig, set_psu_fru_product_version_config)
    register_callback(ipmi_req.GetPsuFruConfig, get_psu_fru_product_version_config)
end

return power_ipmi