-- 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 vos = require 'utils.vos'
local skynet = require 'skynet'
local test_object = require 'dirty_test.optical_module_test_object'
local dirty_test_def = require 'dirty_test.dirty_test_def'
local singleton = require 'mc.singleton'
local class = require 'mc.class'

local dirty_test = class()

function dirty_test:ctor(bus)
    self.update_task_running = false    -- 测试对象信息更新协程运行标志
    self.running_test_list = {}         -- 存储运行中的测试对象及配置，从外部输入获得
    self.test_objects = {}              -- 存储所有的测试对象，通过 test_object.new 创建
    self.test_object_indexes = {}       -- 对象sn到对象索引的映射
    self.bus = bus
end

-- 操作日志记录
local function log_dirty_test_operation(ctx, obj_id, dirty_cmd, cmd_result, cmd_param)
    if obj_id == nil or dirty_cmd == nil or cmd_result == nil then
        return
    end
    if dirty_cmd == dirty_test_def.cmd.INIT then
        if cmd_param == nil or cmd_param.DurationSeconds == nil then
            return
        end
        if cmd_result == 'Success' then
            log:operation(ctx:get_initiator(), 'NetworkAdapter', 'Init %s dirty test successfully, DurationSeconds(%s)', obj_id, cmd_param.DurationSeconds)
        else
            log:operation(ctx:get_initiator(), 'NetworkAdapter', 'Failed to init %s dirty test, DurationSeconds(%s)', obj_id, cmd_param.DurationSeconds)
        end
    elseif dirty_cmd == dirty_test_def.cmd.START then
        if cmd_result == 'Success' then
            log:operation(ctx:get_initiator(), 'NetworkAdapter', 'Start %s dirty test successfully', obj_id)
        else
            log:operation(ctx:get_initiator(), 'NetworkAdapter', 'Failed to start %s dirty test', obj_id)
        end
    elseif dirty_cmd == dirty_test_def.cmd.CLOSE then
        if cmd_result == 'Success' then
            log:operation(ctx:get_initiator(), 'NetworkAdapter', 'Shutdown %s dirty test successfully', obj_id)
        else
            log:operation(ctx:get_initiator(), 'NetworkAdapter', 'Failed to shutdown %s dirty test', obj_id)
        end
    end
end

function dirty_test:find_test_object_by_npu_id(npu_id, logical_id)
    for _, test_object in pairs(self.test_objects) do
        if test_object.npu_id == npu_id and test_object.logical_id == logical_id then
            return test_object
        end
    end
    return nil
end

-- 更新对象的信息，返回更新标志
-- 注意：测试过程中出现模块不在位，直接删除对象可能导致异常。因此，模块不在位或更换时仅更新信息
function dirty_test:update_test_object(npu_id, test_object_id, test_object_presence, logical_id, op_type)
    if test_object_id == '' or test_object_id == 'N/A' then
        return
    end
    -- 1、检查是否有更换动作，即新的加入前先检查是否有老的存在并删除
    if self.test_object_indexes[test_object_id] == nil then
        local old_obj = self:find_test_object_by_npu_id(npu_id, logical_id)
        if old_obj then
            self.test_object_indexes[old_obj.test_object_id] = nil
        end 
    end
    -- 2、更新测试对象主体
    local test_obj_key = string.format('%s_%s', npu_id, logical_id)
    if self.test_objects[test_obj_key] == nil then
        log:notice('create npu id %s logical id %s dirty test object, id: %s', npu_id, logical_id, test_object_id)
        self.test_objects[test_obj_key] = test_object.new(test_object_id, test_object_presence, npu_id, self.bus, logical_id, op_type)
        -- 初始化光模块对象是否支持dirty Test
        self.test_objects[test_obj_key]:update_dirty_test_supported()
    else
        -- 光模块类型周期性通过ipmb获取，初始化测试对象的时候可能还未获取到，需要每次更新
        self.test_objects[test_obj_key].type = op_type
        if self.test_objects[test_obj_key].test_object_id ~= test_object_id then
            log:notice('update npu id %s logical id %s dirty test object id: %s', npu_id, logical_id, test_object_id)
            self.test_objects[test_obj_key].test_object_id = test_object_id
            -- 存在感知换模块但不感知不在位的情况，也需要恢复标志
            self.test_objects[test_obj_key].support_dirty_test = nil
        end
        if self.test_objects[test_obj_key].presence ~= test_object_presence then
            log:notice('update npu id %s logical id %s dirty test object presence: %s', npu_id, logical_id, test_object_presence)
            self.test_objects[test_obj_key].presence = test_object_presence
        end
    end
    -- 3、更新测试对象索引
    if test_object_presence == 0 then
        -- 不在位仅删除索引，不删除主体
        self.test_object_indexes[test_object_id] = nil
        -- 光模块不在位，是否支持dirty test属性恢复
        self.test_objects[test_obj_key].support_dirty_test = nil
    else
        if self.test_object_indexes[test_object_id] ~= test_obj_key then
            self.test_object_indexes[test_object_id] = test_obj_key
        end
    end
