-- 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 log = require 'mc.logging'
local skynet = require 'skynet'
local c_object = require 'mc.orm.object'
local c_object_manage = require 'mc.orm.object_manage'
local ctx = require 'mc.context'
local client = require 'power_mgmt.client'
local psu_def = require 'macros.psu_def'
local utils = require 'power_mgmt_utils'
local c_psu_slot_object = require 'device.psu_slot'
local c_efficiency_curve_object = require 'efficiency_curve'
local system_power = require 'system_power'
local fw_def = require 'macros.fw_def'
local mdb = require 'mc.mdb'
local power_mgmt = require 'device.power_mgmt'
local custom_msg = require 'messages.custom'
local FWINVENTORY_PATH = '/bmc/kepler/UpdateService/FirmwareInventory/'
local FWINFO_INTERFACE = 'bmc.kepler.UpdateService.FirmwareInfo'
local enums = require 'macros.power_mgmt_enums'
local base_msg = require 'messages.base'

local E_OK <const> = nil -- 函数执行成功返回nil
local E_FAILED <const> = '' -- 空错误信息

local INPUT_ERROR_BIT <const> = 6
local DEFAULT <const> = -1

local psu_manager = c_object('OnePower')

function psu_manager:ctor()
    self.ps_id = 0
    self.psu_slot = c_psu_slot_object.collection:find({SlotNumber = self.SlotNumber})
    self.firmware_id = ''
    self.system_power_instance = system_power.get_instance()
    self.power_mgmt_instance = power_mgmt.get_instance()
    self.last_comm_status = 0
    self.last_firmware_version = ''
    self.last_rate = DEFAULT
    self.last_input_voltage = DEFAULT
end

function psu_manager:dtor()
    self:is_mismatch()
    self:unmount_fw_info()
end

function psu_manager:unmount_fw_info()
    local ok, err = client:FirmwareInventoryFirmwareInventoryDelete(
        ctx.new(),
        self.firmware_id
    )
    if not ok then
        log:error('unmount PSU%d firmware failed, err = %s', self.ps_id, err)
    else
        log:notice('unmount PSU%d firmware successfully', self.ps_id)
    end
end

function psu_manager:register_fw_info()
    -- 前向兼容，电源砖固件资源路径与普通电源保持一致
    local device_locator = string.gsub(self.DeviceLocator, "PowerConverter", "PSU")
    self.firmware_id = string.format('%s_%s', self.Position, device_locator)
    local ok, err = client:PFirmwareInventoryFirmwareInventoryAdd(
        ctx.new(),
        {
            Id = self.firmware_id,
            Name = self.DeviceLocator, Version = self.FirmwareVersion or '',
            BuildNum = '', ReleaseDate = '', LowestSupportedVersion = '',
            SoftwareId = 'PSU-' .. self.Model, Manufacturer = self.Manufacturer,
            Location = self.Position, State = 'Enabled', Severity = 'Informational'
        },
        true, 1, 10 -- 电源固件包当前最大10MB
    )
    if not ok then
        log:error('register PSU%d firmware failed, err = %s', self.ps_id, err)
    else
        log:notice('register PSU%d firmware successfully', self.ps_id)
    end
end

function psu_manager:update_firmware_info()
    local path = FWINVENTORY_PATH .. self.firmware_id
    local bus = c_object_manage.get_instance().bus
    local ok, obj = pcall(mdb.get_cached_object, bus, path, FWINFO_INTERFACE)
    if not ok then
        log:error('[PowerUpgrade] get firmware object fail, path: %s, error: %s', path, obj)
        return
    end
    if obj.Version ~= self.FirmwareVersion then
        obj.Version = self.FirmwareVersion
        log:notice('[PowerUpgrade] updated firmware version %s to FirmwareInventory, id: %s',
            obj.Version, self.firmware_id)
    end
end

function psu_manager:get_black_box_data()
    local ok, black_box_max_length, black_box_data = pcall(function ()
        return self.BlackBox.methods.GetBlackBoxData()
    end)
    if not ok then
        log:error('get_black_box_data failed, ps%s', self.ps_id)
        return E_FAILED, black_box_max_length
    end
    return E_OK, black_box_max_length, black_box_data
end

function psu_manager:set_work_mode(work_mode)
    local ok, resp = pcall(function ()
        self.PowerMode.methods.SetWorkMode(work_mode)
    end)
    if not ok then
        log:error('set ps%u work_mode to %s failed: %s', self.ps_id, work_mode, resp)
        return E_FAILED
    end
    return E_OK
end

