-- 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 cjson = require 'cjson'
local vos = require 'utils.vos'
local mdb_service = require 'mc.mdb.mdb_service'
local context = require 'mc.context'
local custom_messages = require 'messages.custom'
-- Desription: 升级管理
local m = {}

function m.check_system_id_valid(system_id)
    if system_id == nil then
        -- 用户不传SystemId时，入参为nil
        return true
    end
    local ok, ret = pcall(mdb_service.is_valid_path, bus, '/bmc/kepler/Systems/' .. system_id)
    if not ok or not ret.Result then
        log:error('Invalid system id (%s)', system_id)
        error(custom_messages.ResourceNotFound('SystemId', system_id))
    end
    return true
end

function m.get_expand_parameters(is_clear_conf, active_mode, force_update, stage, targets)
    local ret_paras = {}

    if active_mode == 'ResetBMC' and is_clear_conf then
        -- BMC生效分离时，不会立即复位，不能做恢复出厂设置，与V2策略一致
        error(custom_messages.UpgradeParameterConflict('ActiveMode', active_mode, 'RestoreFactorySettings'))
    elseif is_clear_conf then
        ret_paras.RestoreFactorySettings = 'true'
    elseif active_mode then
        ret_paras.ActiveMode = active_mode
    elseif not active_mode then
        ret_paras.ActiveMode = "Immediately"
    end

    if force_update == nil or force_update == true then
        ret_paras.ForceUpdate = 'true'
    else
        ret_paras.ForceUpdate = 'false'
    end
    ret_paras.Stage = stage and 'true' or 'false'
    local temp_targets
    if targets then
        temp_targets = table.concat(targets, ',')
    end
    if temp_targets and #temp_targets > 0 then
        ret_paras.Targets = temp_targets
    end
    return ret_paras
end

