-- Copyright (c) 2025 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 singleton = require 'mc.singleton'
local class = require 'mc.class'
local bios_enum = require 'domain.bios_firmware.defs'
local bios_factory = require 'factory.bios_factory'
local package_builder = require 'domain.bios_firmware.package.package_builder'
local krun_flash = require 'domain.bios_firmware.package.krun_flash'
local upgrade_state_machine = require 'service.upgrade_state_machine'
local context = require 'mc.context'
local client = require 'bios.client'
local msg = require 'bios.ipmi.ipmi_message'
local ipmi = require 'ipmi'
local comp_code = ipmi.types.Cc

local REPLY_ERR<const> = -1
local REPLY_OK<const> = 0

local E_OK = 1
local E_FAILED = -1

local KrunService =  class()

function KrunService:ctor(bus, db, local_db)
    self.bus = bus
    self.db = db
    self.local_db = local_db
    self.state_machine = upgrade_state_machine.get_instance()
    self.krun_flash_collection = {}
    self.multihost = false
    self.upgrade_mode = bios_enum.UpgradeMode.Cold
    self.origin_mode = bios_enum.UpgradeMode.Cold
end

function KrunService:add_obj(obj)
    local id = obj.Id
    if self.krun_flash_collection[id] then
        return
    end
    self.krun_flash_collection[id] = krun_flash.new(self.bus, self.db, self.local_db, obj)
end

function KrunService:set_dft_mode()
    self.upgrade_mode = bios_enum.UpgradeMode.Force
    self.origin_mode = bios_enum.UpgradeMode.Force
end

function KrunService:get_obj(system_id)
    for _, krun_obj in pairs(self.krun_flash_collection) do
        if krun_obj:include_system(system_id) then
            return krun_obj
        end
    end
end

function KrunService:get_krun_obj_num()
    local num = 0
    for _, krun_obj in pairs(self.krun_flash_collection) do
        if krun_obj then
            num = num + 1
        end
    end
    return num
end

function KrunService:get_krun_prop(prop, system_id)
    local krun_obj
    if system_id == bios_enum.ALL_HOST_SYSTEM_ID then
        if self:get_krun_obj_num() == 1 then
            _, krun_obj = next(self.krun_flash_collection)
        else
            return
        end
    else
        krun_obj = self:get_obj(system_id)
    end
    if not krun_obj then
        return
    end
    return krun_obj:get_prop(prop)
end

-- 判断是否所有的krun版本都和升级包一致
function KrunService:check_all_host_same_version(version)
    for _, krun_obj in pairs(self.krun_flash_collection) do
        if not krun_obj:get_prop('Version') or krun_obj:get_prop('Version') ~= version then
            return false
        end
    end
    return true
end

function KrunService:_set_state_machine(event)
    self.state_machine:run(event, 'krun')
end

function KrunService:upgrade_mode_recover()
    -- 升级结束需要恢复为原升级模式
    self.upgrade_mode = self.origin_mode
    log:notice('[krun]mode recover to %s', bios_enum.UpgradeModeStr[self.upgrade_mode])
end

function KrunService:_recover()
    self:_set_state_machine(bios_enum.UpgradeEvent.Error)
    if self.package then
        self.package:dtor()
        self.package = nil
    end
    self:upgrade_mode_recover()
end

function KrunService:_prepare(cfg)
    log:notice('[krun]upgrade prepare: build package')
    self.package = package_builder.new():build(cfg.cfg_path)
    log:notice('[krun]upgrade prepare: reply messgae')
end

function KrunService:prepare(cfg)
    log:notice('[krun]upgrade prepare: start')
    self:_set_state_machine(bios_enum.UpgradeEvent.PrepareStart)
    local ok, err = pcall(function()
        self:_prepare(cfg)
        self:_set_state_machine(bios_enum.UpgradeEvent.PrepareFinish)
    end)
    if not ok then
        self:_recover()
        error(err)
    end
end

function KrunService:construct_upgrade_info(upgrade_path)
    local upgrade_info = {
        db = self.local_db,
        upgrade_path = upgrade_path,
        upgrade_mode = self.upgrade_mode,
        hpm_delete_flag = true
    }
    return upgrade_info
end