function psu_manager:set_sleep_mode(sleep_mode)
    local ok, resp = pcall(function ()
        return self.PowerMode.methods.SetSleepMode(sleep_mode)
    end)
    if not ok then
        log:error('set ps%u sleep_mode to %s failed: %s', self.ps_id, sleep_mode, resp)
        if resp.name == base_msg.ActionNotSupportedMessage.Name then
            return enums.SLEEP_MODE_RELATED_ERROR.ACTION_NOT_SUPPORTED
        else
            return enums.SLEEP_MODE_RELATED_ERROR.OPERATION_FAILED
        end
    end
    return E_OK
end

function psu_manager:reset(reset_type)
    local ok, error_info = pcall(function ()
        return self.Power.methods.Reset(reset_type)
    end)
    if not ok or error_info then
        return error_info
    end
    return E_OK
end

function psu_manager:set_psu_fan_rpm(rpm)
    local ok, resp = pcall(function ()
        self.Cooling.methods.SetFanRPM(rpm)
    end)
    if not ok then
        log:error('set ps%u psu_fan_rpm to %s failed, error: %s', self.ps_id, rpm, resp)
        return E_FAILED
    end
    return E_OK
end

function psu_manager:set_psu_power_latch()
    local ok, resp = pcall(function ()
        if not self.Power.methods.PowerLatch then
            log:notice('ps%s not support PowerLatch', self.ps_id)
            return E_FAILED
        end
        return self.Power.methods.PowerLatch()
    end)
    if not ok then
        log:error('set ps%s set_psu_power_latch failed, error: %s', self.ps_id, resp)
        return E_FAILED
    end
    return resp
end

function psu_manager:set_alarm_latch(switch_status)
    local ok, error_info = pcall(function ()
        return self.Power.methods.SetAlarmLatch(switch_status)
    end)
    if not ok or error_info then
        return error_info
    end
    return E_OK
end

function psu_manager:set_power_supply_circuit(circuit)
    local ok, error_info = pcall(function ()
        return self.Power.methods.SetPowerSupplyCircuit(circuit)
    end)
    if not ok or error_info then
        return error_info
    end
    return E_OK
end

function psu_manager:set_output_limit_point_watts(output_power_limit_watts)
    local ok, error_info = pcall(function ()
        return self.Power.methods.SetOutputPowerLimitWatts(output_power_limit_watts)
    end)
    if not ok or error_info then
        return error_info
    end
    return E_OK
end

function psu_manager:set_retransfer_delay_seconds(second)
    local ok, resp = pcall(function ()
        return self.Power.methods.SetRetransferDelaySeconds(second)
    end)
    if not ok then
        log:error('set_retransfer_delay_seconds %s failed: %s', self.ps_id, resp)
        return E_FAILED
    end
    return E_OK
end

function psu_manager:get_retransfer_delay_seconds()
    local ok, resp = pcall(function ()
        return self.Power.methods.GetRetransferDelaySeconds()
    end)
    if not ok then
        log:error('get_retransfer_delay_seconds failed, ps%s', self.ps_id)
        error(custom_msg.OperationFailed())
    end
    return resp
end

function psu_manager:set_vout_debounce_milliseconds(debounce_milliseconds)
    local ok, error_info = pcall(function ()
        return self.Power.methods.SetVOUTDebounceMilliseconds(debounce_milliseconds)
    end)
    if not ok or error_info then
        return error_info
    end
    return E_OK
end

function psu_manager:set_power_capacitor_calibrate()
    local ok, error_info = pcall(function ()
        return self.Power.methods.SetPowerCapacitorCalibrate()
    end)
    if not ok or error_info then
        return error_info
    end
    return E_OK
end

function psu_manager:get_power_capacitor_calibrate_time()
    local ok, resp = pcall(function ()
        return self.Power.methods.GetPowerCapacitorCalibrateTime()
    end)
    if not ok then
        return resp
    end
    return resp
end

function psu_manager:get_power_capacitor_calibrate()
    local ok, error_info = pcall(function ()
        return self.Power.methods.GetPowerCapacitorCalibrate()
    end)
    if not ok or error_info then
        return error_info
    end
    return E_OK
end

function psu_manager:set_depth_of_discharge_volts(depth_of_discharge_volts)
    local ok, error_info = pcall(function ()
        return self.Power.methods.SetDepthOfDischargeVolts(depth_of_discharge_volts)
    end)
    if not ok or error_info then
        return error_info
    end
    return E_OK
end

function psu_manager:set_power_capacitor_enable(enable)
    local ok, resp = pcall(function ()
        if not self.Power.methods.SetPowerCapacitorEnable then
            log:error('ps%s not support Set PowerCapacitorEnable', self.ps_id)
            return E_FAILED
        end
        return self.Power.methods.SetPowerCapacitorEnable(enable)
    end)
    if not ok then
        resp = E_FAILED
        log:error('set ps%d capacitor enable failed', self.ps_id, resp)
    end
    return resp
