-- 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 msg = require 'general_hardware.ipmi.ipmi_message'
local log = require 'mc.logging'
local ipmi = require 'ipmi'
local json = require 'cjson'
local unit_manager = require 'unit_manager.unit_manager'
local cc = ipmi.types.Cc

local SYS_INFO_BLOCK_LEN <const> = 16
local MAX_SYS_INFO_BLOCK <const> = 16
local MANUFACTURE_ID<const> = 0x0007db
local NPU_REGION_TYPE<const> = 2
local RESET_INDEX<const> = 1
local RESET<const> = 0
local UNRESET<const> = 1
local LOCK_INDEX<const> = 2
local RESET_UNLOCK<const> = 0
local REGION_NAME_IDNEX<const> = 3
local GLOBAL_REGION<const> = 0xff
local COMPUTINGUNIT_REGION<const> = 0x01

local NPUBOARD_RESET_MAGIC<const> = {
    ENABLED = 0X5A,
    DISABLED = 0
}

local common_ipmi = {}
common_ipmi.__index = common_ipmi

function common_ipmi.new()
    return setmetatable({}, common_ipmi)
end

function common_ipmi:init(db)
    self.db = db
end

local function init_system_info_list()
    local init_arry = {}
    for _ = 1, SYS_INFO_BLOCK_LEN * MAX_SYS_INFO_BLOCK do
        table.insert(init_arry, "")
    end
    return init_arry
end

local function get_system_info_arry(system_info)
    if not system_info then
        return init_system_info_list()
    else
        return json.decode(system_info)
    end
end

function common_ipmi:set_system_info_parameters(req, ctx)
    local system_info_arry = {}
    local SystemInfoList = self.db.SystemInfoList({
        Id = 'system_info_list'
    })
    system_info_arry = get_system_info_arry(SystemInfoList.SystemInfo)
    local data = {}
    for i = 1, #req.Data do
        table.insert(data, string.byte(req.Data, i))
    end
    if not data[1] or data[1] < 0 or data[1] >=  MAX_SYS_INFO_BLOCK then
        log:error('[Common IPMI] request Data(%s) is invaild', req.SetSelector)
        ipmi.ipmi_operation_log(ctx, 'general_hardware', 'Set system information failed')
        return msg.SetSystemInfoParametersRsp.new(cc.InvalidFieldRequest)
    end
    local offset = SYS_INFO_BLOCK_LEN * (data[1])
    for i = 2, #data do
        system_info_arry[offset + i - 1] = data[i]
    end
    -- 清除超出长度的值
    local len = system_info_arry[1]
    if type(len) ~= 'number' then
        log:error('[Common IPMI]  SystemInfo len is invaild')
        ipmi.ipmi_operation_log(ctx, 'general_hardware', 'Set system information failed')
        return msg.SetSystemInfoParametersRsp.new(cc.DataNotAvailable)
    end
    for i = len + 2, #system_info_arry do
        system_info_arry[i] = ""
    end
    local system_info = json.encode(system_info_arry)
    SystemInfoList.SystemInfo = system_info
    SystemInfoList:save()
    ipmi.ipmi_operation_log(ctx, 'general_hardware', 'Set system information successfully')
    return msg.SetSystemInfoParametersRsp.new(cc.Success)
end

function common_ipmi:get_system_info_parameters(req, ctx)
    local SystemInfoList = self.db.SystemInfoList({
        Id = 'system_info_list'
    })
    if not SystemInfoList.SystemInfo then
        log:error('[Common IPMI] SystemInfo is nil')
        return msg.GetSystemInfoParametersRsp.new(cc.DataNotAvailable, 0, 0)
    end
    local system_info = SystemInfoList.SystemInfo
    local system_info_arry = json.decode(system_info)

    if req.SetSelector < 0 or req.SetSelector >= MAX_SYS_INFO_BLOCK then
        log:error('[Common IPMI] request SetSelector(%s) is invaild', req.SetSelector)
        return msg.GetSystemInfoParametersRsp.new(cc.InvalidFieldRequest, 0, 0)
    end
    local offset = req.SetSelector * SYS_INFO_BLOCK_LEN
    local len = system_info_arry[1]
    if type(len) ~= 'number' or offset > len then
        log:error('[Common IPMI] SystemInfo len(%s) is invaild, SetSelector: %s ', len, req.SetSelector)
        return msg.SetSystemInfoParametersRsp.new(cc.DataNotAvailable, 0, 0)
    end
    local resp_data = ''
    local resp_len = len - offset + 1 < SYS_INFO_BLOCK_LEN and len - offset + 1 or SYS_INFO_BLOCK_LEN
    for i = 1, resp_len do
        if system_info_arry[i + offset] == "" then
            break
        end
        resp_data = resp_data .. string.char(system_info_arry[i + offset])
    end
    return msg.GetSystemInfoParametersRsp.new(cc.Success, req.SetSelector, resp_data)
end

local REGION_PROP <const> = {
    [GLOBAL_REGION] = {
        'GlobalReset', 'GlobalResetLocked', 'Global'
    },
    [COMPUTINGUNIT_REGION] = {
        'ComputingUnitReset', 'ComputingUnitResetLocked', 'ComputingUnit'
    }
}

