-- 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 class = require 'mc.class'
local singleton = require 'mc.singleton'
local method_misc = require 'method_misc'
local common_def = require 'common_def'
local log = require 'mc.logging'
local error_engine = require 'error_engine'
local sml = require 'sml'
local skynet = require 'skynet'
local smart_def = common_def.PD_SMART_DEF

local controller_collection = require 'controller.controller_collection'
local drive_collection = require 'drive.drive_collection'

local rpc_service_drive = class()

local BOOTABLE <const> = 8 -- 8 pd_operation设置启动盘参数
local PD_OPERATION_CRYPTO_ERASE<const> = 6 -- 6 pd_operation设置加密擦除
local U16_MAX <const> = 65535 -- 65535 最大的U16
local U16_MIN <const> = 0 -- 0 最小的U16

local STOP_LOCATE <const> = 0 -- 0关灯
local LOCATE <const> = 1 -- 1 点灯

local START_PATROLSTATE <const> = 1 -- 1 开始巡检
local STOP_PATROLSTATE <const> = 0 --  0 结束巡检

local GLOBAL_HOTSPARE_TYPE <const> = 2 -- 2 pd_operation设置全局热备盘参数
local DEDICATED_HOTSPARE_TYPE <const> = 3 -- 3 pd_operation设置局部热备盘参数
local CANCEL_HOTSPARE_TYPE <const> = 4 -- 4 pd_operation取消热备盘参数
local AUTO_REPLACE_HOTSPARE_TYPE <const> = 10 -- 10 pd_operation自动替换热备盘参数
local PD_OPERATION_SET_STATE <const> = 5 -- 5 pd_operation设置固件状态参数
local PATROL_STATE <const> = 9 -- 9 pd_operation设置巡检状态参数

local SM_CODE_E<const> = error_engine.SM_CODE_E
local SML_ERR_CODE_E<const> = error_engine.SML_ERR_CODE_E

function rpc_service_drive:drive_operate(operate_name, drive_name, ctx, ...)
    local operations = {
        ["SetLocationIndicatorState"] = self.set_location_indicator_state,
        ["SetFirmwareStatus"] = self.set_firmware_status,
        ["SetHotspareType"] = self.set_hotspare_type,
        ["SetBootPriority"] = self.set_boot_priority,
        ["SetPatrolState"] = self.set_patrol_state,
        ["CrypoErase"] = self.crypto_erase,
        ["SetFaultIndicatorState"] = self.set_fault_indicator_state
    }
    local prop_name = string.gsub(operate_name, 'Set', '')
    prop_name = prop_name == 'LocationIndicatorState' and 'IndicatorLED' or prop_name
    log:notice('[Storage]Start %s of %s', operate_name, drive_name)

    local drive = drive_collection.get_instance():check_and_get_drive(drive_name)
    if not drive or (operate_name ~= 'SetLocationIndicatorState' and
        operate_name ~= 'SetFaultIndicatorState' and not drive.identify_pd) then
        -- 存在 drive 就可以设置定位灯和故障灯状态
        log:error("[Storage]get drive or identify_pd failed")
        error_engine.raise_drive_error(ctx, SM_CODE_E.SM_CODE_PD_NOT_IDENTIFIED, prop_name)
    end

    local ok, ret = pcall(operations[operate_name], drive, ctx, ...)

    if not ok and ret then
        if ret[1] ~= common_def.SML_ERR_REBOOT_REQUIRED then
            log:error('[Storage]Failed to %s, and return: %s', operate_name, ret[1])
            error_engine.raise_drive_error(ctx, ret[1], ret[2], ret[3])
        end
        log:notice('[Storage]Successfully %s of %s, please restart OS.', operate_name, drive_name)
    else
        log:notice('[Storage]Successfully %s of %s.', operate_name, drive_name)
    end

    if drive.RefControllerId ~= common_def.INVALID_U8 then
        -- 设置成功后刷新资源树上的信息
        ok, ret = pcall(sml.get_pd_info, {Priority = 'High'}, drive.RefControllerId, drive.device_id)
        if ok then
            drive.on_pd_update:emit(ret)
        end
    end
end