end

function psu_manager:get_upgrade_channel()
    local ok, resp = pcall(function ()
        if not self.Upgrade.methods.GetUpgradeChannel then
            log:notice('ps%s not support GetUpgradeChannel', self.ps_id)
            return E_FAILED
        end
        return self.Upgrade.methods.GetUpgradeChannel()
    end)
    if not ok then
        resp = E_FAILED
        log:error('set ps%s get_upgrade_channel failed, error: %s', self.ps_id, resp)
    end
    return resp
end

function psu_manager:change_polling_channel(upgrade_channel)
    local ok, resp = pcall(function ()
        if not self.Upgrade.methods.ChangePollingChannel then
            log:notice('ps%s not support ChangePollingChannel', self.ps_id)
            return E_FAILED
        end
        return self.Upgrade.methods.ChangePollingChannel(upgrade_channel)
    end)
    if not ok then
        resp = E_FAILED
        log:error('set ps%s change_polling_channel failed, error: %s', self.ps_id, resp)
    end
    return resp
end

function psu_manager:recover_polling_channel()
    local ok, resp = pcall(function ()
        if not self.Upgrade.methods.RecoverPollingChannel then
            log:notice('ps%s not support RecoverPollingChannel', self.ps_id)
            return E_FAILED
        end
        return self.Upgrade.methods.RecoverPollingChannel()
    end)
    if not ok then
        resp = E_FAILED
        log:error('set ps%s recover_polling_channel failed, error: %s', self.ps_id, resp)
    end
    return resp
end

function psu_manager:get_psu_fan_rpm()
    local ok, resp = pcall(function ()
        return self.Cooling.methods.GetFanRPM()
    end)
    if not ok then
        log:error('get ps%u psu_fan_rpm failed, error: %s', self.ps_id, resp)
        error(custom_msg.OperationFailed())
    end
    return resp
end

function psu_manager:get_psu_register_info(cmd, length)
    local ok, resp = pcall(function ()
        return self.Power.methods.GetPsuRegisterInfo(cmd, length)
    end)
    if not ok then
        log:error('get ps%u register info failed, error: %s', self.ps_id, resp)
        error(custom_msg.OperationFailed())
    end
    return resp
end

function psu_manager:get(prop, ...)
    local ok, resp = pcall(self.psu_slot.get, self.psu_slot, prop, ...)
    if not ok then
        return nil, string.format('get ps%u %s failed: %s', self.ps_id, prop, resp)
    end
    return resp, E_OK
end

function psu_manager:is_healthy()
    return self.Health == psu_def.IS_HEALTH and self.OutputState ~= 0 and self.AlarmStatus & INPUT_ERROR_BIT == 0
end

function psu_manager:power_upgrade(upgrade_path, upgrade_process, para_tab)
    local ok, resp = pcall(function()
        return self.Upgrade.methods.Upgrade(upgrade_path, upgrade_process, para_tab)
    end)
    self:next_tick(function()
        self:update_firmware_info()
    end)
    -- 电源升级完成后刷新一次电子标签
    self:next_tick(function()
        local config_value = self.system_power_instance:get_psu_fru_config()
        self.Power.methods.GetFruData(config_value)
    end)
    if ok then
        return resp
    end
    log:error('ps%d upgrade failed, resp = %s', self.ps_id, resp)
    return fw_def.SUPPLY_1
end

function psu_manager:psm_monitor_stop()
    local ok, resp = pcall(function()
        return self.Power.methods.PowerAccessControl(false)
    end)
    if not ok then
        log:error('ps%d contorl stop failed, ret: %s', self.ps_id, resp)
        return resp
    end
    return E_OK
end

function psu_manager:psm_monitor_start()
    local ok, resp = pcall(function()
        return self.Power.methods.PowerAccessControl(true)
    end)
    if not ok then
        log:error('ps%d contorl start failed, ret: %s', self.ps_id, resp)
        return resp
    end
    return E_OK
end

function psu_manager:update_power_level()
    if self.PowerLevel ~= "N/A" then
        log:debug("Power level has been updated to %s", self.PowerLevel)
        return
    end
    for _ = 1, 10, 1 do
        if not self.position or not self.PartNumber then
            goto continue
        end
        c_efficiency_curve_object.collection:fold(function(_, obj)
            if obj.Position == self.position and obj.PartNumber == self.PartNumber and obj.PowerLevel
                and obj.PowerLevel ~= "N/A" then
                self.PowerLevel = obj.PowerLevel
            end
        end)
        if self.PowerLevel ~= "N/A" then
            break
        end
        ::continue::
        skynet.sleep(1000)
    end
    if self.PowerLevel == "N/A" then
        log:error('Update power[%s] level failed', self.position)
        return
    end
    log:notice('Update power[%s] level successfully, current level: %s', self.position, self.PowerLevel)
