-- 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 ipmi = require 'ipmi'
local bs = require 'mc.bitstring'
local log = require 'mc.logging'
local enums = require 'ipmi.enums'
local dirty_test_def = require 'dirty_test.dirty_test_def'
local class = require 'mc.class'
local comp_code = ipmi.types.Cc
local MAX_FFT_LENGTH<const> = 190 -- 最大220，考虑已有固定请求体已占21字节，fft传输部分先暂定最大190字节

local test_object = class()
function test_object:ctor(object_id, presence_state, npu_id, bus, logical_id, op_type)
    self.stage = dirty_test_def.stage[dirty_test_def.state_machine.Pending]
    self.stage_status = dirty_test_def.stage_status[dirty_test_def.state_machine.Pending]
    self.state_machine = dirty_test_def.state_machine.Pending
    self.test_object_id = object_id
    self.duration_seconds = 0
    self.npu_id = npu_id
    self.logical_id = logical_id
    self.presence = presence_state
    self.dirty_result = {
        contamination_detected = 'Unknown',
        local_contamination_detected = 'Unknown',
        remote_contamination_detected = 'Unknown',
        first_contamination_distance = {-1, -1, -1, -1, -1, -1, -1, -1},
        second_contamination_distance = {-1, -1, -1, -1, -1, -1, -1, -1}
    }
    self.bus = bus
    self.type = op_type
    self.fft_message = ''       -- fft信息
    self.get_fft_flag = false   -- fft信息查询完成标志
    self.get_fft_offset = 1     -- fft信息查询偏移
    self.set_fft_flag = false   -- fft信息设置完成标志
    self.set_fft_offset = 1     -- fft信息设置偏移
end

local channel_type = enums.ChannelType
local GET_ARM_NPU_CMD<const> = 0x17
local DMP_LAST_REQ<const> = 0x80

local dirty_test_cmd_request = {
    ['common'] = {ipmi_request_info = '<<0xDB0700:3/unit:8, cmd:1/unit:8, lun:1/unit:8, ' ..
        'request_parameter:1/unit:8, op_cmd:1/unit:8, op_fun:1/unit:8, offset:4/unit:8, ' ..
        'data_length:4/unit:8, operation_type:1/unit:8, optical_module_num:1/unit:8, optical_module_id:1/unit:8, reserve:1/unit:8>>'},
    ['init'] = {ipmi_request_info = '<<0xDB0700:3/unit:8, cmd:1/unit:8, lun:1/unit:8,' ..
        'request_parameter:1/unit:8, op_cmd:1/unit:8, op_fun:1/unit:8, offset:4/unit:8,' ..
        'data_length:4/unit:8, operation_type:1/unit:8, optical_module_num:1/unit:8, optical_module_id:1/unit:8, reserve1:1/unit:8, ' .. 
        'duration_seconds:2/unit:8, reserve2:4/unit:8>>'},
    ['start'] = {ipmi_request_info = '<<0xDB0700:3/unit:8, cmd:1/unit:8, lun:1/unit:8, ' ..
        'request_parameter:1/unit:8, op_cmd:1/unit:8, op_fun:1/unit:8, offset:4/unit:8, ' ..
        'data_length:4/unit:8, operation_type:1/unit:8, start_item:1/unit:8, optical_module_num:1/unit:8, optical_module_id:1/unit:8, reserve:1/unit:8>>'},
    ['get_fft'] = {ipmi_request_info = '<<0xDB0700:3/unit:8, cmd:1/unit:8, lun:1/unit:8,' ..
        'request_parameter:1/unit:8, op_cmd:1/unit:8, op_fun:1/unit:8, offset:4/unit:8,' ..
        'data_length:4/unit:8, operation_type:1/unit:8, optical_module_num:1/unit:8, optical_module_id:1/unit:8, reserve1:1/unit:8, ' .. 
        'fft_offset:1/unit:8, reserve2:1/unit:8>>'},
    ['set_fft'] = {ipmi_request_info = '<<0xDB0700:3/unit:8, cmd:1/unit:8, lun:1/unit:8,' ..
        'request_parameter:1/unit:8, op_cmd:1/unit:8, op_fun:1/unit:8, offset:4/unit:8,' ..
        'data_length:4/unit:8, operation_type:1/unit:8, optical_module_num:1/unit:8, optical_module_id:1/unit:8, reserve1:1/unit:8, ' .. 
        'fft_offset:1/unit:8, end_flag:1/unit:8, fft_length:1/unit:8, reserve2:2/unit:8, tail/string>>'}
}

