-- 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 'prbs_test.optical_module_test_object'
local prbs_test_def = require 'prbs_test.prbs_test_def'
local cjson = require 'cjson'
local singleton = require 'mc.singleton'

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

-- 操作日志记录
local function log_prbs_test_operation(ctx, obj_id, prbs_cmd, cmd_result, cmd_param)
    if obj_id == nil or prbs_cmd == nil or cmd_result == nil then
        return
    end
    if prbs_cmd == prbs_test_def.cmd.CONFIG then
        if cmd_param == nil or cmd_param.ConfigType == nil or cmd_param.PatternId == nil or
            cmd_param.DurationSeconds == nil then
            return
        end
        if cmd_result == 'Success' then
            log:operation(ctx:get_initiator(), 'NetworkAdapter',
                'Configurate %s PRBS test successfully, ConfigType(%s), PatternId(%u), DurationSeconds(%s)',
                obj_id, cmd_param.ConfigType, cmd_param.PatternId, cmd_param.DurationSeconds)
        else
            log:operation(ctx:get_initiator(), 'NetworkAdapter',
                'Failed to configurate %s PRBS test, ConfigType(%s), PatternId(%u), DurationSeconds(%s)',
                obj_id, cmd_param.ConfigType, cmd_param.PatternId, cmd_param.DurationSeconds)
        end
    end
    if prbs_cmd == prbs_test_def.cmd.CLEAR_STATISTICS then
        if cmd_result == 'Success' then
            log:operation(ctx:get_initiator(), 'NetworkAdapter',
                'Clear %s PRBS test statistics successfully', obj_id)
        else
            log:operation(ctx:get_initiator(), 'NetworkAdapter',
                'Failed to clear %s PRBS test statistics', obj_id)
        end
    end
    if prbs_cmd == prbs_test_def.cmd.CLOSE then
        if cmd_result == 'Success' then
            log:operation(ctx:get_initiator(), 'NetworkAdapter',
                'Shutdown %s PRBS test successfully', obj_id)
        else
            log:operation(ctx:get_initiator(), 'NetworkAdapter',
                'Failed to shutdown %s PRBS test', obj_id)
        end
    end
end