function KrunService:build_info(cfg, id)
    log:notice('[krun]upgrade process: upgrade mode(%s)', bios_enum.UpgradeModeStr[self.upgrade_mode])
    local upgrade_info = self:construct_upgrade_info(cfg.file_path)
    upgrade_info.krun_id = id
    return upgrade_info
end

-- 构造升级参数
function KrunService:_process(cfg)
    local can_upgrade = false
    for _, krun_obj in pairs(cfg.krun_objs) do
        if not krun_obj:check_uid_suitable(self.package.uid_list) then
            goto continue
        end
        can_upgrade = true
        log:notice('[krun] upgrade krun id %s process', krun_obj:get_krun_id())
        local upgrade_info = self:build_info(cfg, krun_obj:get_krun_id())
        upgrade_info.block_chip = krun_obj.block_chip
        upgrade_info.flash_chip = krun_obj.flash_chip
        self.package:process(upgrade_info, krun_obj)
        ::continue::
    end
    if not can_upgrade then
        error('upgrade package not suit all krun flasg, cant upgrade')
    end
end

function KrunService:process(cfg)
    log:notice('[krun]upgrade process: start')
    self:_set_state_machine(bios_enum.UpgradeEvent.ProcessStart)
    local ok, err = pcall(function()
        self:_process(cfg)
        self:_set_state_machine(bios_enum.UpgradeEvent.ProcessFinish)
    end)
    if not ok then
        self:_recover()
        error(err)
    end
end

function KrunService:finish(cfg)
    log:notice('[krun]upgrade finish: start')
    self:_set_state_machine(bios_enum.UpgradeEvent.FimwareStart)
    self:_set_state_machine(bios_enum.UpgradeEvent.FimwareFinish)
    self:upgrade_mode_recover()
    log:notice('[krun]hpm upgrade: krun package upgrade successfully')
end

function KrunService:get_obj_table(system_id)
    if not system_id then
        error('[krun]get krun obj table failed, system id is nil')
    end
    if system_id == bios_enum.ALL_HOST_SYSTEM_ID then
        return self.krun_flash_collection
    end
    return {[system_id] = self:get_obj(system_id)}
end


function KrunService:upgrade_hpm(phrase, cfg)
    local krun_objs = self:get_obj_table(cfg.system_id)
    if not krun_objs or not next(krun_objs) then
        error(string.format('[krun]upgrade hpm: get krun objs fail, system id is %s', cfg.system_id))
    end
    local fun_map = {
        prepare = KrunService.prepare,
        process = KrunService.process,
        finish = KrunService.finish
    }
    local fun = fun_map[phrase]
    if not fun then
        log:error('[krun]hpm upgrade: phrase(%s) invalid', phrase)
        return
    end
    cfg.krun_objs = krun_objs
    fun(self, cfg)
end

function KrunService:enable_multihost()
    self.multihost = true
end

function KrunService:_effective_prepare(krun_obj)
    log:notice('[krun]effective upgrade: start prepare upgrade bios package')
    self:_set_state_machine(bios_enum.UpgradeEvent.PrepareStart)
    local ok, err = pcall(function()
        self.package = package_builder.new():build_with_snapshot(krun_obj)
        self:_set_state_machine(bios_enum.UpgradeEvent.PrepareFinish)
    end)
    if not ok then
        self:_recover()
        error(err)
    end
end

function KrunService:_effective_process(krun_obj)
    log:notice('[krun]effective upgrade: start process upgrade bios package')
    self:_set_state_machine(bios_enum.UpgradeEvent.ProcessStart)
    local ok, err = pcall(function()
        -- 强制升级模式，并且定制为下次重启，需要将启动模式改为Cold
        log:notice('[krun]effective upgrade: start upgrade krun package, krun_id id: %s', krun_obj:get_krun_id())
        if self.upgrade_mode == bios_enum.UpgradeMode.Force then
            log:notice('[krun]effective process:change upgrade mode from force to cold')
            self.upgrade_mode = bios_enum.UpgradeMode.Cold
        end
        local upgrade_info = self:construct_upgrade_info(krun_obj:get_cache_path())
        upgrade_info.krun_id = krun_obj:get_krun_id()
        upgrade_info.block_chip = krun_obj.block_chip
        upgrade_info.flash_chip = krun_obj.flash_chip
        self.package:process(upgrade_info, krun_obj)
        self:_set_state_machine(bios_enum.UpgradeEvent.ProcessFinish)
    end)
    if not ok then
        self:_recover()
        error(err)
    end
