-- 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.

-- Description: 光模块dfx日志收集

local log = require 'mc.logging'
local utils = require 'mc.utils'
local utils_core = require 'utils.core'
local file_sec = require 'utils.file'
local skynet = require 'skynet'
local vos = require 'utils.vos'
local dfx_defs = require 'dfx_collect.dfx_defs'
local log_collector = require 'device.class.log_collector'
local DEFAULT_MSG_TYPE<const> = 0xff
local optical_module_dfx = {}
optical_module_dfx.__index = optical_module_dfx

local function move_log(src_file, dst_file)
    utils.remove_file(dst_file)
    if vos.get_file_accessible(src_file) then
        file_sec.move_file_s(src_file, dst_file)
    end
end

local function create_header_buffer(msg_type, dfx_obj)
    dfx_obj.msg_type = msg_type
    local buffer = ""
    if msg_type == dfx_defs.optical_module.LINKDOWN then
        buffer = string.format('LogTime,CollectType,Location,OpticalModuleId,TimeStamp,' ..
        'VccCurrent(0.1mV),TxPowerCurrent(0.1uW),RxPowerCurrent(0.1uW),TxBiasCurrent(uA),' ..
        'TxLos,RxLos,LatchedTxLol,LatchedRxLol,Temperature(degrees C),TxFault,HostSNR,MediaSNR\n')
    elseif msg_type == dfx_defs.optical_module.PERIOD then
        buffer = string.format('LogTime,CollectType,Location,OpticalModuleId,VccCurrentMax(0.1mV),' ..
        'VccCurrentMin(0.1mV),TxPowerCurrentMax(0.1uW),TxPowerCurrentMin(0.1uW),RxPowerCurrentMax(0.1uW),' ..
        'RxPowerCurrentMin(0.1uW),TxBiasCurrentMax(uA),TxBiasCurrentMin(uA),TxLos,RxLos,LatchedTxLol,' ..
        'LatchedRxLol,TemperatureMax(degrees C),TemperatureMin(degrees C),OdspDieTemperatureMax(degrees C),' ..
        'OdspDieTemperatureMin(degrees C),OdspHighTempRuntime(s),HostSNRMax,HostSNRMin,MediaSNRMax,MediaSNRMin\n')
    end
    return buffer
end

local function needs_header(msg_type, dfx_obj, file_length)
    if file_length < dfx_defs.optical_module_file_info.FILE_SIZE and file_length > 0 then
        -- msg_type不切换时不会有表头
        if msg_type == dfx_obj.msg_type then
            return false
        end
        -- BMC重启场景or组件重启场景，这种一开始是需要表头的，没有表头会呈现有歧义
        if dfx_obj.msg_type == DEFAULT_MSG_TYPE then
            dfx_obj.msg_type = msg_type
        end
    end
    return true
end

local function get_optical_module_id(npu_id)
    -- 两个npu_id映射一个光模块，当前npuid为硬编码在csr中属性，固定1至16，共8个光模块
    return math.floor((npu_id + 1) / 2)
end

local function rotate()
    local src_file = nil
    local dst_file = nil
    -- 顺序挪动xxlog1.tar.gz -> xxlog2.tar.gz -> xxlog3.tar.gz -> xxlog4.tar.gz
    for i = dfx_defs.optical_module_file_info.COMPRESS_NUM - 1, 1, -1 do
        src_file = dfx_defs.optical_module_file_info.PATH ..
            dfx_defs.optical_module_file_info.FILE_NAME .. i .. '.tar.gz'
        dst_file = dfx_defs.optical_module_file_info.PATH ..
            dfx_defs.optical_module_file_info.FILE_NAME .. i + 1 .. '.tar.gz'
        move_log(src_file, dst_file)
    end
    -- 压缩原文件
    src_file = dfx_defs.optical_module_file_info.FILE_PATH
    dst_file = dfx_defs.optical_module_file_info.PATH .. 
        dfx_defs.optical_module_file_info.FILE_NAME  .. '1.tar.gz'
    if vos.get_file_accessible(src_file) then
        vos.check_before_system_s('/bin/tar', '-czf', dst_file, '-C',
            dfx_defs.optical_module_file_info.PATH, 'optical_module_history_info_log.csv')
        --删除文件
        utils.remove_file(src_file)
        utils_core.chmod_s(dst_file, utils.S_IRUSR | utils.S_IRGRP)
    end