local function find_test_object_by_npu_id(test_objects_list, npu_id, logical_id)
    for _, test_object in pairs(test_objects_list) 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 prbs_test:update_test_object(npu_id, test_object_id, test_object_presence, logical_id)
    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 = find_test_object_by_npu_id(self.test_objects, 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 prbs 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)
        -- 初始化光模块对象是否支持PRBS Test
        self.test_objects[test_obj_key]:get_prbs_test_supported()
    else
        if self.test_objects[test_obj_key].test_object_id ~= test_object_id then
            log:notice('update npu id %s logical id %s prbs 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_prbs_test = nil
        end
        if self.test_objects[test_obj_key].presence ~= test_object_presence then
            log:notice('update npu id %s logical id %s prbs 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
        -- 光模块不在位，是否支持prbs test属性恢复
        self.test_objects[test_obj_key].support_prbs_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 prbs_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 prbs_test:query_info(ctx, test_objects)
    if test_objects == nil or #test_objects > 8 then
        log:error('input parm is invaild')
        return
    end
    log:info(test_objects)

    local result = {}
    local op_test_object = nil
    for _, obj_id in pairs(test_objects) do
        op_test_object = self:get_test_object_by_object_id(obj_id, true)
        if op_test_object == nil then
            log:error('test object %s not exist', obj_id)
            -- 非支持对象返回unknown
            result[#result + 1] = {
                TestObjectId = obj_id,
                State = prbs_test_def.prbs_state.UNKNOWN,
                Statistics = {
                    BitErrorRate = {0, 0, 0, 0, 0, 0, 0, 0},
                    LossOfLock = {0, 0, 0, 0, 0, 0, 0, 0}
                }
            }
            goto continue
        end
        if self.update_task_running and op_test_object.update_self_running then
            -- 如果测试中直接从对象取数据
            result[#result + 1] = {
                TestObjectId = obj_id,
                State = op_test_object.state,
                Statistics = {
                    BitErrorRate = op_test_object.bit_error_rate,
                    LossOfLock = op_test_object.loss_of_lock
                }
            }
        else
            -- 如果非测试中，从HDK查询得到数据
            result[#result + 1] = {
                TestObjectId = obj_id,
                State = op_test_object:get_prbs_test_state(),
                Statistics = {
                    BitErrorRate = {0, 0, 0, 0, 0, 0, 0, 0},
                    LossOfLock = {0, 0, 0, 0, 0, 0, 0, 0}
                }
            }
        end
        ::continue::
    end
    return cjson.encode(result)
end

local function get_config_input_param(test_config_object)
    local operation_pram = {}
    if test_config_object == nil then
        log:error('test_config_object input invalid')
        return nil
    end
    if test_config_object.ConfigType == nil or test_config_object.PatternId == nil or
        test_config_object.DurationSeconds == nil then
        log:error('config input invalid, ConfigType:%s, PatternId:%s, DurationSeconds:%s',
            test_config_object.ConfigType, test_config_object.PatternId, test_config_object.DurationSeconds)
        return nil
    end
    operation_pram.prbs_cmd = prbs_test_def.cmd.CONFIG
    operation_pram.prbs_type = test_config_object.PatternId
    operation_pram.test_time = test_config_object.DurationSeconds
    if test_config_object.ConfigType == 'StartTransmitter' then
        operation_pram.config_item = prbs_test_def.config_item.START_TX
    elseif test_config_object.ConfigType == 'StartReceiver' then
        operation_pram.config_item = prbs_test_def.config_item.START_RX
    else
        log:error('config item %s invalid', test_config_object.ConfigType)
        return nil
    end
    return operation_pram
end

local function update_running_test_list(running_test_list, test_obj_config)
    local current_time = vos.vos_tick_get() // 1000
    for _, running_obj in pairs(running_test_list) do
        if running_obj.test_object_id == test_obj_config.TestObjectId then
            -- 如果已经在运行列表中，则更新信息
            running_obj.close_fail_count = 0
            running_obj.start_time = current_time
            running_obj.duration_seconds = test_obj_config.DurationSeconds
            log:notice('update obj %s in running test list', running_obj.test_object_id)
            return
        end
    end
    local running_test = {
        close_fail_count = 0,
        start_time = current_time,
        duration_seconds = test_obj_config.DurationSeconds,
        test_object_id = test_obj_config.TestObjectId
    }
    table.insert(running_test_list, running_test)
    log:notice('add obj %s to running test list', running_test.test_object_id)
end

function prbs_test:loop_config(ctx, test_objects_table)
    local result = {}
    local rsp = nil
    local op_test_object = nil
    local operation_pram = nil
    -- 再次尝试标志位
    local tried_again_flag = nil
    -- 步骤1 下发prbs测试配置
    for _, v in pairs(test_objects_table) do
        tried_again_flag = false
        op_test_object = self:get_test_object_by_object_id(v.TestObjectId, true)
        if op_test_object == nil then
            log:error('test object %s not exist', v.TestObjectId)
            log_prbs_test_operation(ctx, v.TestObjectId, prbs_test_def.cmd.CONFIG, 'Failed', v)
            result[#result + 1] = {v.TestObjectId, 'Failed'}
            goto continue
        end
        operation_pram = get_config_input_param(v)
        if operation_pram == nil then
            log:error('test object %s get config input parameter failed', v.TestObjectId)
            log_prbs_test_operation(ctx, v.TestObjectId, prbs_test_def.cmd.CONFIG, 'Failed', v)
            result[#result + 1] = {v.TestObjectId, 'Failed'}
            goto continue
        end
        ::again::
        rsp = op_test_object:send_prbs_ipmi_to_hdk(operation_pram)
        if rsp == nil or rsp.exe_result ~= prbs_test_def.config_result.SUCCESS then
            if not tried_again_flag then
                tried_again_flag = true
                log:error('test object %s config failed, try_again', v.TestObjectId)
                goto again
            end
            log:error('test object %s config failed', v.TestObjectId)
            log_prbs_test_operation(ctx, v.TestObjectId, prbs_test_def.cmd.CONFIG, 'Failed', v)
            result[#result + 1] = {v.TestObjectId, 'Failed'}
        else
            log_prbs_test_operation(ctx, v.TestObjectId, prbs_test_def.cmd.CONFIG, 'Success', v)
            result[#result + 1] = {v.TestObjectId, 'Success'}
            -- 配置成功的对象加入/更新到运行列表中
            update_running_test_list(self.running_test_list, v)
            -- 更新对象的信息：标志位为true说明处于测试中
            op_test_object:update_running(true)
        end
        ::continue::
    end
    return result
end

-- prbs测试配置
-- 步骤1 下发prbs测试配置
-- 步骤2 启动协程轮询每个光模块的STATE\GET_RESULT
-- 输入
-- test_objects = {
--     [1] = {
--         TestObjectId = '123',
--         ConfigType = 'StartTransmitter',
--         PatternId = 0,
--         DurationSeconds = 300
--     },
--     ...
-- }
function prbs_test:config(ctx, test_objects)
    if test_objects == nil then
        log:error('input parm is invaild')
        return {}
    end
    log:info(test_objects)

    local test_objects_table = cjson.decode(test_objects)
    if test_objects_table == nil or next(test_objects_table) == nil or #test_objects_table > 8 then
        log:error('decode input parm failed')
        return {}
    end
    log:info(test_objects_table)

    -- 步骤1 下发prbs测试配置
    local result = self:loop_config(ctx, test_objects_table)
    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

-- 轮询每个光模块的STATE\GET_RESULT
local function running_test_update_task(prbs_test_obj)
    log:notice('running test update task create')
    local test_obj = nil
    local close_ret = nil
    local current_time = nil
    while #prbs_test_obj.running_test_list > 0 do
        -- 遍历主体对象获取信息，刷新数据
        for k, v in pairs(prbs_test_obj.running_test_list) do
            -- 每光模块让出总线1s
            skynet.sleep(100)
            test_obj = prbs_test_obj: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)
                prbs_test_obj.running_test_list[k] = nil
                goto continue
            end

            -- 更新状态和统计
            test_obj:update_optical_module_info()

            -- 若已终止测试
            if test_obj.state == prbs_test_def.prbs_state.PENDING then
                -- 更新对象的信息：标志位为false说明不处于测试中
                test_obj:update_running(false)
                -- 清除统计并在running_test_list中删除对象
                test_obj:update(nil)
                log:notice('test stopped, close running test for object %s', v.test_object_id)
                prbs_test_obj.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_prbs_test()
            if close_ret ~= prbs_test_def.common_result.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 == prbs_test_def.common_result.SUCCESS or v.close_fail_count >= 3 then
                -- 更新对象的信息：标志位为false说明不处于测试中
                test_obj:update_running(false)
                -- 恢复状态及清除本地统计记录
                test_obj.state = prbs_test_def.prbs_state.PENDING
                test_obj:update(nil)
                -- running_test_list中删除对象
                log:notice('timeout and close running test for object %s', v.test_object_id)
                prbs_test_obj.running_test_list[k] = nil
            end
            ::continue::
        end
    end
    prbs_test_obj.update_task_running = false
    prbs_test_obj.running_test_list = {}
    log:notice('test info update task exit')
end

-- 终止测试
-- 步骤1 发送ipmi终止HDK测试
-- 步骤2 清除本地缓存
function prbs_test:shutdown(ctx, test_objects)
    if test_objects == nil or #test_objects > 8 then
        log:error('input parm is invaild')
        return {}
    end
    log:info(test_objects)
    local op_test_object = nil
    local result = {}
    for _, v in pairs(test_objects) 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_prbs_test_operation(ctx, v, prbs_test_def.cmd.CLOSE, 'Failed', nil)
            result[#result + 1] = {v, 'Failed'}
            goto continue
        end
        -- 仅下发终止测试并更新状态，running_test_list中对象配置的仍由test_info_update_task管理
        if op_test_object:close_prbs_test() ~= prbs_test_def.common_result.SUCCESS then
            log:error('shutdown test for object %s failed', v)
            log_prbs_test_operation(ctx, v, prbs_test_def.cmd.CLOSE, 'Failed', nil)
            result[#result + 1] = {v, 'Failed'}
        else
            log_prbs_test_operation(ctx, v, prbs_test_def.cmd.CLOSE, 'Success', nil)
            op_test_object.state = prbs_test_def.prbs_state.PENDING
            result[#result + 1] = {v, 'Success'}
        end
        ::continue::
    end
    return result
end

-- 清除PRBS统计数据
-- 步骤1 发送ipmi清除HDK数据
-- 步骤2 清除本地数据
function prbs_test:clear_statistics(ctx, test_objects)
    if test_objects == nil or #test_objects > 8 then
        log:error('input parm is invaild')
        return {}
    end
    log:info(test_objects)
    local op_test_object = nil
    local result = {}
    local exe_ret = nil
    for _, v in pairs(test_objects) 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_prbs_test_operation(ctx, v, prbs_test_def.cmd.CLEAR_STATISTICS, 'Failed', nil)
            result[#result + 1] = {v, 'Failed'}
            goto continue
        end
        op_test_object:creat_clear_task()
        ::continue::
    end

    -- 等待清除协程结果，最多等2s
    local wait_time = 10
    while wait_time > 0 do
        wait_time = wait_time - 1
        -- 集齐所有对象结果后可以提前退出循环
        if #result == #test_objects then
            break
        end

        -- 循环等待结果
        skynet.sleep(20)
        for _, v in pairs(self.test_objects) do
            -- 清除协程仍存在的暂不获取结果
            if v.clear_task_running then
                goto continue
            end
            -- 已获取过结果跳过的跳过
            if v.clear_result == nil then
                goto continue
            end
            exe_ret = v.clear_result == prbs_test_def.common_result.SUCCESS and 'Success' or 'Failed'
            result[#result + 1] = {v.test_object_id, exe_ret}
            v.clear_result = nil
            log_prbs_test_operation(ctx, v.test_object_id, prbs_test_def.cmd.CLEAR_STATISTICS, exe_ret, nil)
            ::continue::
        end
    end
    return result
end

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

return singleton(prbs_test)