function test_object:assemble_ipmi_request(operation_pram)
    local valid_op_id = 0
    if self.logical_id and self.logical_id ~= 255 then
        valid_op_id = self.logical_id
    end
    local data = nil
    if operation_pram.dirty_cmd == dirty_test_def.cmd.GET_CAPABILITY or
        operation_pram.dirty_cmd == dirty_test_def.cmd.GET_RESULT or
        operation_pram.dirty_cmd == dirty_test_def.cmd.CLOSE then
        log:info('assemble_ipmi_request common, dirty_cmd %d', operation_pram.dirty_cmd)
        data = bs.new(dirty_test_cmd_request['common'].ipmi_request_info):pack({
            cmd = GET_ARM_NPU_CMD, lun = DMP_LAST_REQ, request_parameter = 2,
            op_cmd = 0xa9, op_fun = 0x06, offset = 0, data_length = 4,
            operation_type = operation_pram.dirty_cmd, optical_module_num = 1, optical_module_id = valid_op_id, reserve = 0})
    elseif operation_pram.dirty_cmd == dirty_test_def.cmd.INIT then
        log:info('assemble_ipmi_request init, dirty_cmd %d', operation_pram.dirty_cmd)
        data = bs.new(dirty_test_cmd_request['init'].ipmi_request_info):pack({
            cmd = GET_ARM_NPU_CMD, lun = DMP_LAST_REQ, request_parameter = 2,
            op_cmd = 0xa9, op_fun = 0x06, offset = 0, data_length = 10,
            operation_type = operation_pram.dirty_cmd, optical_module_num = 1, optical_module_id = valid_op_id, reserve1 = 0,
            duration_seconds = operation_pram.test_time, reserve2 = 0})
    elseif operation_pram.dirty_cmd == dirty_test_def.cmd.START then
        log:info('assemble_ipmi_request common, dirty_cmd %d', operation_pram.dirty_cmd)
        data = bs.new(dirty_test_cmd_request['start'].ipmi_request_info):pack({
            cmd = GET_ARM_NPU_CMD, lun = DMP_LAST_REQ, request_parameter = 2,
            op_cmd = 0xa9, op_fun = 0x06, offset = 0, data_length = 5,
            operation_type = operation_pram.dirty_cmd, start_item = operation_pram.start_item, optical_module_num = 1, optical_module_id = valid_op_id, reserve = 0})
    elseif operation_pram.dirty_cmd == dirty_test_def.cmd.PHASE1_GET_FFT or
            operation_pram.dirty_cmd == dirty_test_def.cmd.PHASE2_GET_FFT then
        log:info('assemble_ipmi_request get_fft, dirty_cmd %d', operation_pram.dirty_cmd)
        data = bs.new(dirty_test_cmd_request['get_fft'].ipmi_request_info):pack({
            cmd = GET_ARM_NPU_CMD, lun = DMP_LAST_REQ, request_parameter = 2,
            op_cmd = 0xa9, op_fun = 0x06, offset = 0, data_length = 6,
            operation_type = operation_pram.dirty_cmd, optical_module_num = 1, optical_module_id = valid_op_id, reserve1 = 0,
            fft_offset = operation_pram.fft_offset, reserve2 = 0})
    elseif operation_pram.dirty_cmd == dirty_test_def.cmd.PHASE1_SET_FFT or
            operation_pram.dirty_cmd == dirty_test_def.cmd.PHASE2_SET_FFT then
        log:info('assemble_ipmi_request set_fft, dirty_cmd %d', operation_pram.dirty_cmd)
        data = bs.new(dirty_test_cmd_request['set_fft'].ipmi_request_info):pack({
            cmd = GET_ARM_NPU_CMD, lun = DMP_LAST_REQ, request_parameter = 2,
            op_cmd = 0xa9, op_fun = 0x06, offset = 0, data_length = 9 + operation_pram.fft_length,
            operation_type = operation_pram.dirty_cmd, optical_module_num = 1, optical_module_id = valid_op_id, reserve1 = 0,
            fft_offset = operation_pram.fft_offset, end_flag = operation_pram.end_flag, fft_length = operation_pram.fft_length, reserve2 = 0, tail = operation_pram.fft_message})
    else
        log:error('assemble_ipmi_request: dirty_cmd %d invalid', operation_pram.dirty_cmd)
    end
    return data