end

function dirty_test:get_test_object_by_object_id(object_id, assert_present)
    local test_obj_key = self.test_object_indexes[object_id]
    if test_obj_key == nil then
        return nil
    end
    if assert_present == true and self.test_objects[test_obj_key].presence == 0 then
        return nil
    end
    return self.test_objects[test_obj_key]
end

function dirty_test:update_running_test_list(test_obj)
    local running_test = {
        close_fail_count = 0,
        start_time = vos.vos_tick_get() // 1000,
        duration_seconds = test_obj.duration_seconds,
        test_object_id = test_obj.test_object_id
    }
    table.insert(self.running_test_list, running_test)
    log:notice('add obj %s to running test list', running_test.test_object_id)
end

-- 轮询每个光模块的STATE\GET_RESULT
function dirty_test:running_test_update_task()
    log:notice('running test update task create')
    local test_obj, close_ret, current_time
    while #self.running_test_list > 0 do
        -- 遍历主体对象获取信息，刷新数据
        for k, v in pairs(self.running_test_list) do
            -- 每光模块让出总线1s
            skynet.sleep(100)
            test_obj = self:get_test_object_by_object_id(v.test_object_id, false)
            if test_obj == nil then
                log:notice('obj not exist, close running test for object %s', v.test_object_id)
                self.running_test_list[k] = nil
                goto continue
            end
            -- 初始化需要时间，不能直接判断是否终止检测,先等待10s
            if test_obj.state_machine == dirty_test_def.state_machine.Pending then
                skynet.sleep(1000)
            end
            -- 更新fft信息
            test_obj:update_fft_message()
            -- 更新状态和统计
            test_obj:get_dirty_test_result()
            -- 若已终止测试或测试异常
            if test_obj.state_machine == dirty_test_def.state_machine.Pending or test_obj.state_machine == dirty_test_def.state_machine.Abnormal then
                log:notice('test stopped, close running test for object %s', v.test_object_id)
                -- 恢复状态及清除本地统计记录
                test_obj:clear_local_record()
                -- 从running_test_list中移除
                self.running_test_list[k] = nil
                goto continue
            end
            -- 未超时不处理
            current_time = vos.vos_tick_get() // 1000
            if current_time - v.start_time < v.duration_seconds then
                goto continue
            end
            -- 若超时，终止测试
            close_ret = test_obj:close_dirty_test()
            if not close_ret or close_ret.exe_result ~= dirty_test_def.rsp_code.SUCCESS then
                log:error('timeout but close running test for object %s fail', v.test_object_id)
                v.close_fail_count = v.close_fail_count + 1
            end
            -- close成功或已失败3次，不再管理
            if close_ret.exe_result == dirty_test_def.rsp_code.SUCCESS or v.close_fail_count >= 3 then
                -- 恢复状态及清除本地统计记录
                test_obj:clear_local_record()
                -- running_test_list以及ready_list中删除对象
                log:notice('timeout and close running test for object %s', v.test_object_id)
                self.running_test_list[k] = nil
            end
            ::continue::
        end
    end
    self.update_task_running = false
    self.running_test_list = {}
    log:notice('test info update task exit')
end

function dirty_test:create_running_test_update_task()
    if self.update_task_running then
        return
    end
    self.update_task_running = true
    skynet.fork_once(self.running_test_update_task, self)
end