function rpc_service_drive.set_location_indicator_state(drive, ctx, location_indicator_state)
    if location_indicator_state ~= LOCATE and location_indicator_state ~= STOP_LOCATE then
        log:error("[Storage]location_indicator_state value invalid")
        error({common_def.SM_CODE_PARA_DATA_ILLEGAL, 'IndicatorLED', tostring(location_indicator_state)})
    end

    local ret = common_def.RET_ERR
    if drive.RefControllerId == common_def.INVALID_U8 then
        if drive:set_locate_led_by_smc(location_indicator_state) then
            ret = common_def.SM_CODE_SUCCESS
        end
    else
        local pd = drive.identify_pd
        ret = pd:pd_object_operation(location_indicator_state, "")
    end

    local ret_str = ret == 0 and 'successfully' or 'failed'
    if ctx and ctx.get_initiator then
        log:operation(ctx:get_initiator(), 'storage',
        '%s locating %s %s', location_indicator_state == 1 and 'Start' or 'Stop', drive.Name, ret_str)
    end
    if ret ~= common_def.SM_CODE_SUCCESS then
        error({ret, '%LocationIndicatorState', tostring(location_indicator_state)})
    end
end

function rpc_service_drive.set_fault_indicator_state(drive, ctx, fault_indicator_state)
    if fault_indicator_state ~= common_def.FAULT and fault_indicator_state ~= common_def.STOP_FAULT then
        log:error("[Storage]fault_indicator_state value invalid")
        error({common_def.SM_CODE_PARA_DATA_ILLEGAL, 'IndicatorLED', tostring(fault_indicator_state)})
    end

    local ret = common_def.RET_ERR
    if drive:set_fault_led_by_smc(fault_indicator_state) then
        ret = common_def.SM_CODE_SUCCESS
    end

    local ret_str = ret == 0 and 'successfully' or 'failed'
    if ctx and ctx.get_initiator then
        log:operation(ctx:get_initiator(), 'storage',
        'Turn %s fault led of %s %s', fault_indicator_state == 1 and 'on' or 'off', drive.Name, ret_str)
    end
    if ret ~= common_def.SM_CODE_SUCCESS then
        error({ret, '%FaultIndicatorState', tostring(fault_indicator_state)})
    end
end

function rpc_service_drive.set_firmware_status(drive, ctx, firmware_status)
    if firmware_status ~= common_def.FW_UG and firmware_status ~= common_def.FW_ONLINE and
        firmware_status ~= common_def.FW_OFFLINE and firmware_status ~= common_def.FW_SYSTEM then
        log:error("[Storage]firmware_status value invalid")
        error({common_def.SM_CODE_PARA_DATA_ILLEGAL, '%FirmwareStatus', tostring(firmware_status)})
    end

    if drive.FirmwareStatus == common_def.FW_FOREIGN then
        log:error("[Storage]FirmwareStatus cannot be set")
        error({common_def.SML_ERR_PD_STATE_UNSUPPORTED_TO_SET, '%FirmwareStatus', tostring(firmware_status)})
    end

    local ret =
        drive.identify_pd:pd_object_operation(PD_OPERATION_SET_STATE, string.pack('B', firmware_status) .. '\x01')
    local ret_str = ret == 0 and 'successfully' or 'failed'
    local set_state = common_def.PD_STATE_STR[firmware_status] or 'Unknown'
    if ctx and ctx.get_initiator then
        log:operation(ctx:get_initiator(), 'storage', 'Set physical drive %s state to %s %s',
            drive.Name, set_state, ret_str)
    end
    if ret ~= common_def.SM_CODE_SUCCESS then
        error({ret, '%FirmwareStatus', tostring(firmware_status)})
    end
end

local function set_drive_hotspare_asyn(ctx, type_func_table, hotspare_type, name)
    local ret = type_func_table[hotspare_type](0)
    local hot_spare_str = common_def.PD_HOT_SPARE_STR[hotspare_type]
    if ctx and ctx.get_initiator then
        if ret == common_def.SM_CODE_SUCCESS then
            log:operation(ctx:get_initiator(), 'storage', 'Set physical drive %s hot spare to %s finished',
                name, hot_spare_str)
        else
            log:operation(ctx:get_initiator(), 'storage', 'Failed to set physical drive %s hot spare to %s',
                name, hot_spare_str)
        end
    end
end

