-- 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 bs = require 'mc.bitstring'
local utils = require 'mc.utils'
local file_sec = require 'utils.file'
local utils_core = require 'utils.core'
local log_collector = require 'device.class.log_collector'
local ncsi_core = require 'ncsi.ncsi_core'
local card_resource_mpu = require 'device.class.network_card_adapter.resource_tree.card_resource_mpu'

local card_resource_ncsi_collect_log = {}

local LOG_RETRY_TIMES<const> = 3
local GETLOG_SUBCMD_WITH_NEW_CARD_FW<const> = 0x15
local GETLOG_SUBCMD_WITH_OLD_CARD_FW<const> = 0x12

-- 网卡需要收集的日志类型
-- 必须要使用匿名函数，只有这样时间戳才会变化
local LOG_TYPES<const> = {
    [0] = function ()
        return 'type0_' .. log_collector.get_time() .. '.bin'
    end,    -- runlog
    [1] = function ()
        return 'type1_' .. log_collector.get_time() .. '.bin'
    end,    -- counter
    [2] = function ()
        return 'type2_' .. log_collector.get_time() .. '.bin'
    end,    -- register
    [3] = function ()
        return 'type3_' .. log_collector.get_time() .. '.bin'
    end     -- black box
}

local function get_log_by_ncsi(log_type, total_frame, base_offset, resource_obj)
    local rsptext = {}
    local rsp
    local frame_index = 1
    local reqdata = 0
    local req_format = bs.new([[<<
        offset:32,
        length:32
    >>]])
    -- 优先使用新固件0x15命令字，若无响应则使用旧固件命令字0x12
    local sub_cmd = GETLOG_SUBCMD_WITH_NEW_CARD_FW

    -- 通过ncsi获取指定offset日志，每帧长度1024
    while frame_index <= total_frame do
        reqdata = req_format:pack({
            offset = base_offset + 1024 * (frame_index - 1),
            length = 1024
        })
        rsp = resource_obj.ncsi_config_obj:GetLog({
            sub_cmd_id = sub_cmd,
            extra_cmd = log_type,
            data = reqdata,
            package_id = resource_obj.package_id
        }):value()
        if not rsp then
            if sub_cmd == GETLOG_SUBCMD_WITH_OLD_CARD_FW then -- 旧命令字0x12发不通，则退出
                log:error('get log by ncsi failed')
                return false, table.concat(rsptext)
            end
            -- 新命令字0x15发不通，后续全部采用旧命令字，并重置循环
            sub_cmd = GETLOG_SUBCMD_WITH_OLD_CARD_FW
            rsptext = {}
            frame_index = 1
        else
            rsptext[frame_index] = rsp
            frame_index = frame_index + 1
        end
    end

    return true, table.concat(rsptext)
end

local function fetch_log_by_ncsi(log_type, total_frame, base_offset, log_file, resource_obj)
    local fp_w, err = file_sec.open_s(log_file, 'wb+')
    if not fp_w then
        log:error('open file failed, err: %s', err)
        return false
    end
    return utils.safe_close_file(fp_w, function()
        utils_core.chmod_s(log_file, utils.S_IRUSR | utils.S_IWUSR | utils.S_IRGRP) -- 文件权限设置为0640

        local ret, log_data = get_log_by_ncsi(log_type, total_frame, base_offset, resource_obj)
        if not ret then
            log:info('collect type %s log failed', log_type)
            return false
        end
        fp_w:write(log_data)
        log:info('finish to collect type %s log', log_type)
        return true
    end)
end