local function post_reset(npu_board, prop)
    local ok, res = pcall(function()
        npu_board:set_prop(prop[RESET_INDEX], UNRESET)
        npu_board:set_prop('ResetEnabled', NPUBOARD_RESET_MAGIC.DISABLED)
    end)
    if not ok then
        log:error('[Common IPMI] unreset npuboard fail, err is %s', res)
    end
    return ok
end

local function reset_npuboard_by_region(npu_board, ctx, region_id, slot)
    local prop = REGION_PROP[region_id]
    if not prop then
        log:error('[Common IPMI] Reset NPU Board fail, Domain id (%s) invalid', region_id)
        return msg.ResetNpuBoardRsp.new(cc.InvalidFieldRequest, MANUFACTURE_ID)
    end

    local ok, res = pcall(function()
        npu_board:set_prop('ResetEnabled', NPUBOARD_RESET_MAGIC.ENABLED)
        npu_board:set_prop(prop[LOCK_INDEX], RESET_UNLOCK)
        npu_board:set_prop(prop[RESET_INDEX], RESET)
    end)
    if not ok then
        log:error('[Common IPMI] Reset NPU Board (Domain id %s) fail, err is %s', region_id, res)
        ipmi.ipmi_operation_log(ctx, 'general_hardware',
            'Reset NPU Board %s (Domain id %s) fail', slot, prop[REGION_NAME_IDNEX])
        skynet.sleep(10)
        post_reset(npu_board, prop)
        return msg.ResetNpuBoardRsp.new(cc.DataNotAvailable, MANUFACTURE_ID)
    end
    -- 解复位之前等待CPLD写入结束
    skynet.sleep(10)
    ok = post_reset(npu_board, prop)
    if not ok then
        ipmi.ipmi_operation_log(ctx, 'general_hardware',
            'Reset NPU Board %s (Domain id %s) fail', slot, prop[REGION_NAME_IDNEX])
        return msg.ResetNpuBoardRsp.new(cc.DataNotAvailable, MANUFACTURE_ID)
    end
    ipmi.ipmi_operation_log(ctx, 'general_hardware',
        'Reset NPU Board %s (Domain id %s) successfully', slot, prop[REGION_NAME_IDNEX])
    return msg.ResetNpuBoardRsp.new(cc.Success, MANUFACTURE_ID)
end

function common_ipmi:set_device_action(req, ctx)
    local slot = req.Slot
    local npu_board = unit_manager.get_instance():get_npu_by_slot(slot)
    if not npu_board then
        log:error('[Common IPMI] set device action fail, slot(%s) invalid', slot)
        return msg.SetDeviceActionRsp.new(cc.InvalidFieldRequest, MANUFACTURE_ID)
    end

    -- 兼容新机型Npu模组全域复位
    if npu_board:get_prop('BusType') == NPU_REGION_TYPE then
        return reset_npuboard_by_region(npu_board, ctx, GLOBAL_REGION, slot)
    end

    local ok, res = pcall(function()
        npu_board:set_prop('Reset', 0x0)
    end)
    if not ok then
        log:error('[Common IPMI] set device action fail, reset fail, err %s', res )
        ipmi.ipmi_operation_log(ctx, 'general_hardware', 'Reset all chips of NPU board %s failed', slot)
        return msg.SetDeviceActionRsp.new(cc.DataNotAvailable, MANUFACTURE_ID)
    end
    ipmi.ipmi_operation_log(ctx, 'general_hardware', 'Reset all chips of NPU board %s successfully', slot)
    return msg.SetDeviceActionRsp.new(cc.Success, MANUFACTURE_ID)
end

local expected_req <const> = { GroupId = 0xff, Lun = 0x00, Offset = 0x0000, Length = 0x01 }
-- 支持按区域复位Npu模组
function common_ipmi:reset_npuboard(req, ctx)
    for k, expected_value in pairs(expected_req) do
        if req[k] ~= expected_value then
            log:error('[Common IPMI] Reset NPU Board fail, %s (%s) invalid', k, req[k])
            return msg.ResetNpuBoardRsp.new(cc.InvalidFieldRequest, MANUFACTURE_ID)
        end
    end

    if not req.Data or #req.Data ~= expected_req.Length then 
        log:error('[Common IPMI] Reset NPU Board fail, request data length invalid')
        return msg.ResetNpuBoardRsp.new(cc.InvalidFieldRequest, MANUFACTURE_ID)
    end

    local slot = req.ComponentId
    local npu_board = unit_manager.get_instance():get_npu_by_slot(slot)
    if not npu_board then
        log:error('[Common IPMI] Reset NPU Board fail, slot(%s) invalid', slot)
        return msg.ResetNpuBoardRsp.new(cc.InvalidFieldRequest, MANUFACTURE_ID)
    end

    if npu_board:get_prop('BusType') ~= NPU_REGION_TYPE then
        log:error('[Common IPMI] Reset NPU Board fail, NPU Board (%s) not support', slot)
        return msg.ResetNpuBoardRsp.new(cc.InvalidFieldRequest, MANUFACTURE_ID)
    end

    return reset_npuboard_by_region(npu_board, ctx, string.byte(req.Data), slot)
end
return common_ipmi