end

function KrunService:_effective_finish()
    log:notice('[krun]effective upgrade: start finish upgrade krun package')
    self:_set_state_machine(bios_enum.UpgradeEvent.FimwareStart)
    self:_set_state_machine(bios_enum.UpgradeEvent.FimwareFinish)
    self:upgrade_mode_recover()
    log:notice('[krun]effective upgrade: krun package upgrade successfully')
end

function KrunService:_power_off_effective(krun_obj)
    if not krun_obj or not krun_obj:is_cache() then
        log:notice('[krun]effective upgrade no need activate krun, snapshot is %s', krun_obj)
        return REPLY_OK
    end

    local ok, err = pcall(function ()
        self:_effective_prepare(krun_obj)
        self:_effective_process(krun_obj)
        self:_effective_finish()
    end)
    if not ok then
        log:error('[krun]effective upgrade: upgrade krun package fail, %s', err)
        return REPLY_ERR
    end
    return REPLY_OK
end

function KrunService:power_off_effective(system_id)
    local result = REPLY_OK
    if system_id == bios_enum.ALL_HOST_SYSTEM_ID then
        for _, krun_obj in pairs(self.krun_flash_collection) do
            if self:_power_off_effective(krun_obj) ~= REPLY_OK then
                result = REPLY_ERR
            end
        end
    else
        return self:_power_off_effective(self:get_obj(system_id))
    end
    return result
end

function KrunService:register_multihost(param, pkg_snapshot)
    if pkg_snapshot and pkg_snapshot:is_cache() then
        param[3] = {Key = 'ActiveCondition', Value = 'ChassisPowerOff'}
        self:active_register(param)
        return
    end
end

function KrunService:judge_active_register(system_id)
    log:notice('[Krun]Start judge power status')
    local param = bios_enum.KrunActiveRegisterList
    if self.multihost then
        if system_id == bios_enum.ALL_HOST_SYSTEM_ID then  -- all host
            for _, krun_obj in pairs(self.krun_flash_collection) do
                self:register_multihost(param, krun_obj)
            end
        else
            local krun_obj = self:get_obj(system_id)
            self:register_multihost(param, krun_obj)
        end
    else
        local krun_obj = self:get_obj(bios_enum.SINGLE_HOST_SYSTEM_ID)
        if krun_obj and krun_obj:is_cache() then
            param[3] = {Key = 'ActiveCondition', Value = 'PowerOff'}
            self:active_register(param)
        end
    end
end

function KrunService:active_register(param)
    log:notice('[Krun]Start judge power status, not OFF , start regitser')
    client:FirmwareActiveFirmwareActiveRegisterActiveAction(context.new(), param)
end

function KrunService:update_krun_version(req, ctx)
    local bios_ser = bios_factory.get_service('bios_service')
    local resp = bios_ser:get_manu_id()
    local system_id = bios_ser:get_system_id(ctx)
    local obj = self:get_obj(system_id)
    if not obj then
        log:error('krun_service: update krun version fail, system id(%s) invalid.', system_id)
        return msg.SetBiosVersionRsp.new(comp_code.InvalidFieldRequest, resp)
    end

    local ret = bios_ser.judge_req(req)
    if ret == E_FAILED then
        return msg.SetBiosVersionRsp.new(comp_code.InvalidFieldRequest, resp)
    end
    local manu_id = bios_ser:get_manu_id()
    ret = bios_ser.judge_manu_id_valid(req, manu_id)
    if ret == E_FAILED then
        return msg.SetBiosVersionRsp.new(comp_code.InvalidCommand, resp)
    end

    if not req.Length or not req.Version or #req.Version ~= req.Length then
        return msg.SetBiosVersionRsp.new(comp_code.ReqDataLenInvalid, resp)
    end

    obj:set_version(req.Version)
    log:notice('set krun version to %s successfully, system is %s',
        req.Version, system_id)
    ipmi.ipmi_operation_log(ctx, 'BIOS', "Set Krun version to %s successfully", req.Version)
    return msg.SetBiosVersionRsp.new(comp_code.Success, resp)
end

return singleton(KrunService)