end

local function write_buf_to_file(buffer, dfx_obj, msg_type)
    if not log_collector.create_dir(dfx_defs.optical_module_file_info.PATH) then
        return
    end
    -- 判断文件大小并转存
    local file_length = vos.get_file_length(dfx_defs.optical_module_file_info.FILE_PATH)
    if file_length >= dfx_defs.optical_module_file_info.FILE_SIZE then
        rotate()
    end
    -- 写日志文件
    local is_exist = vos.get_file_accessible(dfx_defs.optical_module_file_info.FILE_PATH)
    local fp = file_sec.open_s(dfx_defs.optical_module_file_info.FILE_PATH, "a+")
    if not fp then
        log:error('open log file failed')
        return
    end
    -- 对新创建的文件修改文件权限
    return utils.safe_close_file(fp, function()
        if not is_exist then
            utils_core.chmod_s(
                dfx_defs.optical_module_file_info.FILE_PATH, utils.S_IRUSR | utils.S_IWUSR | utils.S_IRGRP)
        end
        if needs_header(msg_type, dfx_obj, file_length) then
            buffer = string.format("%s%s", create_header_buffer(msg_type, dfx_obj), buffer)
        end
        local ret = fp:write(buffer)
        if not ret then
            log:error('write log file failed')
            return
        end
    end)
end

local function parse_int_optical_module_array(op_array)
    local buffer = string.format('%d', op_array[1])
    for i = 2, #op_array do
        buffer = string.format('%s %d', buffer, op_array[i])
    end
    return buffer
end

local function parse_float_optical_module_array(op_array)
    local buffer = string.format('%f', op_array[1])
    for i = 2, #op_array do
        buffer = string.format('%s %f', buffer, op_array[i])
    end
    return buffer
end

local function parse_optical_module_base_info(dfx_msg, dfx_obj)
    local buffer = string.format('%s,%s,NPU%d,%d,%d,%d', os.date("%Y-%m-%d %H:%M:%S", os.time()),
        'periodic recording', dfx_msg.npu_id, get_optical_module_id(dfx_msg.npu_id),
        dfx_msg.vcc_max, dfx_msg.vcc_min)
    buffer = string.format('%s,%s', buffer, parse_int_optical_module_array(dfx_msg.tx_power_max))
    buffer = string.format('%s,%s', buffer, parse_int_optical_module_array(dfx_msg.tx_power_min))
    buffer = string.format('%s,%s', buffer, parse_int_optical_module_array(dfx_msg.rx_power_max))
    buffer = string.format('%s,%s', buffer, parse_int_optical_module_array(dfx_msg.rx_power_min))
    buffer = string.format('%s,%s', buffer, parse_int_optical_module_array(dfx_msg.tx_bias_max))
    buffer = string.format('%s,%s', buffer, parse_int_optical_module_array(dfx_msg.tx_bias_min))
    buffer = string.format('%s,%d,%d,%d,%d,%d,%d', buffer, dfx_msg.tx_los, dfx_msg.rx_los,
        dfx_msg.tx_lol, dfx_msg.rx_lol, dfx_msg.temperature_max, dfx_msg.temperature_min)
    buffer = string.format('%s,%d,%d,%d', buffer, dfx_msg.odsp_die_temp_max, dfx_msg.odsp_die_temp_min,
        dfx_msg.odsp_high_temp_runtime)
    buffer = string.format('%s,%s', buffer, parse_float_optical_module_array(dfx_msg.host_snr_max))
    buffer = string.format('%s,%s', buffer, parse_float_optical_module_array(dfx_msg.host_snr_min))
    buffer = string.format('%s,%s', buffer, parse_float_optical_module_array(dfx_msg.media_snr_max))
    buffer = string.format('%s,%s\n', buffer, parse_float_optical_module_array(dfx_msg.media_snr_min))
    return buffer
end

local function check_linkdown_log_limiting(dfx_msg, dfx_obj)
    if dfx_obj.linkdown_timer.hour_timer.per_module_log_cnt[dfx_msg.npu_id] == nil then
        dfx_obj.linkdown_timer.hour_timer.per_module_log_cnt[dfx_msg.npu_id] = 0
    end
    -- linkdown每天所有模块日志限流500条，每小时限流100条， 每小时每个模块限流30条
    if dfx_obj.linkdown_timer.day_timer.all_module_log_cnt >= 500 or
        dfx_obj.linkdown_timer.hour_timer.all_module_log_cnt >= 100 or
        dfx_obj.linkdown_timer.hour_timer.per_module_log_cnt[dfx_msg.npu_id] >= 30 then
        return true
    end
    return false
