-- 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 skynet = require 'skynet'
local log = require 'mc.logging'
local c_psu_object = require 'device.psu'
local c_psu_slot_object = require 'device.psu_slot'
local utils = require 'mc.utils'
local vos = require 'utils.vos'
local mdb = require 'mc.mdb'
local log_def = require 'macros.log_def'
local worker = require 'worker.core'
local file_sec = require 'utils.file'
local add_event = require 'add_event'
local task = require 'task_service'
local sys_info = require 'libsoc_adapter.sys_info'
local utils_core = require 'utils.core'
local client = require 'power_mgmt.client'
local mc_context = require 'mc.context'
local context = mc_context.new()
local psu_service = require 'psu_service'

local TEST_MODE <const> = {
    NONE = 0,
    UT = 1,
    IT = 2
}
 
local g_test_mode = TEST_MODE.NONE

local g_dump_task = {}

local log_service = {}
log_service.__index = log_service

local MAX_TASK_NUM <const> = 10
local function table_len(tb)
    local tb_len = 0
    for _, _ in pairs(tb) do
        tb_len = tb_len + 1
    end
    return tb_len;
end

local ONE_YEAR_SECOND <const> = 31536000
local INPUT_MODE <const> = {
    [0] = "DC",
    [1] = "AC",
    [2] = "AC/DC",
    [255] = "N/A"
}

local function get_bmc_time()
    local date_time
    client:ForeachTimeObjects(function (obj)
        date_time = obj['DateTime']
    end)
    return date_time
end

local function power_type_to_input_mode(power_type)
    return INPUT_MODE[power_type] or "N/A"
end

local function collect_black_box_data(bus, f_log, psu_id)
    local bmc_time = get_bmc_time()
    if not bmc_time then
        return
    end

    local black_box_info = psu_service.get_instance():psm_get_black_box_info(psu_id)
    local str_tmp
    local str_len
    for dev_id, value in pairs(black_box_info) do
        local temp_buf = table.concat({
            string.format('PSU%d black box info:\r\n', dev_id),
            string.format('BMC Time:%s    Power SN:%s\r\n', bmc_time, value.serial_number)
        })
        for i = 1, value.black_box_max_length // log_def.BLACK_BOX_LINE_LEN do
            str_tmp = value.black_box_data:sub(
                (i - 1) * log_def.BLACK_BOX_LINE_LEN + 1, i * log_def.BLACK_BOX_LINE_LEN)
            str_len = #str_tmp
            temp_buf = table.concat({
                temp_buf,
                string.format(
                '0x%03x: ' .. string.rep('0x%02x ', str_len), i - 1, string.unpack(string.rep('B', str_len), str_tmp)),
                string.rep('0x00 ', log_def.BLACK_BOX_LINE_LEN - str_len),
                '\r\n'
            })
        end
        f_log:write(temp_buf)
        skynet.sleep(500)
    end
end

function log_service:dump_objs_info(path)
    local obj_info_path = path .. "/current_psu_black_box_info.txt"
    local fp_w = file_sec.open_s(obj_info_path, 'w+')
    if not fp_w then
        log:error('open file failed')
        return false
    end
    return utils.safe_close_file(fp_w, function()
        local ok, ret = pcall(function()
            psu_service.get_instance():dump_obj_infos(fp_w)
            collect_black_box_data(self.bus, fp_w)
        end)
        return ok, ret
    end)
end

local function format_psu_info_header()
    local header = string.format("%-5s  |  %-8s  |  %-15s  |  %-32s  |  %-32s  |  %-25s  |  %-12s  |  %-8s  |" ..
        "  %-16s  |  %-16s  |  %-8s  |  %-8s\r\n",
        "Slot", "presence", "Manufacturer", "Type", "SN", "Version", "Rated Power", "InputMode",
        "PartNum", "DeviceName", "Vin", "Vout")
    return header
end

local function format_psu_info_body()
    local format_string = "%-5s  |  %-8s  |  %-15s  |  %-32s  |  %-32s  |  %-25s  |  %-12s  |  %-8s  |" ..
        "  %-16s  |  %-16s  |  %-8.2f  |  %-8.2f\r\n"
    local body_table = {}
    local ok, str
    c_psu_object.collection:fold(function (_, psu)
        ok, str = pcall(function ()
            return string.format( format_string, psu.SlotNumber, "present",
            psu.Manufacturer, psu.Model,psu.SerialNumber, psu.FirmwareVersion, psu.Rate,
            power_type_to_input_mode(psu.PowerSupplyType), psu.PartNumber,
            psu.DeviceLocator, psu.InputVoltage, psu.OutputVoltage)
        end)
        if not ok then
            log:error("dump psu info error is %s", str)
            str = ""
        end
        table.insert(body_table, str)
    end)
    return table.concat(body_table)