end

local dirty_test_cmd_respond = {
    ['common'] = {ipmi_respond_info = '<<error_code:1/unit:8, opcode:2/little-unit:8, total_length:4/little-unit:8,' ..
        'length:4/little-unit:8, optical_module_num:1/little-unit:8, exe_result:1/little-unit:8, tail/string>>'},
    ['get_result'] = {ipmi_respond_info = '<<error_code:1/unit:8, opcode:2/little-unit:8, total_length:4/little-unit:8, length:4/little-unit:8, ' ..
        'optical_module_num:1/little-unit:8, dirty_status:1/signed-unit:8, dirty_result:1/little-unit:8, ' ..
        'bit_map_max:1/little-unit:8, reserve1:1/little-unit:8, ' ..
        'bit_map_second:1/little-unit:8, reserve2:1/little-unit:8, ' ..
        'bit_map_tx_warn:1/little-unit:8, reserve3:1/little-unit:8, ' ..
        'bit_map_rx_warn:1/little-unit:8, reserve4:1/little-unit:8, ' ..
        'max_reflection_pos1:32/little-float, max_reflection_pos2:32/little-float, max_reflection_pos3:32/little-float, max_reflection_pos4:32/little-float, ' ..
        'max_reflection_pos5:32/little-float, max_reflection_pos6:32/little-float, max_reflection_pos7:32/little-float, max_reflection_pos8:32/little-float, ' ..
        'second_reflection_pos1:32/little-float, second_reflection_pos2:32/little-float, second_reflection_pos3:32/little-float, second_reflection_pos4:32/little-float, ' ..
        'second_reflection_pos5:32/little-float, second_reflection_pos6:32/little-float, second_reflection_pos7:32/little-float, second_reflection_pos8:32/little-float, tail/string>>'},
    ['get_fft'] = {ipmi_respond_info = '<<error_code:1/unit:8, opcode:2/little-unit:8, total_length:4/little-unit:8,' ..
        'length:4/little-unit:8, optical_module_num:1/little-unit:8, fft_offset:1/little-unit:8, end_flag:1/little-unit:8,' .. 
        'fft_length:1/little-unit:8, reserve1:1/unit:8, tail/string>>'}
}

local function assemble_ipmi_respond(operation_pram)
    local data = nil
    if operation_pram.dirty_cmd == dirty_test_def.cmd.GET_CAPABILITY or
        operation_pram.dirty_cmd == dirty_test_def.cmd.INIT or
        operation_pram.dirty_cmd == dirty_test_def.cmd.START or
        operation_pram.dirty_cmd == dirty_test_def.cmd.CLOSE or 
        operation_pram.dirty_cmd == dirty_test_def.cmd.PHASE1_SET_FFT or
        operation_pram.dirty_cmd == dirty_test_def.cmd.PHASE2_SET_FFT then
        data = bs.new(dirty_test_cmd_respond['common'].ipmi_respond_info)
    elseif operation_pram.dirty_cmd == dirty_test_def.cmd.GET_RESULT then
        data = bs.new(dirty_test_cmd_respond['get_result'].ipmi_respond_info)
    elseif operation_pram.dirty_cmd == dirty_test_def.cmd.PHASE1_GET_FFT or
            operation_pram.dirty_cmd == dirty_test_def.cmd.PHASE2_GET_FFT then
        data = bs.new(dirty_test_cmd_respond['get_fft'].ipmi_respond_info)
    else
        log:error('assemble_ipmi_respond: dirty_cmd %d invalid', operation_pram.dirty_cmd)
    end
    return data
end

function test_object:send_dirty_ipmi_to_hdk(operation_pram)
    if operation_pram == nil or operation_pram.dirty_cmd == nil then
        log:debug("dirty test ipmi input pram is invalid")
        return nil
    end
    local dirty_req = self:assemble_ipmi_request(operation_pram)
    if dirty_req == nil then
        log:debug("assemble ipmi request fail, dirty cmd %d", operation_pram.dirty_cmd)
        return nil
    end
    local ok, result, payload = pcall(ipmi.request, self.bus, {channel_type.CT_ME:value(), self.npu_id},
        {DestNetFn = 0x30, Cmd = 0x98, Payload = dirty_req})
    if not ok then
        log:debug('send request by ipmi fail, err_message:%s', result)
        return nil
    end
    if result ~= comp_code.Success then
        log:debug('dirty ipmi request failed, npuid = %s, comp_code = 0x%x, dirty_cmd = %s', self.npu_id, result, operation_pram.dirty_cmd)
        return nil
    end
    local dirty_respond_bs = assemble_ipmi_respond(operation_pram)
    if dirty_respond_bs == nil then
        log:debug("assemble ipmi respond fail, dirty cmd %d", operation_pram.dirty_cmd)
        return nil
    end
    local rsp = dirty_respond_bs:unpack(payload)
    if not rsp then
        log:debug('unpack respond failed, dirty cmd %d', operation_pram.dirty_cmd)
        return nil
    end
    log:debug(rsp)
    return rsp