end

local function parse_optical_module_linkdown_info(dfx_msg, dfx_obj)
    if check_linkdown_log_limiting(dfx_msg, dfx_obj) then
        log:debug('optiacl module(%d) is reaches the log traffic limit.', dfx_msg.npu_id)
        return nil
    end
    local buffer = string.format('%s,%s,NPU%d,%d,%s,%d', os.date("%Y-%m-%d %H:%M:%S", os.time()),
        'link down', dfx_msg.npu_id, get_optical_module_id(dfx_msg.npu_id), os.date("%Y-%m-%d %H:%M:%S",
        dfx_msg.timestamp), dfx_msg.vcc_current)
    buffer = string.format('%s,%s', buffer, parse_int_optical_module_array(dfx_msg.tx_power))
    buffer = string.format('%s,%s', buffer, parse_int_optical_module_array(dfx_msg.rx_power))
    buffer = string.format('%s,%s', buffer, parse_int_optical_module_array(dfx_msg.bias))
    buffer = string.format('%s,%d,%d,%d,%d,%d,%d', buffer, dfx_msg.tx_los, dfx_msg.rx_los,
        dfx_msg.tx_lol, dfx_msg.rx_lol, dfx_msg.temperature, dfx_msg.tx_fault)
    buffer = string.format('%s,%s', buffer, parse_float_optical_module_array(dfx_msg.host_snr))
    buffer = string.format('%s,%s\n', buffer, parse_float_optical_module_array(dfx_msg.media_snr))
    dfx_obj.linkdown_timer.day_timer.all_module_log_cnt = dfx_obj.linkdown_timer.day_timer.all_module_log_cnt + 1
    dfx_obj.linkdown_timer.hour_timer.all_module_log_cnt = dfx_obj.linkdown_timer.hour_timer.all_module_log_cnt + 1
    dfx_obj.linkdown_timer.hour_timer.per_module_log_cnt[dfx_msg.npu_id] =
        dfx_obj.linkdown_timer.hour_timer.per_module_log_cnt[dfx_msg.npu_id] + 1
    return buffer
end

local function parse_optical_module_info(dfx_msg, dfx_obj)
    if dfx_msg.msg_type == dfx_defs.optical_module.LINKDOWN then
        return parse_optical_module_linkdown_info(dfx_msg, dfx_obj)
    elseif dfx_msg.msg_type == dfx_defs.optical_module.PERIOD then
        return parse_optical_module_base_info(dfx_msg, dfx_obj)
    end
    return nil
end

function optical_module_dfx:update_timer()
    local timestamp = os.time()
    -- 24h 定时日志计数器清0
    if timestamp - self.linkdown_timer.day_timer.timestamp > 24 * 60 * 60 then
        self.linkdown_timer.day_timer.all_module_log_cnt = 0
        self.linkdown_timer.day_timer.timestamp = timestamp
    end
    -- 1h 定时日志计数器清0
    if timestamp - self.linkdown_timer.hour_timer.timestamp > 60 * 60 then
        self.linkdown_timer.hour_timer.per_module_log_cnt = {}
        self.linkdown_timer.hour_timer.all_module_log_cnt = 0
        self.linkdown_timer.hour_timer.timestamp = timestamp
    end
end

function optical_module_dfx:optical_module_dfx_hander(dfx_msg)
    self:update_timer()
    local buffer = parse_optical_module_info(dfx_msg, self)
    if buffer == nil then
        return
    end
    write_buf_to_file(buffer, self, dfx_msg.msg_type)
end

function optical_module_dfx.new()
    return setmetatable({
        msg_type = DEFAULT_MSG_TYPE,
        linkdown_timer = {
            day_timer = {
                all_module_log_cnt = 0,
                timestamp = 0
            },
            hour_timer = {
                per_module_log_cnt = {},
                all_module_log_cnt = 0,
                timestamp = 0
            }
        }
    }, optical_module_dfx)
end

return optical_module_dfx