end

function log_service:dump_psu_info(path)
    local psu_info_path = path .. "/psu_info.txt"
    local fp_w = file_sec.open_s(psu_info_path, 'w+')
    if not fp_w then
        log:error('open file failed')
        return false
    end
    utils.safe_close_file(fp_w, function()
        local psu_header = format_psu_info_header()
        fp_w:write(psu_header)
        local psu_body = format_psu_info_body()
        fp_w:write(psu_body)
    end)
    return true
end

function log_service:dump_log(ctx, path)
    local ok = self:dump_objs_info(path)
    if not ok then
        log:error('dump current objs info failed')
    end
    ok = self:dump_psu_info(path)
    if not ok then
        log:error('dump psu info failed')
    end
    log:notice('collect power_mgmt log finished')
end

local function creat_dump_task(bus, psu_id)
    if table_len(g_dump_task) > MAX_TASK_NUM then
        error(string.format('collet ps%d failed, create task failed, exceed %d tasks', psu_id, MAX_TASK_NUM))
    end
    local path = psu_service.get_instance():get_psu_property(psu_id, 'path')
    local task_id, msg = task.create(bus, 'dump_psu_blackbox', path)
    if not task_id then
        error(string.format('collet ps%d failed, create task failed, msg: %s', psu_id, msg))
    end
    g_dump_task[task_id] = psu_id
    return task_id
end

local function destroy_dump_task(task_id)
    task.destroy_task_by_id(task_id)
    g_dump_task[task_id] = nil
end

local function write_ps_black_box_log(bus, psu_id)
    local bmc_time = get_bmc_time()
    if not bmc_time then
        return
    end
    local black_box_info = psu_service.get_instance():psm_get_black_box_info(psu_id)
    for dev_id, value in pairs(black_box_info) do
        log:ps_black_box_print('PSU%d black box info:', dev_id)
        log:ps_black_box_print('BMC Time:%s    Power SN:%s', bmc_time, value.serial_number)
        for i = 1, value.black_box_max_length // log_def.BLACK_BOX_LINE_LEN do
            local str_tmp = value.black_box_data:sub(
                (i - 1) * log_def.BLACK_BOX_LINE_LEN + 1, i * log_def.BLACK_BOX_LINE_LEN)
            local str_len = #str_tmp
            log:ps_black_box_print(string.format('0x%03x: ' .. string.rep('0x%02x ', str_len), i - 1,
                string.unpack(string.rep('B', str_len), str_tmp)) ..
                string.rep('0x00 ', log_def.BLACK_BOX_LINE_LEN - str_len))
        end
    end
end

local function write_black_box_log(bus)
    local collect_black_box_flag = 0
    c_psu_slot_object.collection:fold(function (_, obj)
        collect_black_box_flag = collect_black_box_flag | ((1 << obj['SlotNumber']) * obj['Presence'])
    end)

    skynet.fork(function ()
        local loop_cnt = 30
        while collect_black_box_flag > 0 and loop_cnt > 0 do
            c_psu_object.collection:fold(function (_, obj)
                -- 需要收集黑匣子日志的电源
                if collect_black_box_flag & (1 << obj['SlotNumber']) > 0 then
                    pcall(write_ps_black_box_log, bus, obj['SlotNumber'])
                    collect_black_box_flag = collect_black_box_flag & (~(1 << obj['SlotNumber']))
                end
            end)
            loop_cnt = loop_cnt - 1
            skynet.sleep(500)
        end
    end)
end

function log_service:dump_psu_blackbox(psu_id)
    local task_id = creat_dump_task(self.bus, psu_id)
    skynet.fork_once(function ()
        write_ps_black_box_log(self.bus, psu_id)
         -- 5s后销毁任务
        skynet.timeout(500, function()
            destroy_dump_task(task_id)
        end)
    end)
    return task_id
end


local function cal_ac_timestamps(aclost_record)
    local history_time = aclost_record.CurTime
    local record_time = os.time()
    -- 上次记录的存活时间到现在时间超过10min，记录历史时间之后的5分钟为掉电时间，否则认为当前时间的三分钟前为掉电时间
    if history_time > ONE_YEAR_SECOND and record_time - history_time > 600 then
        record_time = history_time + 300
        log:notice("last record time is before 10 mins")
    else
        record_time = record_time - 180
        log:notice("last record time is less 10 mins")
    end
    local ac_time = os.date("%Y-%m-%d %H:%M:%S UTC", record_time)
    log:notice("record real ac time is %s", ac_time)
    return ac_time