local function set_drive_hotspare(drive, hotspare_type, volume_id, ctx)
    local hot_spare_str = common_def.PD_HOT_SPARE_STR[hotspare_type]
    local is_vendor_pmc = method_misc:test_controller_vendor(drive.RefControllerTypeId, common_def.VENDER_PMC)
    local pmc_raid_offset = is_vendor_pmc and common_def.SML_ASYNC_OPERATION_OFFSET or 0

    local type_func_table = {
        [0] = function(offset)-- 0 取消热备盘
            return drive.identify_pd:pd_object_operation(CANCEL_HOTSPARE_TYPE + offset,
                string.pack('H', 0))
        end,
        [1] = function(offset)-- 1 全局热备盘
            return drive.identify_pd:pd_object_operation(GLOBAL_HOTSPARE_TYPE + offset,
                string.pack('H', 0))
        end,
        [2] = function(offset)-- 2 局部热备盘
            return drive.identify_pd:pd_object_operation(DEDICATED_HOTSPARE_TYPE + offset,
                string.pack('H', volume_id))
        end,
        [3] = function(offset)-- 3 自动替换热备盘
            return drive.identify_pd:pd_object_operation(AUTO_REPLACE_HOTSPARE_TYPE + offset,
                string.pack('H', volume_id))
        end
    }
    local ret = type_func_table[hotspare_type](pmc_raid_offset)
    if ret ~= common_def.SM_CODE_SUCCESS then
        if ctx and ctx.get_initiator then
            log:operation(ctx:get_initiator(), 'storage', 'Failed to set physical drive %s hot spare to %s',
                drive.Name, hot_spare_str)
        end
        return ret
    end

    if is_vendor_pmc then
        hot_spare_str = hot_spare_str .. ' start'
        skynet.fork_once(function ()
            set_drive_hotspare_asyn(ctx, type_func_table, hotspare_type, drive.Name)
        end)
    end
    if ctx and ctx.get_initiator then
        log:operation(ctx:get_initiator(), 'storage', 'Set physical drive %s hot spare to %s successfully',
            drive.Name, hot_spare_str)
    end
    return ret
end

function rpc_service_drive.set_hotspare_type(drive, ctx, hotspare_type, volume_id)
    local hot_spare_str = common_def.PD_HOT_SPARE_STR[hotspare_type] or 'Unknown'

    if type(volume_id) ~= 'number' or volume_id > U16_MAX or volume_id < U16_MIN then
        log:error("[Storage]volume_id type invalid")
        error({common_def.SM_CODE_PARA_DATA_ILLEGAL, '%VolumeId', tostring(volume_id)})
    end

    if hotspare_type ~= common_def.CANCEL_HOTSPARE and hotspare_type ~= common_def.GLOBAL_HOTSPARE and
        hotspare_type ~= common_def.DEDICATED_HOTSPARE and hotspare_type ~= common_def.AUTO_REPLACE_HOTSPARE then
        log:error("[Storage]hotspare_type value invalid")
        error({common_def.SM_CODE_PARA_DATA_ILLEGAL, '%HotspareType', tostring(hotspare_type)})
    end

    if hotspare_type == drive.HotspareType then
        log:error("[Storage]hotspare_type value is as same as before")
        error({common_def.SML_ERR_INVALID_PARAMETER, '%HotspareType', hot_spare_str})
    end

    if not drive.RefControllerTypeId or drive.RefControllerTypeId == common_def.INVALID_U8 then
        log:error("[Storage]get ref_controller_typeid failed")
        error({common_def.SM_CODE_PD_NOT_IDENTIFIED, '%HotspareType'})
    end

    local ret = set_drive_hotspare(drive, hotspare_type, volume_id, ctx)

    if ret ~= common_def.SM_CODE_SUCCESS then
        error({ret, '%HotspareType', hot_spare_str})
    end
end

function rpc_service_drive.set_patrol_state(drive, ctx, patrol_state)
    if patrol_state ~= START_PATROLSTATE and patrol_state ~= STOP_PATROLSTATE then
        log:error("[Storage]patrol_state value invalid")
        error({common_def.SM_CODE_PARA_DATA_ILLEGAL, '%PatrolState', tostring(patrol_state)})
    end

    local ret = drive.identify_pd:pd_object_operation(PATROL_STATE, string.pack('B', patrol_state))
    local set_state = ret == 0 and 'Succeeded in setting' or 'Failed to set'
    if ctx and ctx.get_initiator then
        log:operation(ctx:get_initiator(), 'storage', '%s the patrol read state %s of physical drive %s', set_state,
            patrol_state == 1 and 'Run' or 'Stop', drive.Name)
    end
    if ret ~= common_def.SM_CODE_SUCCESS then
        error({ret, '%PatrolState', tostring(patrol_state)})
    end
end

