-- 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 mdb_service = require 'mc.mdb.mdb_service'
local cjson = require 'cjson'
local log = require 'mc.logging'
local context = require 'mc.context'

local m = {}

-- 电源资源树接口
local ON_POWER_MDB_INTF <const> = "bmc.kepler.Systems.PowerMgmt.OnePower"
local METRICS_MDB_INTF <const> = "bmc.kepler.Systems.PowerMgmt.OnePower.Metrics"
local NPUBOARD_MDB_INTF <const> = "bmc.kepler.Systems.Board.NpuBoard"
local POWER_SUPPLY_PATH <const> = '/bmc/kepler/Systems/1/PowerMgmt'
local POWER_SUPPLY_INTERFACE <const> = 'bmc.kepler.Systems.PowerMgmt.OnePower'
local POWER_SUPPLY_METRICS_INTERFACE <const> = 'bmc.kepler.Systems.PowerMgmt.OnePower.Metrics'
local POWER_STRATEGY_INTERFACE <const> = 'bmc.kepler.Systems.PowerStrategy'
local INTERFACE_COUNT <const> = 1
local INDEX_ABSENT <const> = 0

local m_supply_type<const> = {['0'] = 'DC', ['1'] = 'AC', ['2'] = 'ACorDC'}
local m_supply_channel<const> = {['0'] = 'MainCircuit', ['1'] = 'BackupCircuit'}

local function is_include(table, value)
    for _, v in ipairs(table) do
        if v == value then
            return true
        end
    end
    return false
end

---获取在位电源的个数
---@param power_paths any
---@return integer
function m.get_presence_count(power_paths)
    local count = 0
    for _, path in ipairs(power_paths) do
        local obj = mdb.get_object(bus, path, ON_POWER_MDB_INTF)
        if obj.Presence == 1 then
            count = count + 1
        end
    end

    return count
end

---获取服务器所有电源额定功率之和
---@param power_paths any
function m.get_capacity_watts(power_paths)
    local capacity_watts = 0
    for _, path in ipairs(power_paths) do
        local obj = mdb.get_object(bus, path, METRICS_MDB_INTF)
        capacity_watts = capacity_watts + obj.Rate
    end

    return capacity_watts
end

---当PowerStrategy不存在时计算电源总功率
---@param power_paths any
---@param power_consumed_watts any
function m.get_total_watts(power_paths, power_consumed_watts)
    if power_consumed_watts then
        return power_consumed_watts
    end
    local total_watts = 0
    for _, path in ipairs(power_paths) do
        local obj = mdb.get_object(bus, path, METRICS_MDB_INTF)
        total_watts = total_watts + obj.InputPowerWatts
    end

    return total_watts
end

---获取服务器所有电源额定功率以及npu的总功率和
---@param power_paths any
function m.get_capacity_watts_and_npu_watts(power_paths, npu_paths)
    local capacity_watts = m.get_capacity_watts(power_paths)
    for _, path in ipairs(npu_paths) do
        local obj = mdb.get_object(bus, path, NPUBOARD_MDB_INTF)
        capacity_watts = capacity_watts + obj.MaxPowerCapWatts
    end
    return capacity_watts
end

function m.check_power_active(slot_number, expected_active_psu)
    for _, slot in pairs(expected_active_psu) do
        if slot == slot_number then
            return true
        end
    end
    return false
end

local function get_state(supply_item)
    local presense = supply_item.Presence
    if presense == INDEX_ABSENT then
        return 'Absent'
    end
    return nil;
end

local function get_mode(bus, supply_item)
    local ok, power_strategy = pcall(function ()
        return mdb.get_object(bus, '/bmc/kepler/Systems/1/PowerStrategy', POWER_STRATEGY_INTERFACE)
    end)
    if not ok or not power_strategy then
        log:notice('get power_strategy failed, error:%s', power_strategy)
        return nil
    end
    local actual_mode = power_strategy.PowerActualWorkingMode

    if actual_mode == 'LoadBalancing' then
        return 'Shared'
    end

    if actual_mode == 'Active/Standby' then
        return supply_item.WorkMode == 'Enabled' and 'Active' or 'Standby'
    end

    return nil
end

local function get_defult_psu(name)
    local supply = cjson.json_object_new_object()
    supply.Manufacturer = cjson.null
    supply.PartNumber = cjson.null
    supply.Model = cjson.null
    supply.Mode = cjson.null
    supply.OutputVoltage = cjson.null
    supply.FirmwareVersion = cjson.null
    supply.InputWatts = cjson.null
    supply.SerialNumber = cjson.null
    supply.Name = name
    supply.LineInputVoltage = cjson.null
    supply.PowerSupplyChannel = cjson.null
    supply.PowerSupplyType = cjson.null
    supply.PowerCapacityWatts = cjson.null
    supply.Position = cjson.null
    return supply