end

function test_object:update_dirty_test_supported()
    local operation_pram = {
        dirty_cmd = dirty_test_def.cmd.GET_CAPABILITY
    }
    local rsp = self:send_dirty_ipmi_to_hdk(operation_pram)
    if rsp == nil then
        self.support_dirty_test = nil
    elseif rsp.exe_result == dirty_test_def.dirty_support_type.SUPPORT then
        self.support_dirty_test = true
    else
        self.support_dirty_test = false
    end
end

function test_object:update_dirty_info(rsp)
    local pos
    self.dirty_result.contamination_detected = dirty_test_def.dirty_result.NotDetected
    -- 最大反射强度点位置
    for bit_num = 0, 7 do
        if rsp.bit_map_max & (1 << bit_num) == (1 << bit_num) then
            pos = 'max_reflection_pos' .. (bit_num + 1)
            self.dirty_result.first_contamination_distance[bit_num + 1] = rsp[pos] / 10 -- 转化成米
            self.dirty_result.contamination_detected = dirty_test_def.dirty_result.Detected
        else
            self.dirty_result.first_contamination_distance[bit_num + 1] = -1
        end
    end
    -- 第二大反射强度点位置
    for bit_num = 0, 7 do
        if rsp.bit_map_second & (1 << bit_num) == (1 << bit_num) then
            pos = 'second_reflection_pos' .. (bit_num + 1)
            self.dirty_result.second_contamination_distance[bit_num + 1] = rsp[pos] / 10 -- 转化成米
            self.dirty_result.contamination_detected = dirty_test_def.dirty_result.Detected
        else
            self.dirty_result.second_contamination_distance[bit_num + 1] = -1
        end
    end
    -- 本端是否存在脏污
    if rsp.bit_map_rx_warn ~= 0 then
        self.dirty_result.contamination_detected = dirty_test_def.dirty_result.Detected
        self.dirty_result.local_contamination_detected = dirty_test_def.dirty_result.Detected
    else
        self.dirty_result.local_contamination_detected = dirty_test_def.dirty_result.NotDetected
    end
    -- 对端是否存在脏污
    if rsp.bit_map_tx_warn ~= 0 then
        self.dirty_result.contamination_detected = dirty_test_def.dirty_result.Detected
        self.dirty_result.remote_contamination_detected = dirty_test_def.dirty_result.Detected
    else
        self.dirty_result.remote_contamination_detected = dirty_test_def.dirty_result.NotDetected
    end
end

function test_object:init_dirty_test()
    if self.duration_seconds == 0 then
        log:error('test object %s init failed because duration seconds is 0', self.test_object_id)
        return false
    end
    local operation_pram = {
        dirty_cmd = dirty_test_def.cmd.INIT,
        test_time = self.duration_seconds
    }
    return self:send_dirty_ipmi_to_hdk(operation_pram)
end

function test_object:start_dirty_test(action)
    if action == 'InitiationPhase1' then
        return self:init_dirty_test()
    end
    -- oDSP光模块没有阶段二，不做任何操作，返回成功
    if self.type == 'oDSP' and (action == 'InitiationPhase2' or action == 'DetectionPhase2') then
        return {exe_result = dirty_test_def.rsp_code.SUCCESS}
    end
    local operation_pram = {
        dirty_cmd = dirty_test_def.cmd.START,
        start_item = dirty_test_def.start_action_to_item_map[action]
    }
    return self:send_dirty_ipmi_to_hdk(operation_pram)
end

function test_object:close_dirty_test()
    local operation_pram = {
        dirty_cmd = dirty_test_def.cmd.CLOSE
    }
    return self:send_dirty_ipmi_to_hdk(operation_pram)
end