function m.protocal_check(uri, protocol, is_permitted, raise_error)
    if string.sub(uri, #uri - 3) ~= '.hpm' then
        return raise_error and
            error(base_messages.ActionParameterValueFormatError('******', 'ImageURI',
                'UpdateService.SimpleUpdate'))
    end

    -- 本地升级不需要传输协议字段,截取前4字节判断是否本地升级cd
    if string.sub(uri, 1, 4) == '/tmp' then
        local ok, ret = pcall(vos.get_file_accessible, uri)
        if not (ok and ret) then
            return raise_error and error(custom_messages.FileNotExist())
        end
        if not is_permitted then
            -- 近端文件权限校验不通过
            return raise_error and error(custom_messages.NoPrivilegeToOperateSpecifiedFile())
        end
        return true
    end

    if not protocol then
        return raise_error and
            error(
                base_messages.ActionParameterMissing('UpdateService.SimpleUpdate',
                    'TransferProtocol'))
    end

    if #uri <= #protocol then
        return false
    end
    local protocol_head = string.sub(uri, 1, #protocol)

    if string.upper(protocol_head) ~= protocol then
        return raise_error and error(custom_messages.FileTransferProtocolMismatch())
    end

    return true
end

function m.full_image_protocal_check(image_uri, protocol, is_permitted)
    if string.sub(image_uri, #image_uri - 3) ~= '.zip' then
        error(base_messages.ActionParameterValueFormatError('******', 'Imageimage_uri',
            'UpdateService.FullImageUpdate'))
    end

    -- 本地升级不需要传输协议字段,截取前4字节判断是否本地升级cd
    if string.sub(image_uri, 1, 4) == '/tmp' then
        local ok, ret = pcall(vos.get_file_accessible, image_uri)
        if not (ok and ret) then
            error(custom_messages.FileNotExist())
        end
        if not is_permitted then
            -- 近端文件权限校验不通过
            error(custom_messages.NoPrivilegeToOperateSpecifiedFile())
        end
        return true
    end

    if not protocol then
        error(
            base_messages.ActionParameterMissing('UpdateService.FullImageUpdate', 'TransferProtocol'))
    end

    if #image_uri <= #protocol then
        return false
    end
    local protocol_head = string.sub(image_uri, 1, #protocol)

    if string.upper(protocol_head) ~= protocol then
        error(custom_messages.FileTransferProtocolMismatch())
    end

    return true
end

function m.parallel_update_protocol_check(packages, results)
    for index, package in ipairs(packages) do
        if not m.protocal_check(package.ImageURI, package.TransferProtocol, results[index], true) then
            return false
        end
    end
    return true
end

function m.get_boards(paths)
    local boards = {}
    for _, v in ipairs(paths) do
        local item = {}
        local obj = mdb.get_object(bus, v, 'bmc.kepler.Object.Properties')
        item['Id'] = obj.ObjectIdentifier[4]
        obj = mdb.get_object(bus, v, 'bmc.kepler.Systems.Board')
        item['NodeId'] = obj.NodeId
        obj = mdb.get_object(bus, v, 'bmc.kepler.Systems.Board.Unit')
        item['Type'] = obj.Type == 'NICCard' and 'NIC' or obj.Type
        boards[#boards + 1] = item -- 1表示将元素添加到表的最后
    end
    return boards
end

function m.get_boards_and_pcie_cards(board_paths, pcie_card_paths)
    local boards_and_pcie_cards = {}
    for _, v in ipairs(board_paths) do
        local item = {}
        local obj = mdb.get_object(bus, v, 'bmc.kepler.Object.Properties')
        item['Id'] = obj.ObjectIdentifier[4]
        obj = mdb.get_object(bus, v, 'bmc.kepler.Systems.Board')
        item['NodeId'] = obj.NodeId
        obj = mdb.get_object(bus, v, 'bmc.kepler.Systems.Board.Unit')
        item['Type'] = obj.Type == 'NICCard' and 'NIC' or obj.Type
        boards_and_pcie_cards[#boards_and_pcie_cards + 1] = item -- 1表示将元素添加到表的最后
    end
    for _, v in ipairs(pcie_card_paths) do
        local item = {}
        if not string.match(v, '(.*)NPUCard') then
            local obj = mdb.get_object(bus, v, 'bmc.kepler.Object.Properties')
            item['Id'] = obj.ObjectIdentifier[4]
            obj = mdb.get_object(bus, v, 'bmc.kepler.Systems.PCIeDevices.PCIeCard')
            item['NodeId'] = obj.NodeID
            item['Type'] = "PCIeCard"
            boards_and_pcie_cards[#boards_and_pcie_cards + 1] = item -- 1表示将元素添加到表的最后
        end
    end
    return boards_and_pcie_cards
end

local function get_component_info(systemId)
    local component_info = cjson.json_object_new_array()
    local bios_obj = mdb.get_object(bus, '/bmc/kepler/Systems/' .. systemId .. '/Bios', 'bmc.kepler.Systems.Bios')
    local res = bios_obj:ExportBiosSetup(require 'mc.context'.new('Redfish', 'NA', 'NA'),
        'ComponentVersion')
    local obj = cjson.json_object_ordered_decode(res.Result)
    local order_keys = cjson.json_object_get_keys(obj.ComponentInfo)
    for _, index in pairs(order_keys) do
        component_info[index] = obj.ComponentInfo[index]
    end
    return component_info
end

local function get_bios_version(systemId)
    local version = {}
    local bios_obj = mdb.get_object(bus, '/bmc/kepler/Systems/' .. systemId .. '/Bios', 'bmc.kepler.Systems.Bios')
    local bios_version = bios_obj.Version
    local basic_version, patch_version = string.match(bios_version, '(.+).(HP.+)')
    if not basic_version or not patch_version then
        basic_version = bios_version
        patch_version = ''
    end
    version.BaseVersion = basic_version
    version.PatchVersion = patch_version
    return version
end

local function device_locator_change_to_system_id(name)
    local lower_name = string.lower(name)
    if lower_name == "bios" then
        return 1
    else
        return tonumber(string.match(lower_name, "bios(%d+)"))
    end
end

function m.get_oem_firmware_inventory_info(fw_info)
    local id, active_mode = fw_info.Id, fw_info.ActiveMode
    local data = cjson.json_object_new_object()
    data.PositionId = fw_info.Location
    if not fw_info.Location or fw_info.Location == '' then
        data.PositionId = cjson.null
    end
    data.Manufacturer = fw_info.Manufacturer
    if string.match(id, '^Bios') then
        local systemId = device_locator_change_to_system_id(id)
        local ok, component_info = pcall(function ()
            return get_component_info(systemId)
        end)
        if not ok then
            component_info = cjson.json_object_new_array()
        end
        data.ComponentInfo = component_info
        local res, version = pcall(function ()
            return get_bios_version(systemId)
        end)
        if not res then
            data.BaseVersion = ''
            data.PatchVersion = ''
        else
            data.BaseVersion = version.BaseVersion
            data.PatchVersion = version.PatchVersion
        end
    end
    local id_table = {}
    for w in string.gmatch(id, '[^_]+') do
        table.insert(id_table, w)
    end
    if id_table[1] == 'BCU' and id_table[2] == 'CPLD1' then
        data.SupportHotUpgrade = false
    elseif id_table[1] == 'BCU' and id_table[2] == 'CPLD2' then
        data.SupportHotUpgrade = true
    end
    if id_table[1] == 'EXU' and id_table[2] == 'CPLD1' then
        data.SupportHotUpgrade = false
    elseif id_table[1] == 'EXU' and id_table[2] == 'CPLD2' then
        data.SupportHotUpgrade = true
    end
    if id == "ActiveBMC" or string.find(id, "SR_") then
        data.ActiveMode = active_mode
        -- ActiveModeSupported在V2表示此机型是否支持升级生效分离
        -- 在V3上默认所有机型支持，此值恒为true
        data.ActiveModeSupported = true
    else
        data.ActiveMode = cjson.null
        data.ActiveModeSupported = cjson.null
    end
    return data
end

function m.get_packages(params)
    local res, package, options = {}, {}, nil
    for _, param in pairs(params) do
        options = {}
        options['TransferProtocol'] = param.TransferProtocol
        options['SystemId'] = param.SystemId
        if param.ForceUpdate == nil or param.ForceUpdate == true then
            options['ForceUpdate'] = 'true'
        else
            options['ForceUpdate'] = 'false'
        end
        if param.Stage == nil or param.Stage == false then
            options['Stage'] = 'false'
        else
            options['Stage'] = 'true'
        end
        package = {param.ImageURI, options}
        res[#res + 1] = package
    end
    return res
end

function m.get_full_image_options(uri, force_update)
    local res = {}
    res.UpdateMode = 'FullImage'
    res.FullImagePath = uri
    if force_update == nil or force_update == true then
        res.ForceUpdate = 'true'
    else
        res.ForceUpdate = 'false'
    end
    return res
end

function m.get_active_policy(policy)
    if not policy or #policy == 0 then
        return {}
    end
    local policy_set = {}
    local unique_values = {}
    for _, value in ipairs(policy) do
        if not policy_set[value] then
            policy_set[value] = true
            table.insert(unique_values, value)
        end
    end
    if policy_set.PowerOn and policy_set.PowerOff then
        error(custom_messages.OperationFailed())
    end
    if policy_set.PowerOff and policy_set.ResetBMC then
        return {ActivationControl = 'ResetAC'}
    end
    table.sort(unique_values)

    return {
        ActivationControl = table.concat(unique_values, ",")
    }
end

function m.modify_default_version(id, version, releasedate)
    local res = version and version .. releasedate or ''
    if id == 'BackupBMCSDK' or id == 'AvailableBMCSDK' or id == 'ActiveBMCSDK' then
        if version == 'N/A' or version == '' then
            res = cjson.null
        end
    end
    return res
end

function m.get_firmware_integrity_certificate(contents)
    local res = cjson.json_object_new_array()
    local object

    for _, content in pairs(contents) do
        object = cjson.json_object_new_object()
        object["CertificateString"] = content
        res[#res + 1] = object
    end

    return res
end

function m.get_firmware_integrity_crl(contents)
    local res = cjson.json_object_new_array()
    local object
    for _, content_tb in pairs(contents) do
        for _, content in pairs(content_tb) do
            object = cjson.json_object_new_object()
            object["CRLString"] = content
            res[#res + 1] = object
        end
    end

    return res
end

function m.get_allow_staging(members)
    local list = {}
    local temp = {}
    local obj
    for _, path in pairs(members) do
        obj = mdb.get_object(bus, path, 'bmc.kepler.UpdateService.FirmwareInfo')
        if not obj.StageSupported then
            goto continue
        end
        for _, v in pairs(obj.RelatedItem) do
            if not temp[v] then
                temp[v] = true
                table.insert(list, v)
            end
        end
        ::continue::
    end
    return list
end

local function search_id(id_table, firmware_type, id, board)
    -- 部分机型无挂耳,chassisBMC对象配置在扩展板上,需要单独判断
    if board.NodeId == 'chassisBMC' then
        -- PSR仅有SR需要升级,无其它固件
        if string.match(id, 'PSR') ~= nil then
            return board.NodeId .. 'PSR'
        end
        return
    end
    if board.Id == id_table[#id_table] and type(board.NodeId) == 'string' then
        if firmware_type == 'SR' then
            return board.NodeId .. 'HWSR'
        end
        if firmware_type == 'VRD' and board.Type == 'BCU' then
            --BCU的VRD不需要类型
            return string.match(board.NodeId, '(CpuBoard%d+)') .. 'VRD'
        end
        if firmware_type == 'CPLD' and id_table[2] == 'DPU' then
            return board.NodeId .. id_table[1]
        end
        if firmware_type == 'CPLD' and string.match(id_table[2], 'CPLD')  then
            return board.NodeId .. id_table[2]
        end
        if firmware_type == 'Retimer' and string.match(id_table[2], 'Retimer')  then
            return board.NodeId .. id_table[2]
        end
        return board.NodeId .. firmware_type
    end
end

local function get_firmwareid_via_uriid(input, id)
    if string.match(id, '_PSU%d+') then
        return 'chassis' .. string.match(id, 'PSU%d+')
    end

    if string.match(id, 'LinkBoard') then
        if string.match(id, 'Patch') then
            return 'LCN' .. string.match(id, 'LinkBoard%d+') .. string.match(id, 'Patch')
        end
        if string.match(id, 'Software') then
            return 'LCN' .. string.match(id, 'LinkBoard%d+') .. string.match(id, 'Software')
        end
    end

    local id_table = {} -- 资源树id格式末尾为_position
    for w in string.gmatch(id, '[^_]+') do
        table.insert(id_table, w)
    end

    local firmware_types = {
        'FPGA',
        'CPLD',
        'MCU',
        'SR',
        'VRD',
        'Retimer'
    }

    local firmware_type = ''
    for _, v in ipairs(firmware_types) do
        if string.match(id, v) ~= nil then
            firmware_type = v
            break
        end
    end

    if firmware_type ~= '' then
        for _, board in ipairs(input) do
            local firmware_id = search_id(id_table, firmware_type, id, board)
            if firmware_id then
                return firmware_id
            end
        end
    end

    return id
end

local function get_active_object_details(active_obj, firmwareid, input)
    local fwinfo_path = '/bmc/kepler/UpdateService/FirmwareInventory/' .. firmwareid
    local fwinfo_obj = mdb.get_object(bus, fwinfo_path, 'bmc.kepler.UpdateService.FirmwareInfo.ToBeActivated')
    if not active_obj then
        return nil
    end
    local object = cjson.json_object_new_object()
    local firmware_id = get_firmwareid_via_uriid(input, firmwareid)
    object['FirmwareType'] = active_obj.FirmwareType .. '(' .. firmware_id .. ')'
    local related_firmware_item = cjson.json_object_new_object()
    if fwinfo_obj then
        related_firmware_item["@odata.id"] = '/redfish/v1/UpdateService/FirmwareInventory/' .. firmware_id
    end
    object['RelatedFirmwareItems'] = cjson.json_object_new_array()
    object['RelatedFirmwareItems'][#object['RelatedFirmwareItems'] + 1] = related_firmware_item
    object['StagedVersion'] = fwinfo_obj and fwinfo_obj.StagedVersion or 'NA'
    object['StagedTime'] = fwinfo_obj and fwinfo_obj.StagedTime or 'NA'
    return object
end

function m.get_take_effect(effects, active_intfs, input)
    local res = cjson.json_object_new_array()
    local object
    local active_obj
    for _, path in pairs(active_intfs) do
        active_obj = mdb.get_object(bus, path, 'bmc.kepler.UpdateService.FirmwareActiveInfo')
        if active_obj and active_obj.ToBeActivatedFirmwareIds and #active_obj.ToBeActivatedFirmwareIds > 0 then
            for _, firmwareid in ipairs(active_obj.ToBeActivatedFirmwareIds) do
                res[#res + 1] = get_active_object_details(active_obj, firmwareid, input)
            end
        elseif active_obj then
            object = cjson.json_object_new_object()
            object['FirmwareType'] = active_obj.FirmwareType
            object['RelatedFirmwareItems'] = cjson.json_object_new_array()
            object['StagedVersion'] = 'NA'
            object['StagedTime'] = 'NA'
            res[#res + 1] = object
        end
    end
    return res
end

function m.get_firmware_id_by_odata(targets, input, members)
    local list = {}
    local temp = {}
    local obj, obj_id, uriid, odata_id
    for _, target in pairs(targets) do
        odata_id = (target["@odata.id"]:match('^/redfish/v1/UpdateService/FirmwareInventory/([^/]+)$') or ''):lower():gsub('_staged$', '')
        if odata_id == '' then
            goto continue
        end
        for _, path in pairs(members) do
            obj = mdb.get_object(bus, path, 'bmc.kepler.UpdateService.FirmwareInfo')
            obj_id = obj.Id
            uriid = get_firmwareid_via_uriid(input, obj.Id)
            if odata_id == uriid:lower() and not temp[obj_id] then
                table.insert(list, obj_id)
                temp[obj_id] = true
            end
        end
        ::continue::
    end
    return list
end

local function is_staged(firmware_id)
    local path = bus:call('bmc.kepler.maca', '/bmc/kepler/MdbService', 'bmc.kepler.Mdb', 'GetPath',
        'a{ss}ssb', context.new(), "bmc.kepler.UpdateService.FirmwareInfo.Stage",
        cjson.encode({FirmwareId = firmware_id}), false)
    return path ~= ''
end

function m.get_firmware_id(input, firmware_list)
    local member = {}
    local stage_list = {}
    for _, path in ipairs(firmware_list) do
        local uriid = string.match(path, '/bmc/kepler/UpdateService/FirmwareInventory/(.*)')
        if uriid and uriid ~= '' then
            local firmware_id = get_firmwareid_via_uriid(input, uriid)
            member[#member + 1] = '/redfish/v1/UpdateService/FirmwareInventory/' .. firmware_id
            if is_staged(uriid) then
                stage_list[#stage_list + 1] = member[#member] .. '_staged'
            end
        end
    end
    for _, value in pairs(stage_list) do
        table.insert(member, value)
    end
    return member
end

return m