function rpc_service_drive.set_boot_priority(drive, ctx, boot_priority)
    local controller_obj = controller_collection.get_instance():get_by_controller_id(drive.RefControllerId)
    if not controller_obj then
        log:error("[Storage]get controller_obj failed")
        error({common_def.SM_CODE_PD_NOT_IDENTIFIED, '%BootPriority', tostring(drive.boot_priority)})
    end

    if method_misc:check_boot_priority_param(boot_priority, controller_obj.TypeId) ~= common_def.SM_CODE_SUCCESS then
        error({common_def.SM_CODE_PARA_DATA_ILLEGAL, '%BootPriority', tostring(boot_priority)})
    end

    if not drive or not drive.identify_pd then
        log:error("[Storage]get drive and identify_pd failed")
        error({common_def.SM_CODE_PD_NOT_IDENTIFIED, '%BootPriority', tostring(drive.boot_priority)})
    end

    local ret = drive.identify_pd:pd_object_operation(BOOTABLE, string.pack('B', boot_priority))
    local boot_str = ''
    local controller_type = drive.identify_pd.controller_type

    if method_misc:test_controller_vendor(controller_type, common_def.VENDER_LSI) then
        boot_str = 'as boot device'
    elseif method_misc:test_controller_vendor(controller_type, common_def.VENDER_PMC) then
        boot_str = common_def.PD_BOOT_PRIORITY[boot_priority] or 'Unknown'
    elseif method_misc:test_controller_vendor(controller_type, common_def.VENDER_HUAWEI) then
        boot_str = common_def.PD_BOOTABLE_DESC[boot_priority] or 'Unknown'
    end

    if ret ~= common_def.SM_CODE_SUCCESS then
        if ctx and ctx.get_initiator then
            log:operation(ctx:get_initiator(), 'storage', 'Failed to set physical drive %s %s', drive.Name, boot_str)
        end
        error({ret, '%BootPriority', tostring(boot_priority)})
    end
    if ctx and ctx.get_initiator then
        log:operation(ctx:get_initiator(), 'storage', 'Set physical drive %s %s successfully', drive.Name, boot_str)
    end
end

function rpc_service_drive.crypto_erase(drive, ctx)
    local controller_obj = controller_collection.get_instance():get_by_controller_id(drive.RefControllerId)
    if not controller_obj then
        log:error("[Storage]get controller_obj failed")
        error({common_def.SM_CODE_PD_NOT_IDENTIFIED, 'CrypoErase'})
    end

    if not controller_obj.CryptoEraseSupported then
        log:error("[Storage]SML_ERR_CTRL_OPERATION_NOT_SUPPORT")
        error({SML_ERR_CODE_E.SML_ERR_CTRL_OPERATION_NOT_SUPPORT, 'CrypoErase'})
    end

    local ret = drive.identify_pd:pd_object_operation(PD_OPERATION_CRYPTO_ERASE, '')
    local ret_str = ret == 0 and 'successfully' or 'failed'
    if ctx and ctx.get_initiator then
        log:operation(ctx:get_initiator(), 'storage', 'Cryptographically erase physical drive %s (SED) %s',
            drive.Name, ret_str)
    end
    if ret ~= common_def.SM_CODE_SUCCESS then
        error({ret, 'CrypoErase'})
    end
end

function rpc_service_drive:mock_record_spare_block(id, slc_spare_block, tlc_spare_block)
    local drive = drive_collection.get_instance():get_drive_by_drive_id(id)
    if not drive then
        log:error("invalid hdd id")
        return
    end

    if slc_spare_block > smart_def.ONE_HUNDRED_PERCENT and tlc_spare_block > smart_def.ONE_HUNDRED_PERCENT then
        log:error("invalid slc_spare_block and tlc_spare_block, should be less than 100")
        return
    end

    log:notice("input_slc:%s input_tlc:%s precent_slc:%s precent_tlc:%s", slc_spare_block, tlc_spare_block,
        drive.SLCSpareBlockPercentage, drive.TLCSpareBlockPercentage)
    local spareblock = {
        slc_value = slc_spare_block,
        tlc_value = tlc_spare_block
    }

    drive:record_maintaince_log_for_spare_black(spareblock, false)
end

function rpc_service_drive:get_drive_info_by_name(drive_name)
    local drive_infos = {}
    local drives = drive_collection.get_instance():get_presence_on_drives(drive_name ~= "*" and drive_name or nil)

    for _, drive in pairs(drives) do
        local drive_info = {}
        local drive_info_item = {}
        drive_info.Name = tostring(drive.Name)
        drive_info.ActivationLed = tostring(drive.ActivationLed)
        drive_info.HddBackplaneStartSlot = tostring(drive.HddBackplaneStartSlot)
        drive_info.RelativeSlot = tostring(drive.RelativeSlot)
        drive_info.LinkFault = tostring(drive.LinkFault)
        drive_info.CommandTimeoutTimes = tostring(drive.CommandTimeoutTimes)
        drive_info.UnexpectedSenseTimes = tostring(drive.UnexpectedSenseTimes)
        drive_info_item.DriveInfo = drive_info
        drive_infos[#drive_infos + 1] = drive_info_item
    end
    return drive_infos
end

return singleton(rpc_service_drive)