function test_object:get_dirty_test_result()
    local operation_pram = {
        dirty_cmd = dirty_test_def.cmd.GET_RESULT
    }
    local rsp = self:send_dirty_ipmi_to_hdk(operation_pram)
    if rsp == nil then
        return
    end
    -- 更新脏污测试状态
    local state_machine = rsp.dirty_status
    if self:check_state_machine_trans(state_machine) then
        self.stage = dirty_test_def.stage[state_machine]
        self.stage_status = dirty_test_def.stage_status[state_machine]
        self.state_machine = state_machine
        -- 从Running跳转到Completed，需要重置fft状态，以备下阶段更新fft信息
        if state_machine == dirty_test_def.state_machine.Phase1Completed or state_machine == dirty_test_def.state_machine.Phase2Completed then
            self:init_fft_state()
        end
    end
    -- 更新脏污测试数据
    if self.state_machine == dirty_test_def.state_machine.Phase2Completed then
        self:update_dirty_info(rsp)
    end
end

function test_object:get_fft_message()
    local operation_pram = {
        dirty_cmd = self.state_machine == dirty_test_def.state_machine.Phase1Running and dirty_test_def.cmd.PHASE1_GET_FFT or dirty_test_def.cmd.PHASE2_GET_FFT,
        fft_offset = self.get_fft_offset
    }
    local rsp = self:send_dirty_ipmi_to_hdk(operation_pram)
    if not rsp then
        return
    end
    if rsp.fft_offset ~= self.get_fft_offset then
        log:notice('the fft offset %s of contamination detection is not equal to expected offset %s', rsp.fft_offset, self.get_fft_offset)
        return
    end
    self.get_fft_offset = self.get_fft_offset + rsp.fft_length
    self.fft_message = self.fft_message .. rsp.tail
    -- 无报错，且查询fft完成
    if rsp.end_flag == 1 then
        self.get_fft_flag = true
    end
end

function test_object:set_fft_message()
    local fft_length = #self.fft_message - self.set_fft_offset + 1
    fft_length = (fft_length > MAX_FFT_LENGTH) and MAX_FFT_LENGTH or fft_length
    local end_flag = (self.set_fft_offset + fft_length >= #self.fft_message + 1) and 1 or 0
    local operation_pram = {
        dirty_cmd = self.state_machine == dirty_test_def.state_machine.Phase1Running and dirty_test_def.cmd.PHASE1_SET_FFT or dirty_test_def.cmd.PHASE2_SET_FFT,
        fft_offset = self.set_fft_offset,
        fft_length = fft_length,
        end_flag = end_flag,
        fft_message = string.sub(self.fft_message, self.set_fft_offset, self.set_fft_offset + fft_length - 1)
    }
    local rsp = self:send_dirty_ipmi_to_hdk(operation_pram)
    if not rsp or rsp.exe_result ~= dirty_test_def.rsp_code.SUCCESS then
        return
    end
    self.set_fft_offset = self.set_fft_offset + fft_length
    -- 无报错，且设置fft完成
    if end_flag == 1 then
        self.set_fft_flag = true
    end
end

function test_object:init_fft_state()
    self.fft_message = ''
    self.get_fft_flag = false
    self.get_fft_offset = 1
    self.set_fft_flag = false
    self.set_fft_offset = 1
end

function test_object:update_fft_message()
    -- 非LPO光模块，或者fft已经设置完成、或者非Running态，不需要更新fft
    if self.type ~= 'LPO' or self.set_fft_flag or
        (self.state_machine ~= dirty_test_def.state_machine.Phase1Running and self.state_machine ~= dirty_test_def.state_machine.Phase2Running) then
        return
    end
    if not self.get_fft_flag then
        self:get_fft_message()
    end
    if self.get_fft_flag then
        self:set_fft_message()
    end
end

function test_object:check_state_machine_trans(state_machine)
    if not state_machine or not dirty_test_def.state_machine_trans_map[self.type] or not dirty_test_def.state_machine_trans_map[self.type][self.state_machine] then
        return false
    end
    return dirty_test_def.state_machine_trans_map[self.type][self.state_machine][state_machine]
end

function test_object:clear_local_record()
    self.duration_seconds = 0
    self.state_machine = dirty_test_def.state_machine.Pending
    self.stage = dirty_test_def.stage[dirty_test_def.state_machine.Pending]
    self.stage_status = dirty_test_def.stage_status[dirty_test_def.state_machine.Pending]
    self:init_fft_state()
end

return test_object