end

local function aclost_int_task(aclost_record)
    skynet.register_protocol {
        name = "gpio",
        id = 98,
        pack = skynet.pack,
        unpack = skynet.unpack
    }
    skynet.dispatch('gpio', function(_, _)
        if g_test_mode ~= TEST_MODE.NONE then
            return
        end
        log:notice('Get AC lost signal')
        aclost_record.CurTime = os.time()
        aclost_record.Flag = log_def.PS_POWER_DROP_FLAG
        client:ForeachFruCtrlObjects(function (obj)
            pcall(obj.SetACLost_PACKED, obj, context, 1)
        end)
        log:notice('Process AC lost complete')
    end)

    skynet.register_protocol {
        name = "test_mode",
        id = 99,
        pack = skynet.pack,
        unpack = skynet.unpack
    }
    skynet.dispatch('test_mode', function(a, test_mode)
        g_test_mode = test_mode
    end)
end

local function aclost_check_task(bus, aclost_record)
    local collect_time = 0
    local aclost_flag = false
    while true do
        -- 每2秒钟轮询一次持久化文件，如果有ACLOST，则更新属性值并复位持久化文件的标志
        skynet.sleep(200)
        collect_time = collect_time + 1
        if aclost_record.Flag == log_def.PS_POWER_DROP_FLAG then
            log:notice('detect drop flag cur_time: %s', os.date("%c", aclost_record.CurTime))
            aclost_flag = true
            aclost_record.Flag = 0
            aclost_record:save()
            skynet.fork(function ()
                -- event准备需要较长时间
                skynet.sleep(10000)
                local time_stamp = cal_ac_timestamps(aclost_record)
                add_event.get_instance().generate_power_failure('true', time_stamp)
                -- 告警产生2s之后，由组件进行消除
                skynet.sleep(200)
                add_event.get_instance().generate_power_failure('false', time_stamp)
                local desc = 'Server power failure occurred at %s, The power has been already restored.'
                local rec = string.format(desc, time_stamp)
                log:maintenance(log.MLOG_WARN, log.FC__PUBLIC_OK, rec)
            end)
        end
        -- 因为电源ACLOST没有进行防抖，保证至少10秒才能进行一次黑匣子收集, 掉电标识只要在10s内获取到就行
        if collect_time >= 5 and aclost_flag then
            collect_time = 0
            local ok = pcall(write_black_box_log, bus)
            aclost_flag = not ok -- 可能psu protocol未注册好，新增重试
        end
    end
end

function log_service.new(bus, db)
    local gpio_work = worker.new(0)
    return setmetatable({ bus = bus, db = db, gpio_work = gpio_work }, log_service)
end

function log_service:__gc()
    self.gpio_work:stop()
    self.gpio_work:join()
end

-- 从寄存器获取BMC复位原因
function log_service:sync_reset_type()
    local sys = sys_info.new()
    local ok, ret = pcall(sys.get_reset_type, sys)
    if not ok then
        log:error('get reset type failed, err: %s', tostring(ret))
        return
    end
    return ret
end

function log_service:get_ac_lost_flag()
    local reset_cause = self:sync_reset_type()
    self.aclost_record.Flag = 0
    if reset_cause ~= 0 then
        return
    end

    if self.aclost_record.ResetFlag ~= 0 then
        return
    end
    self.aclost_record.Flag = log_def.PS_POWER_DROP_FLAG
end

function log_service:record_timestamps_period(ac_table)
    while true do
        self.aclost_record.CurTime = os.time()
        self.aclost_record:save()
        -- 每隔10 min记录一次当前系统时间
        skynet.sleep(600 * 100)
    end
end

function log_service:init()
    -- 获取db数据
    self.aclost_record = self.db.AclostRecordTable({Id = 'AclostRecord'})
    self:get_ac_lost_flag()
    if self.aclost_record then
        aclost_int_task(self.aclost_record)
        self.gpio_work:start_module("gpio_drived")
        skynet.fork(
            function()
                skynet.sleep(10000)
                aclost_check_task(self.bus, self.aclost_record)
            end
        )
    end
    self.aclost_record.ResetFlag = 1
    self.aclost_record:save()
    skynet.fork(
        function ()
            -- 服务初始化之后的100s 之后才开始记录时间，防止记录初始时间为未同步rtc的时间1970
            skynet.sleep(10000)
            self:record_timestamps_period()
        end
    )
end

return log_service