end

local function get_defult_power_converter(name)
    local supply = cjson.json_object_new_object()
    supply.Manufacturer = cjson.null
    supply.Model = cjson.null
    supply.OutputVoltage = cjson.null
    supply.FirmwareVersion = cjson.null
    supply.SerialNumber = cjson.null
    supply.Name = name
    supply.LineInputVoltage = cjson.null
    return supply
end

local function get_component_info(input)
    local count = input[2]
    local slots = {}
    for k = 1, #input[3] do
        table.insert(slots, string.byte(input[3], k))
    end
 
    return count, slots
end
 
local function get_psu_cnt_and_slot(input)
    local psu_cnt = 0
    local psu_slots = {}
    local psu_board_slots = {}
 
    for i = 1, #input do
        for j = 1, #input[i] do
            if input[i][j][1] == 'Psu' then
                psu_cnt, psu_slots = get_component_info(input[i][j])
            elseif input[i][j][1] == 'PsuBoard' then
                _, psu_board_slots = get_component_info(input[i][j])
            end
        end
    end
 
    return psu_cnt, psu_slots, psu_board_slots
end
 
function m.get_supply_list(input)
    local supply_list = cjson.json_object_new_array()
    local supply_paths = mdb_service.get_sub_paths(bus, POWER_SUPPLY_PATH, INTERFACE_COUNT,
        {POWER_SUPPLY_INTERFACE}).SubPaths
    local psu_map = {}
    local psu_metrics_map = {}
    for _, supply_path in pairs(supply_paths) do
        local psu_item = mdb.get_object(bus, supply_path, POWER_SUPPLY_INTERFACE)
        local metrics_item = mdb.get_object(bus, supply_path, POWER_SUPPLY_METRICS_INTERFACE)
        psu_map[psu_item.DeviceLocator] = psu_item
        psu_metrics_map[psu_item.DeviceLocator] = metrics_item
    end

    local psu_cnt, psu_slots, psu_board_slots = get_psu_cnt_and_slot(input)

    for i = 1, psu_cnt do
        local powerName = string.format('PSU%d', i)
        if psu_map[powerName] and psu_metrics_map[powerName] then
            local supply = cjson.json_object_new_object()
            supply.State = get_state(psu_map[powerName])
            supply.Manufacturer = psu_map[powerName].Manufacturer
            supply.PartNumber = psu_map[powerName].PartNumber
            supply.Model = psu_map[powerName].Model
            supply.ManufactureDate = psu_map[powerName].ProductionDate
            supply.Mode = get_mode(bus, psu_map[powerName])
            supply.OutputVoltage = psu_metrics_map[powerName].OutputVoltage
            supply.FirmwareVersion = psu_map[powerName].FirmwareVersion
            supply.InputWatts = psu_metrics_map[powerName].InputPowerWatts
            supply.SerialNumber = psu_map[powerName].SerialNumber
            supply.Name = powerName
            supply.LineInputVoltage = psu_metrics_map[powerName].InputVoltage
            supply.PowerSupplyChannel = m_supply_channel[tostring(psu_map[powerName].PowerSupplyChannel)]
            supply.PowerSupplyType = m_supply_type[tostring(psu_map[powerName].PowerSupplyType)] or 'Unknown'
            supply.PowerCapacityWatts = psu_metrics_map[powerName].Rate
            supply.Position = psu_map[powerName].Position
            supply_list[#supply_list + 1] = supply
        elseif is_include(psu_slots, i) or is_include(psu_board_slots, i) then
            supply_list[#supply_list + 1] = get_defult_psu(powerName)
        end
    end

    return supply_list
end

function m.check_power_data_type(data_type)
    local power_types = {'System', 'Cpu', 'Fan', 'Memory'}
    if not data_type then
        error(base_messages.PropertyMissing("Domain"))
    end
    for _, value in pairs(power_types) do
        if data_type == value then
            return true
        end
    end
    error(base_messages.QueryParameterOutOfRange(data_type, 'Domain', 'System, Cpu, Fan, Memory'))
end

function m.merge_NPU_Metrics(NPUBoardsMetrics, NPUsMetrics)
    local merged_list = {}
    local NPUBoardsMetrics_table = cjson.json_object_to_table(NPUBoardsMetrics)
    local NPUsMetrics_table = cjson.json_object_to_table(NPUsMetrics)
    for i=1, #NPUBoardsMetrics_table do
        table.insert(merged_list, NPUBoardsMetrics_table[i])
    end
    for i=1, #NPUsMetrics_table do
        table.insert(merged_list, NPUsMetrics_table[i])
    end
    return merged_list
end

function m.select_SDI_Metrics(SDIsMetrics)
    local select_list = {}
    local SDIsMetrics_table = cjson.json_object_to_table(SDIsMetrics)
    for _, subList in pairs(SDIsMetrics_table) do
        -- 判断第一个元素是否包含子串"SDI"
        if string.find(subList.Name, "SDI") then
            -- 如果包含，则将除去第一个元素后的部分保存到结果表中
            subList.Name = nil
            table.insert(select_list, subList)
        end
    end
    return select_list
end

function m.get_power_converter_list(input)
    local supply_list = cjson.json_object_new_array()
    local psu_map = {}

    for _, supply_path in pairs(input[1]) do
        psu_map[supply_path.Name] = supply_path
    end

    local power_converter_slots = {}

    if not input[2] then
        return supply_list
    end

    for i = 1, #input[2] do  -- 由关键字Expand得到的数组
        for j = 1, #input[2][i] do  -- 资源树返回的值本身是一个二维数组
            if input[2][i][j][1] and input[2][i][j][1] == 'PowerConverter' then
                _, power_converter_slots = get_component_info(input[2][i][j])
            end
        end
    end

    -- 电源接口统一，Input输入值不能直接使用，手动构造json对象
    -- 类型为普通电源时不显示默认信息
    local power_converter_name
    table.sort(power_converter_slots)
    for index, slot in ipairs(power_converter_slots) do
        power_converter_name = string.format('PowerConverter%d', slot)
        if psu_map[power_converter_name] then
            local supply = cjson.json_object_new_object()
            supply.Manufacturer = psu_map[power_converter_name].Manufacturer
            supply.Model = psu_map[power_converter_name].Model
            supply.ManufactureDate = psu_map[power_converter_name].ProductionDate
            supply.OutputVoltage = psu_map[power_converter_name].OutputVoltage
            supply.FirmwareVersion = psu_map[power_converter_name].FirmwareVersion
            supply.InputWatts = psu_map[power_converter_name].InputWatts
            supply.SerialNumber = psu_map[power_converter_name].SerialNumber
            supply.Name = power_converter_name
            supply.LineInputVoltage = psu_map[power_converter_name].LineInputVoltage
            supply.PowerCapacityWatts = psu_map[power_converter_name].PowerCapacityWatts
            supply_list[#supply_list + 1] = supply
        elseif is_include(power_converter_slots, slot) then
            supply_list[#supply_list + 1] = get_defult_power_converter(power_converter_name)
        end
    end

    return supply_list
end

function m.check_energy_saving_scene_type(scene)
    local supported = {'HPC', 'LightWork', 'Default'}
    if not scene then
        error(base_messages.PropertyMissing('Type'))
    end
    for _, value in ipairs(supported) do
        if scene == value then
            return true
        end
    end
    error(base_messages.QueryParameterOutOfRange(scene, 'Type', table.concat(supported, ', ')))
end

function m.get_energy_saving_mode(input)
    local res = cjson.json_object_new_array()
    if not input then
        return res
    end
    for mode, value in pairs(input) do
        local temp_data = cjson.json_object_new_object()
        temp_data['Mode'] = mode
        temp_data['Value'] = value
        table.insert(res, temp_data)
    end
    return res
end

function m.set_power_limit_fail_action(fail_action)
    local MDB_SERVICE = 'bmc.kepler.power_strategy'
    local MDB_PATH = '/bmc/kepler/Systems/1/PowerStrategy'
    local MDB_INTERFACE = 'bmc.kepler.Systems.PowerStrategy'
    local err, _ = bus:pcall(MDB_SERVICE, MDB_PATH, MDB_INTERFACE, 'SetPowerLimitException', 'a{ss}s',
    context.get_context() or context.new(), fail_action)
    if err and err.name == "org.freedesktop.DBus.Error.UnknownMethod" then
        if fail_action == "LogEventOnly" then
            error(base_messages.PropertyValueNotInList("LogEventOnly", "PowerLimitException"))
        end
        return false
    end
    if err then
        error(err)
    end
    return true
end

function m.get_power_limit_fail_action()
    local MDB_SERVICE = 'bmc.kepler.power_strategy'
    local MDB_PATH = '/bmc/kepler/Systems/1/PowerStrategy'
    local MDB_INTERFACE = 'bmc.kepler.Systems.PowerStrategy'
    local err, fail_action = bus:pcall(MDB_SERVICE, MDB_PATH, MDB_INTERFACE, 'GetPowerLimitException', 'a{ss}',
    context.get_context() or context.new())
    if err and err.name == "org.freedesktop.DBus.Error.UnknownMethod" then
        return nil
    end
    if err then
        error(err)
    end
    return fail_action
end

return m
