-- Copyright (c) 2026 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: 部件插入拔出日志记录
local class = require 'mc.class'
local skynet = require 'skynet'
local singleton = require 'mc.singleton'
local log = require 'mc.logging'
local initiator = require 'mc.initiator'
local skynet_queue = require 'skynet.queue'

local PCIECARD_INTF <const> = 'bmc.kepler.Systems.PCIeDevices.PCIeCard'
local card_record_tbl = {
    ['PCIeCard'] = 'PCIe',
    ['OCPCard'] = 'OCP'
}

-- 用于标识load_info从持久化数据库获取时，卡加载的不同阶段，避免在BMC重启场景下重复记录日志
local SKIP_STATUS <const> = {
    PREPARE = 0,    -- 标记从持久化数据库获取到卡的load_info
    PROCESS = 1,    -- 检测到Connector在位变化，实际发生了卡加载
    CANCEL = 2,     -- 未发生实际的卡加载，清除PREPARE标记
    COMPLETED = 3   -- 跳过日志记录完成
}

local plug_record_mgmt = class()

function plug_record_mgmt:ctor()
    self.card_skip_list = {}
    self.queue = skynet_queue()
end

function plug_record_mgmt:skip_card_record_prepare(type, slot)
    if not card_record_tbl[type] then
        return
    end

    if not self.card_skip_list[type] then
        self.card_skip_list[type] = {}
    end
    self.card_skip_list[type][slot] = SKIP_STATUS.PREPARE
    log:notice('skip_card_record_prepare return, type is %s, slot is %s', type, slot)
end

function plug_record_mgmt:skip_card_record_process(type, slot)
    if not card_record_tbl[type] then
        return
    end

    if not self.card_skip_list[type] or self.card_skip_list[type][slot] ~= SKIP_STATUS.PREPARE then
        log:info('skip_card_record_process failed, card not in prepare status, type is %s, slot is %s', type, slot)
        return
    end
    self.card_skip_list[type][slot] = SKIP_STATUS.PROCESS
    log:notice('skip_card_record_process return, type is %s, slot is %s', type, slot)
end

-- 未通过持久化数据库加载卡时，清除跳过插入日志记录标志
function plug_record_mgmt:skip_card_record_cancel(type, slot)
    if not card_record_tbl[type] then
        return
    end

    if not self.card_skip_list[type] or self.card_skip_list[type][slot] ~= SKIP_STATUS.PREPARE then
        log:info('no need to skip_card_record_cancel')
        return
    end
    self.card_skip_list[type][slot] = SKIP_STATUS.CANCEL
    log:notice('skip_card_record_cancel return, type is %s, slot is %s', type, slot)
end

local function card_plug_log_operation_log(info, card_type, op_type)
    if op_type ~= 'add' and op_type ~= 'delete' then
        log:error('card_plug_log_operation_log failed, op_type (%s) invalid', op_type)
        return
    end

    local description = string.format('vid: 0x%04x, did: 0x%04x, sub_vid: 0x%04x, sub_did: 0x%04x', info.vid,
        info.did, info.sub_vid, info.sub_did)

    if info.sn ~= '' and info.sn ~= 'N/A' then
        description = description .. string.format(', SN: %s', info.sn)
    elseif info.pn  ~= '' and info.pn  ~= 'N/A' then
        description = description .. string.format(', PN: %s', info.pn)
    end

    log:operation(initiator.new('N/A', 'N/A', '127.0.0.1'), 'pcie_device', 'The %s card%d(%s) %s successfully',
        card_type, info.slot_id, description, op_type == 'add' and 'plug in' or 'plug out')
end

function plug_record_mgmt:card_plug_in_record_task(obj, type)
    local slot = obj[PCIECARD_INTF].SlotID
    if self.card_skip_list[type] and self.card_skip_list[type][slot] == SKIP_STATUS.PROCESS then
        log:notice('card plug in record failed, %s (%s) in card_skip_list', type, slot)
        self.card_skip_list[type][slot] = SKIP_STATUS.COMPLETED
        return
    end

    local obj_info = {
        slot_id = obj[PCIECARD_INTF].SlotID,
        vid = obj[PCIECARD_INTF].VendorID,
        did = obj[PCIECARD_INTF].DeviceID,
        sub_did = obj[PCIECARD_INTF].SubDeviceID,
        sub_vid = obj[PCIECARD_INTF].SubVendorID,
        sn = obj[PCIECARD_INTF].SerialNumber,
        pn = obj[PCIECARD_INTF].PartNumber
    }
    card_plug_log_operation_log(obj_info, card_record_tbl[type], 'add')
end

function plug_record_mgmt:card_plug_in_record(obj, position, type)
    if not obj then
        log:error('card_plug_in_record failed, %s (%s) obj is nil.', type, position)
        return
    end

    -- 监听slot_id属性，slot_id同步完成才记录日志
    local has_recorded = false
    obj[PCIECARD_INTF].property_changed:on(function(name, value)
        if has_recorded or name ~= 'SlotID' then
            return
        end

        if value ~= 0 then
            has_recorded = true
            self:card_plug_in_record_task(obj, type)
        end
    end)

    if not has_recorded and obj[PCIECARD_INTF].SlotID ~= 0 then
        has_recorded = true
        self:card_plug_in_record_task(obj, type)
    end
end

function plug_record_mgmt:plug_in_record(class_name, mds_obj, position)
    if card_record_tbl[class_name] then
        skynet.fork_once(function ()
            log:notice('%s (%s) plug_in_record start', class_name, position)
            self.queue(function(obj, pos, type)
                self:card_plug_in_record(obj, pos, type)
            end, mds_obj, position, class_name)
        end)
    end
end

function plug_record_mgmt:card_plug_out_record(obj_info, position, type)
    if not obj_info then
        log:error('card plug out record failed, %s (%s) obj is nil.', type, position)
        return
    end

    -- slot_id 未同步完成时不记录 
    if obj_info.slot_id == 0 then 
        log:notice('card plug out record failed, slot_id not ready')
        return
    end

    card_plug_log_operation_log(obj_info, card_record_tbl[type], 'delete')
 end

function plug_record_mgmt:plug_out_record(class_name, mds_obj, position)
    if card_record_tbl[class_name] then
        log:notice('%s (%s) plug_out_record start', class_name, position)
        local obj_info = {
            slot_id = mds_obj[PCIECARD_INTF].SlotID,
            vid = mds_obj[PCIECARD_INTF].VendorID,
            did = mds_obj[PCIECARD_INTF].DeviceID,
            sub_did = mds_obj[PCIECARD_INTF].SubDeviceID,
            sub_vid = mds_obj[PCIECARD_INTF].SubVendorID,
            sn = mds_obj[PCIECARD_INTF].SerialNumber,
            pn = mds_obj[PCIECARD_INTF].PartNumber
        }
        skynet.fork_once(function ()
            self.queue(function(info, pos, type)
                self:card_plug_out_record(info, pos, type)
            end, obj_info, position, class_name)
        end)
    end
end

return singleton(plug_record_mgmt)