-- dirty测试配置
-- 步骤1 下发dirty测试配置
-- 输入
-- Detectors = {
--     [1] = {
--         DetectorId = '123',
--         DurationSeconds = 300
--     },
--     ...
-- }
function dirty_test:initiate(ctx, params)
    if params == nil or next(params) == nil or #params > 8 then
        log:error('input parm is invaild')
        return {}
    end
    -- 仅保存初始化配置,实际初始化动作在start执行
    local detector_id, op_test_object, rsp
    local result = {}
    for _, v in pairs(params) do
        detector_id = v.DetectorId
        if v.DurationSeconds < 60 or v.DurationSeconds > 900 then
            log:error('init param DurationSeconds %s is invalid', v.DurationSeconds)
            log_dirty_test_operation(ctx, detector_id, dirty_test_def.cmd.INIT, 'Failed', v)
            result[#result + 1] = {detector_id, 'Failed'}
        end
        op_test_object = self:get_test_object_by_object_id(detector_id, true)
        if op_test_object == nil then
            log:error('test object %s not exist', detector_id)
            log_dirty_test_operation(ctx, detector_id, dirty_test_def.cmd.INIT, 'Failed', v)
            result[#result + 1] = {detector_id, 'Failed'}
        elseif op_test_object.state_machine ~= dirty_test_def.state_machine.Pending then
            log:error('init is not supported by current state')
            log_dirty_test_operation(ctx, detector_id, dirty_test_def.cmd.INIT, 'Failed', v)
            result[#result + 1] = {detector_id, 'Failed'}
        else
            -- 保存超时时间
            op_test_object.duration_seconds = v.DurationSeconds
            result[#result + 1] = {detector_id, 'Success'}
        end
    end
    return result
end

-- dirty测试开始
-- 步骤1 启动dirty测试
-- 步骤2 启动协程轮询STATE\GET_RESULT
-- 输入
-- Detectors = {
--     [1] = {
--         DetectorId = '123',
--         Operation = ''
--     },
--     ...
-- }
function dirty_test:start(ctx, params)
    if params == nil or next(params) == nil or #params > 8 then
        log:error('input parm is invaild')
        return {}
    end
    -- 步骤1 启动dirty测试
    local result = {}
    local rsp, state_machine
    local op_test_object, test_object_id, start_action, tried_again_flag
    for _, v in pairs(params) do
        test_object_id = v.Detector[1].Value
        start_action = v.Detector[2].Value
        if not start_action then
            log:error('start param is invalid')
            log_dirty_test_operation(ctx, test_object_id, dirty_test_def.cmd.START, 'Failed', v)
            result[#result + 1] = {test_object_id, 'Failed'}
            goto continue
        end
        tried_again_flag = false
        op_test_object = self:get_test_object_by_object_id(test_object_id, true)
        if op_test_object == nil then
            log:error('test object %s not exist', test_object_id)
            log_dirty_test_operation(ctx, test_object_id, dirty_test_def.cmd.START, 'Failed', v)
            result[#result + 1] = {test_object_id, 'Failed'}
            goto continue
        end
        -- bmc重启场景，光模块可能处于脏污测试状态，需主动更新一次状态
        op_test_object:get_dirty_test_result()
        -- 检查是否允许start操作
        state_machine = dirty_test_def.start_param_to_machine_map[start_action]
        if not state_machine or not op_test_object:check_state_machine_trans(state_machine) then
            log:error('operation %s is invalid or not supported by current state', start_action)
            log_dirty_test_operation(ctx, test_object_id, dirty_test_def.cmd.START, 'Failed', v)
            result[#result + 1] = {test_object_id, 'Failed'}
            goto continue
        end
        ::again::
        rsp = op_test_object:start_dirty_test(start_action)
        if not rsp or rsp.exe_result ~= dirty_test_def.rsp_code.SUCCESS then
            if not tried_again_flag then
                tried_again_flag = true
                log:error('test object %s start failed, try_again', test_object_id)
                goto again
            end
            log:error('test object %s start failed', test_object_id)
            log_dirty_test_operation(ctx, test_object_id, dirty_test_def.cmd.START, 'Failed', v)
            result[#result + 1] = {test_object_id, 'Failed'}
        else
            log_dirty_test_operation(ctx, test_object_id, dirty_test_def.cmd.START, 'Success', v)
            result[#result + 1] = {test_object_id, 'Success'}
            -- 配置成功的对象加入/更新到运行列表中
            self:update_running_test_list(op_test_object)
            -- 只有中间态Running，才需要主动更新状态机，否则内部轮询更新
            if state_machine == dirty_test_def.state_machine.Phase1Running or state_machine == dirty_test_def.state_machine.Phase2Running then
               op_test_object.state_machine = state_machine
               op_test_object.stage = dirty_test_def.stage[state_machine]
               op_test_object.stage_status = dirty_test_def.stage_status[state_machine]
            end
        end
        ::continue::
    end
    if next(self.running_test_list) == nil then
        log:info('running_test_list is empty, do not start config task')
        return result
    end
    -- 步骤2 启动协程轮询STATE\GET_RESULT
    self:create_running_test_update_task()
    return result
end

-- 从测试主体集合中获取信息
function dirty_test:get_result(ctx, params)
    if params == nil or #params > 8 then
        log:error('input parm is invaild')
        return
    end
    log:info(params)
    local result = {}
    local op_test_object = nil
    local struct
    for _, obj_id in pairs(params) do
        struct = {Detection = {}}
        op_test_object = self:get_test_object_by_object_id(obj_id, true)
        if op_test_object == nil or op_test_object.state_machine == dirty_test_def.state_machine.Abnormal then
            -- 非支持对象返回unknown
            log:error('test object %s not exist', obj_id)
            struct.Detection[#struct.Detection + 1] = {Key = 'DetectorId', Value = obj_id}
            struct.Detection[#struct.Detection + 1] = {Key = 'DetectionPhase', Value = dirty_test_def.stage[dirty_test_def.state_machine.Abnormal]}
            struct.Detection[#struct.Detection + 1] = {Key = 'DetectionStatus', Value = dirty_test_def.stage[dirty_test_def.state_machine.Abnormal]}
            struct.Detection[#struct.Detection + 1] = {Key = 'ContaminationDetected', Value = dirty_test_def.dirty_result.Unknown}
            struct.Detection[#struct.Detection + 1] = {Key = 'LocalContaminationDetected', Value = dirty_test_def.dirty_result.Unknown}
            struct.Detection[#struct.Detection + 1] = {Key = 'RemoteContaminationDetected', Value = dirty_test_def.dirty_result.Unknown}
            struct.Detection[#struct.Detection + 1] = {Key = 'FirstContaminationDistance', Value = '-1/-1/-1/-1/-1/-1/-1/-1'}
            struct.Detection[#struct.Detection + 1] = {Key = 'SecondContaminationDistance', Value = '-1/-1/-1/-1/-1/-1/-1/-1'}
        else
            -- 支持对象直接获取，获取前更新一次脏污信息
            op_test_object:get_dirty_test_result()
            struct.Detection[#struct.Detection + 1] = {Key = 'DetectorId', Value = obj_id}
            struct.Detection[#struct.Detection + 1] = {Key = 'DetectionPhase', Value = op_test_object.stage}
            struct.Detection[#struct.Detection + 1] = {Key = 'DetectionStatus', Value = op_test_object.stage_status}
            struct.Detection[#struct.Detection + 1] = {Key = 'ContaminationDetected', Value = op_test_object.dirty_result.contamination_detected}
            struct.Detection[#struct.Detection + 1] = {Key = 'LocalContaminationDetected', Value = op_test_object.dirty_result.local_contamination_detected}
            struct.Detection[#struct.Detection + 1] = {Key = 'RemoteContaminationDetected', Value = op_test_object.dirty_result.remote_contamination_detected}
            struct.Detection[#struct.Detection + 1] = {Key = 'FirstContaminationDistance', Value = table.concat(op_test_object.dirty_result.first_contamination_distance, '/')}
            struct.Detection[#struct.Detection + 1] = {Key = 'SecondContaminationDistance', Value = table.concat(op_test_object.dirty_result.second_contamination_distance, '/')}
        end
        result[#result + 1] = struct
    end
    return result
end

-- 终止测试
function dirty_test:shutdown(ctx, params)
    if params == nil or #params > 8 then
        log:error('input parm is invaild')
        return {}
    end
    log:info(params)
    local op_test_object = nil
    local result = {}
    local rsp
    for _, v in pairs(params) do
        op_test_object = self:get_test_object_by_object_id(v, true)
        if op_test_object == nil then
            log:error('input test object %s not exist', v)
            log_dirty_test_operation(ctx, v, dirty_test_def.cmd.CLOSE, 'Failed', nil)
            result[#result + 1] = {v, 'Failed'}
            goto continue
        end
        -- 仅下发终止测试并更新状态，running_test_list中对象配置的仍由test_info_update_task管理，防止公共变量冲突
        rsp = op_test_object:close_dirty_test()
        if not rsp or rsp.exe_result ~= dirty_test_def.rsp_code.SUCCESS then
            log:error('shutdown test for object %s failed', v)
            log_dirty_test_operation(ctx, v, dirty_test_def.cmd.CLOSE, 'Failed', nil)
            result[#result + 1] = {v, 'Failed'}
        else
            log_dirty_test_operation(ctx, v, dirty_test_def.cmd.CLOSE, 'Success', nil)
            result[#result + 1] = {v, 'Success'}
            -- 恢复状态及清除本地统计记录
            op_test_object:clear_local_record()
        end
        ::continue::
    end
    return result
end

return singleton(dirty_test)