local function collect_log_by_rmii(log_dir, resource_obj)
    local time = log_collector.get_time()
    local ok = ncsi_core.ncsi_get_log(0, 'eth0', log_dir, time)
    if not ok then
        log:notice('cannot fetch log by RMII Based Transport')
    end
    local curr_log_files_rmii = {}
    curr_log_files_rmii[#curr_log_files_rmii + 1] = 'mpu_ucode_' .. time .. '.bin'
    utils_core.chmod_s((log_dir .. curr_log_files_rmii[#curr_log_files_rmii]),
        utils.S_IRUSR | utils.S_IWUSR | utils.S_IRGRP) -- 文件权限设置为0640

    curr_log_files_rmii[#curr_log_files_rmii + 1] = 'last_word_' .. time .. '.bin'
    utils_core.chmod_s((log_dir .. curr_log_files_rmii[#curr_log_files_rmii]),
        utils.S_IRUSR | utils.S_IWUSR | utils.S_IRGRP) -- 文件权限设置为0640

    curr_log_files_rmii[#curr_log_files_rmii + 1] = 'counter_' .. time .. '.bin'
    utils_core.chmod_s((log_dir .. curr_log_files_rmii[#curr_log_files_rmii]),
        utils.S_IRUSR | utils.S_IWUSR | utils.S_IRGRP) -- 文件权限设置为0640

    curr_log_files_rmii[#curr_log_files_rmii + 1] = 'register_' .. time .. '.bin'
    utils_core.chmod_s((log_dir .. curr_log_files_rmii[#curr_log_files_rmii]),
        utils.S_IRUSR | utils.S_IWUSR | utils.S_IRGRP) -- 文件权限设置为0640

    log_collector.delete_old_log_file(log_dir, curr_log_files_rmii)
    log:notice('finish to dump log for netcard by RMII Based Transport, NodeId:%s', resource_obj.NodeId)
end

-- SDI6.0收集命令字和逻辑与5.0存在差异
-- 先使用collect_log_by_rmii收集，如果前者失败，再使用该函数收集
local function collect_log_by_rmii_new(log_dir, resource_obj)
    log:notice('start to collect log by rmii with new command')
    local time = log_collector.get_time()
    local ok = ncsi_core.ncsi_get_log_new(0, 'eth0', log_dir, time)
    if not ok then
        log:notice('cannot fetch log by RMII with new command')
    end

    local curr_log_files_rmii = {
        'type0_' .. time .. '.bin',
        'type1_' .. time .. '.bin',
        'type2_' .. time .. '.bin',
        'type3_' .. time .. '.bin'
    }
    for _, filename in pairs(curr_log_files_rmii) do
        utils_core.chmod_s(log_dir .. filename, utils.S_IRUSR | utils.S_IWUSR | utils.S_IRGRP) -- 文件权限设置为0640
    end

    log_collector.delete_old_log_file(log_dir, curr_log_files_rmii)
    log:notice('finish to dump log for netcard by RMII with new command, NodeId:%s', resource_obj.NodeId)
    return ok
end

-- 采用重试机制获取日志
local function excute_task_retry(retry_times, title, cb)
    for i = 1, retry_times do
        local ok, ret = pcall(cb)
        if ok then
            return ret
        end
        log:error("excute task(%s) failed, retry times: %d, err: %s", title, i, ret)
        skynet.sleep(10)
    end
end

local function get_sub_log(req_format, sub_log_len, log_type, fp_w, sub_idx, cur_len, resource_obj)
    local cur_max_len = 0   -- 当前子日志应当获取的最大长度，用于日志补0
    local last_frame = 0    -- 表示当前是否为最后一帧
    local remain_len = 0    -- 需要补0的长度
    cur_max_len = cur_max_len + sub_log_len[sub_idx] * 1024
    local rsptext = {}
    while true do
        local rsp = excute_task_retry(LOG_RETRY_TIMES, 'GetNewLog Data', function ()
            local reqdata = req_format:pack({
                frame_type = 0,
                offset = cur_len,
                length = 1024
            })
            local rsp_data = resource_obj.ncsi_config_obj:GetNewLog({
                extra_cmd = log_type,
                data = reqdata,
                package_id = resource_obj.package_id
            }):value()
            if not rsp_data then
                error('get log data failed')
            end
            return rsp_data
        end)
        if not rsp then
            return false
        end
        last_frame = string.unpack('<I1', rsp)
        local data = string.sub(rsp, 5)
        rsptext[#rsptext + 1] = data
        if last_frame == 1 then
            -- 最后一帧可能没有1024，需要获取具体长度
            cur_len = cur_len + #data
            if cur_len < cur_max_len then
                remain_len = cur_max_len - cur_len
                cur_len = cur_max_len
            end
            break
        end
        cur_len = cur_len + 1024
    end

    rsptext[#rsptext + 1] = string.rep('\x00', remain_len)
    fp_w:write(table.concat(rsptext))
    return cur_len
end

local function fetch_log_data(sub_log_num, total_length, sub_log_len, req_format, log_type, fp_w, resource_obj)
    local sub_idx = 1   -- 当前获取的子日志序列
    local cur_len = 0   -- 当前已获取日志长度，单位byte

    while sub_idx <= sub_log_num and cur_len < total_length * 1024 do
        local ret = get_sub_log(req_format, sub_log_len, log_type, fp_w, sub_idx, cur_len, resource_obj)
        if not ret then
            return false
        end
        cur_len = ret
        sub_idx = sub_idx + 1
    end
    return true
end

-- 获取指定类型的日志
local function get_spec_log(log_type, log_file, resource_obj)
    log:notice('start to collect log, log type: %s', log_type)

    local req_format = bs.new([[<<
        frame_type:32,
        offset:32,
        length:32
    >>]])

    -- 发送控制帧，获取子日志个数
    local rsp = excute_task_retry(LOG_RETRY_TIMES, 'GetNewLog Control', function ()
        local reqdata = req_format:pack({
            frame_type = 0x5a5a5a5a,
            offset = 0,
            length = 0
        })
        local rsp_data = resource_obj.ncsi_config_obj:GetNewLog({
            extra_cmd = log_type,
            data = reqdata,
            package_id = resource_obj.package_id
        }):value()
        if not rsp_data then
            error('send control command failed')
        end
        return rsp_data
    end)

    if not rsp then
        return false
    end

    -- 在控制帧发送成功后再创建文件
    local fp_w, err = file_sec.open_s(log_file, 'wb+')
    if not fp_w then
        log:error('open file failed, err: %s', err)
        return false
    end
    return utils.safe_close_file(fp_w, function()
        utils_core.chmod_s(log_file, utils.S_IRUSR | utils.S_IWUSR | utils.S_IRGRP) -- 文件权限设置为0640

        local total_length, sub_log_num, _, pos = string.unpack('<I4I1B', rsp)
        local sub_log_len = {}
        for _ = 1, sub_log_num do
            sub_log_len[#sub_log_len + 1], pos = string.unpack('<I2', rsp, pos)
        end

        -- 发送数据帧，获取日志内容
        if not fetch_log_data(sub_log_num, total_length, sub_log_len, req_format, log_type, fp_w, resource_obj) then
            return false
        end
        return true
    end)
end

-- sdi6.0新增命令获取网卡日志，新命令将和BMC解耦
local function collect_log_by_new_cmd(log_dir, curr_log_files, resource_obj)
    for k, v in pairs(LOG_TYPES) do
        curr_log_files[#curr_log_files + 1] = v()
        if not get_spec_log(k, log_dir .. curr_log_files[#curr_log_files], resource_obj) then
            log:error("collect log by ncsi failed, log_type: %s", k)
            return false
        end
    end
    return true
end

local function collect_log_by_old_cmd(curr_log_files, log_dir, resource_obj)
    local is_pcie_disconnected = false
    -- 获取mpu_ucode日志
    curr_log_files[#curr_log_files + 1] = 'mpu_ucode_' .. log_collector.get_time() .. '.bin'
    local ok = fetch_log_by_ncsi(0, 640, 0, log_dir .. curr_log_files[#curr_log_files], resource_obj)
    if not ok then
        is_pcie_disconnected = true
        log:notice('cannot fetch mpu ucode log')
    end

    -- 获取last_word日志
    curr_log_files[#curr_log_files + 1] = 'last_word_' .. log_collector.get_time() .. '.bin'
    ok = fetch_log_by_ncsi(0, 256, 0xA0000, log_dir .. curr_log_files[#curr_log_files], resource_obj)
    if not ok then
        is_pcie_disconnected = true
        log:notice('cannot fetch last word log')
    end

    -- 获取counter日志
    curr_log_files[#curr_log_files + 1] = 'counter_' .. log_collector.get_time() .. '.bin'
    ok = fetch_log_by_ncsi(1, 136, 0, log_dir .. curr_log_files[#curr_log_files], resource_obj)
    if not ok then
        is_pcie_disconnected = true
        log:notice('cannot fetch counter log')
    end

    -- 获取register日志
    curr_log_files[#curr_log_files + 1] = 'register_' .. log_collector.get_time() .. '.bin'
    ok = fetch_log_by_ncsi(2, 8, 0, log_dir .. curr_log_files[#curr_log_files], resource_obj)
    if not ok then
        is_pcie_disconnected = true
        log:notice('cannot fetch register log')
    end
    return is_pcie_disconnected
end

-- sdi通过ncsi收集日志，与smbus隔离开
function card_resource_ncsi_collect_log.collect_log_by_ncsi_task(resource_obj)
    local available = card_resource_mpu.wait_mpu_idle(resource_obj)
    if not available then
        return
    end
    local log_dir = log_collector.get_ncsi_log_dir(resource_obj)
    if not log_dir then
        return
    end
    if not log_collector.create_dir(log_dir) then
        return
    end

    log:notice('start to dump ncsi log for netcard, NodeId:%s', resource_obj.NodeId)
    local curr_log_files = {}
    local is_pcie_disconnected = false

    -- 首先尝试新命令获取日志，如果获取失败，则采用原方式获取
    local ok = collect_log_by_new_cmd(log_dir, curr_log_files, resource_obj)
    if not ok then
        curr_log_files = {}
        is_pcie_disconnected = collect_log_by_old_cmd(curr_log_files, log_dir, resource_obj)
    end

    -- 删除老的日志
    log_collector.delete_old_log_file(log_dir, curr_log_files)
    log:notice('finish to dump log for netcard, NodeId:%s', resource_obj.NodeId)
    if is_pcie_disconnected then
        -- 优先使用新命令获取日志，新命令获取失败使用老命令
        if not collect_log_by_rmii_new(log_dir, resource_obj) then
            collect_log_by_rmii(log_dir, resource_obj)
        end
    end
end

return card_resource_ncsi_collect_log