end

function psu_manager:update_efficiency_curve()
    local curve = false
    -- 读取10次，每次间隔十秒，如果100秒内读取不到电源信息，就退出
    for _ = 1, 10, 1 do
        if not self.position or not self.PartNumber or not self.InputVoltage or self.InputVoltage == 0 then
            goto continue
        end
        curve = c_efficiency_curve_object.collection:find(function(obj)
            return obj.Position == self.position and obj.PartNumber == self.PartNumber and
                math.abs(obj.InputVoltage - self.InputVoltage) < 10 
        end)
        if curve then
            break
        end
        ::continue::
        skynet.sleep(1000)
    end
    if curve and curve.PowerLevel ~= "N/A" then
        self.PowerLevel = curve.PowerLevel
        log:notice('Update power[%s] level successfully, current level: %s', self.position, self.PowerLevel)
    end
    if not curve or not curve.LoadPercentRange or not curve.EfficiencyCurve then
        log:error('Load power[%s] efficiency curve failed', self.position)
        return
    end
    if #curve.LoadPercentRange ~= #curve.EfficiencyCurve then
        log:error('Power[%s] efficiency is configured incorrectly', self.PartNumber)
        return
    end
    -- 插入数据前先清空资源树，保证数据干净
    self.EfficiencyCurve = {}
    for i = 1, #curve.LoadPercentRange do
        table.insert(self.EfficiencyCurve, {curve.LoadPercentRange[i], curve.EfficiencyCurve[i]})
    end
end

function psu_manager:is_attribute_mismatch(attribute)
    local mismatch = false
    local attribute_value
    psu_manager.collection:fold(function (_, psu_obj)
        if not attribute_value then
            attribute_value = psu_obj[attribute]
        elseif psu_obj[attribute] ~= attribute_value then
            mismatch = true
        end
    end)
    return mismatch
end

function psu_manager:is_mismatch()
    local is_manufacturer_mismatch = self:is_attribute_mismatch("Manufacturer")
    local is_model_mismatch = self:is_attribute_mismatch("Model")
    log:notice("For power supplies, manufacturer mismatch: %s, model mismatch: %s",
        is_manufacturer_mismatch, is_model_mismatch)
    self.power_mgmt_instance:set_value("IsManufacturerMismatch", is_manufacturer_mismatch)
    self.power_mgmt_instance:set_value("IsModelMismatch", is_model_mismatch)
end

function psu_manager.create_mdb_object()
end

function psu_manager:update_firmware()
    log:notice('The firmware version of PSU%s changes from %s to %s',
        self.ps_id, self.last_firmware_version, self.FirmwareVersion)
    self:update_firmware_info()
    self.last_firmware_version = self.FirmwareVersion
end

function psu_manager:update_rate()
    if self.last_rate ~= DEFAULT then
        log:maintenance(log.MLOG_INFO, log.FC__PUBLIC_OK, 
            'The rate of PSU%s changes from %s to %s, and the input voltage changes from %s to %s',
            self.ps_id, self.last_rate, self.Rate, self.last_input_voltage, self.InputVoltage)
    end
    self.last_rate = self.Rate
    self.last_input_voltage = self.InputVoltage
end

function psu_manager:prop_listening_register()
    self:listen('CommunicationStatus', function ()
        log:notice('The commuinication status of PSU%s changes from %s to %s', self.ps_id,
            self.last_comm_status, self.CommunicationStatus)
        if self.last_comm_status == 1 and self.CommunicationStatus == 0 then
            self.Power.methods.UpdatePowerSupplyInfo()
            self:update_firmware_info()
            local config_value = self.system_power_instance:get_psu_fru_config()
            self.Power.methods.GetFruData(config_value)
        end
        self.last_comm_status = self.CommunicationStatus
    end)
    self:listen('FirmwareVersion', function() self:update_firmware() end)
    self:listen('Rate', function() self:update_rate() end)
end

function psu_manager:init()
    self.ps_id = self.SlotNumber
    self.last_comm_status = self.CommunicationStatus
    self:connect_signal(self.on_add_object_complete, function()
        self:next_tick(function()
            self:register_fw_info()
            self:prop_listening_register()
        end)
        -- 检测到OnePower对象分发时要刷新一次电子标签
        self:next_tick(function()
            local config_value = self.system_power_instance:get_psu_fru_config()
            self.Power.methods.GetFruData(config_value)
        end)
        -- 添加电源转换效率曲线
        self:next_tick(function()
            self:update_efficiency_curve()
            self:update_power_level()
        end)
        -- 电源是否一致匹配
        self:next_tick(function() self:is_mismatch() end)
    end)
    psu_manager.super.init(self)
end

return psu_manager