-- 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 class = require 'mc.class'
local log = require 'mc.logging'
local mod_service = require 'frudata.service'
local fru_ipmi = require 'fru_ipmi'
local object_manage = require 'mc.mdb.object_manage'
local frudata_service = require 'frudata_service'
local frudata_intf = require 'frudata_intf'
local reboot = require 'mc.mdb.micro_component.reboot'
local fru_mgmt = require 'fru_management'
local sn_replace = require 'sn_replace'
local ipmi_register = require 'ipmi_register'
local intf_debug = require 'mc.mdb.micro_component.debug'
local dump = require 'fru_dump'
local common = require 'common'
local manage = require 'frudata_object.manage'
local base_messages = require 'messages.base'
local class_mgnt = require 'mc.class_mgnt'
local mc_admin = require 'mc.mc_admin'
local mdb_config_manage = require 'mc.mdb.micro_component.config_manage'
local fru_persistence = require 'fru_persistence'
local customize = require 'frudata_customize'
local fru_singleton = require 'fru_singleton'
local client = require 'frudata.client'
local utils_core = require 'utils.core'

local cfrudata = class(mod_service)
function cfrudata:on_delete_object(class_name, object, position)
    local object_name = object:get_object_name()
    log:notice('Delete object, Class: %s, ObjectName: %s', class_name, object_name)
    if class_name == 'Fru' then
        -- 卸载之前 FruId 已经分配的情况下发出信号
        local system_id = common.format_system_id(object.path)
        local fru_mdb_obj = object:get_mdb_object('bmc.kepler.Systems.Fru')
        if fru_mdb_obj.FruDataId and #fru_mdb_obj.FruDataId > 0 then
            log:notice('Fru [Id: %d, Name: %s] emit removed signal start', fru_mdb_obj.FruId, fru_mdb_obj.FruName)
            self:FrusFrusFruRemoved(self.systems[system_id], fru_mdb_obj.FruId, fru_mdb_obj.FruName, object.path)
            log:notice('Fru [Id: %d, Name: %s] emit removed signal end', fru_mdb_obj.FruId, fru_mdb_obj.FruName)
        end

        -- 回收1-63范围内的fruid
        if object['FruId'] >= 1 and object['FruId'] <= common.FRU_ID_MAX_AUTO_ALLOC then
            fru_mgmt.free_fru_id(position, self.db)
        end
    end
    if class_name == 'FruData' then
        if self.non_standard[object.FruId] or fru_singleton.get_instance().is_mcu_fru[object.FruId] then
            fru_persistence.get_instance():delete_db_raw(object.FruId)
            self.non_standard[object.FruId] = nil
            fru_singleton.get_instance().is_mcu_fru[object.FruId] = nil
        end
        if fru_singleton.get_instance().update_status[object.FruId] then
            fru_singleton.get_instance().update_status[object.FruId] = nil
        end
        frudata_intf.delete_fru_info(object.FruId)
    end
end

function cfrudata:add_frudata_object(object, position)
    fru_mgmt.allot_fruid(object, position, self.db)
    log:notice('frudata add object, obj.FruId = %d, frudata name = %s, type = %s', object.FruId,
        object:get_object_name(), object.StorageType)
    self.frudata_service:add_object(self.fruinfo_manage, object)
end

function cfrudata:add_fru_object(object, position)
    fru_mgmt.allot_fruid(object, position, self.db)
    object.GroupPosition = position
    -- 通过对象名获取对象
    local frudata_obj = class_mgnt('FruData')[object.FruDataId]
    if frudata_obj then
        -- 初始化FruData对象的FruName,FruType
        frudata_obj.FruName = object.FruName
        frudata_obj.FruType = tostring(object.Type)
    end

    -- 更新PcbVersion
    fru_mgmt.update_pcbversion(object)

    -- 根据当前接入的 Fru 对象确认更新资源
    local system_id = common.format_system_id(object.path)
    if not self.systems[system_id] then
        self.systems[system_id] = self:CreateFrus(system_id)
        log:notice('register system %d RPC resources by path %s', system_id, object.path)
    end

    -- 接入之后 FruId 已经分配的情况下对于电子标签不为空的 Fru 则发出已添加信号
    local fru_mdb_obj = object:get_mdb_object('bmc.kepler.Systems.Fru')
    log:notice('fru_mdb_obj.FruDataId = %s', fru_mdb_obj.FruDataId)
    if fru_mdb_obj.FruDataId and #fru_mdb_obj.FruDataId > 0 then
        local fru_id = fru_mdb_obj.FruId
        log:notice('Fru [Id: %d, Name: %s] emit added signal start', fru_id, fru_mdb_obj.FruName)
        self:FrusFrusFruAdded(self.systems[system_id], fru_id, fru_mdb_obj.FruName, object.path)
        log:info('Fru [Id: %d, Name: %s] emit added signal end', fru_id, fru_mdb_obj.FruName)

        local uid = fru_singleton.get_instance().fru_uids[fru_id]
        if uid and uid ~= '' then
            fru_mdb_obj.UniqueId = uid
            log:notice('update fru(%d) uid(%s)', fru_id, uid)
        end
    end
end

function cfrudata:on_add_object(class_name, object, position)
    local switch = {
        ['FruData'] = function()
            self:add_frudata_object(object, position)
        end,
        ['Component'] = function()
            self.component[#self.component + 1] = sn_replace.new(self.db, object, object:get_object_name())
            self.component[#self.component]:update_component_serialnumber(self.db, object, object:get_object_name())
        end,
        ['Fru'] = function()
            self:add_fru_object(object, position)
        end,
        ['DftEeprom'] = function()
            self.dft_frudata[#self.dft_frudata + 1] = {
                Id = object.Id,
                Type = object.Type,
                Slot = object.Slot,
                DeviceNum = object.DeviceNum,
                FruData = object.FruData
            }
        end,
        ['DftVersion'] = function()
            self.dft_version[#self.dft_version + 1] = object
        end,
        ['DftEepromWp'] = function()
            self.dft_frudata[#self.dft_frudata + 1] = {
                Id = object.Id,
                Type = object.Type,
                Slot = object.Slot,
                DeviceNum = object.DeviceNum,
                FruData = object.FruData
            }
        end
    }
    if switch[class_name] then
        switch[class_name]()
        local object_name = object:get_object_name()
        log:info('Add object, Class: %s, ObjectName: %s', class_name, object_name)
    end
end

function cfrudata:ctor()
    self.fruinfo_manage = {}
    self.dft_frudata = {}
    -- intf_slot存储对象interface地址和对象的映射
    self.intf_slot = {}
    -- non_standard存储非标电子标签的fruid
    self.non_standard = {}
    self.dft_version = {}
    self.systems = {}
    self.component = {}
    self.frudata_service = frudata_service:new(self.fruinfo_manage, self.non_standard, self.intf_slot)
end

-- 依赖检查
function cfrudata:check_dependencies()
    local admin = mc_admin.new()
    admin:parse_dependency(APP_WORKING_DIRECTORY .. '/mds/service.json')
    admin:check_dependency(self.bus)
end

local function operation(ctx, format)
    local initiator = {}
    if ctx and not ctx:is_empty() then
        initiator = ctx:get_initiator()
    else
        local mc_initiator = require 'mc.initiator'
        initiator = mc_initiator.new('N/A', 'N/A', 'localhost')
    end
    log:operation(initiator, 'frudata', format)
end

local function SetProductAssetTag(obj, ctx, fruid, assettag)
    -- 3 5 为AssetTag的区域
    if not common.validate_byte(3, 5, assettag) then
        if ctx and not ctx:is_empty() then
            local format = string.format('Write fru%d E-label data failed', fruid)
            log:operation(ctx:get_initiator(), 'frudata', format)
        end
        log:error('Input %s invalid', assettag)
        return
    end
    -- 3 5 为AssetTag的区域
    frudata_intf.ipmi_write_elabel(fruid, 3, 5, 0, #assettag, assettag)
    local format = string.format('Write fru%d E-label data(%s) successfully', fruid, assettag)
    operation(ctx, format)

    fru_ipmi.updata_file_id(fruid)
    format = string.format('Update fru%d E-label data successfully', fruid)
    operation(ctx, format)
end

-- 循环GC避免内存占用过高
local function start_gc_task()
    skynet.fork_loop({count = 0}, function()
        skynet.sleep(60000) -- 10分钟后开始循环GC
        log:notice('start collect garbage')
        while true do
            collectgarbage('collect')
            utils_core.malloc_trim(0)
            skynet.sleep(6000) -- 每60sGC一次
        end
    end)
end

function cfrudata:init()
    log:notice('===== frudata service init =======')
    cfrudata.super.init(self)
    cfrudata:check_dependencies()

    mod_service:CreateComponents(1)

    self:register_rpc_methods()

    -- 初始化app复位接口
    self:init_reboot()

    self:register_signal()
    fru_persistence.new(self.db)
    fru_singleton.new()

    intf_debug.on_dump(dump.log_dump_cb)
    -- 注册对象响应回调函数
    -- 添加对象回调
    object_manage.on_add_object(self.bus,
        function(class_name, object, position)
            self:on_add_object(class_name, object, position)
        end)
    -- 删除对象回调
    object_manage.on_delete_object(self.bus, function(class_name, object, position)
        self:on_delete_object(class_name, object, position)
    end)

    skynet.fork(function()
        self.ipmi_mgr = ipmi_register.new(self.non_standard)

        self.ipmi_mgr:register_ipmi(function(...)
            self:register_ipmi_cmd(...)
        end)
        local ok, err = pcall(fru_mgmt.init_fru_id, self.db)
        if not ok then
            log:error('fru_id management init failed, err: %s', err)
        end

        client:OnNodeLocationInterfacesAdded(function(sender, path, properties)
            return fru_singleton.get_instance():init_slot_id(sender, path, properties)
        end)
    
        client:OnNodeLocationPropertiesChanged(function(properties)
            return fru_singleton.get_instance():set_slot_id(properties)
        end)
    end)

    -- 出厂配置备份/恢复
    mdb_config_manage.on_backup(function(ctx)
        log:operation(ctx:get_initiator(), 'frudata', 'Set manufacturer default configuration successfully')
        return {}
    end)

    mdb_config_manage.on_recover(function(ctx)
        log:operation(ctx:get_initiator(), 'frudata',
            'Recover manufacturer default configuration and reset BMC successfully')
    end)

    -- 导入导出注册
    mdb_config_manage.on_import(function(...)
        return customize.import(...)
    end)
    mdb_config_manage.on_export(function(...)
        return customize.export(...)
    end)
    mdb_config_manage.on_verify(function(...)
        return customize.verify_config(...)
    end)

    start_gc_task()
end

local function write_fru_data(self, fru_id, data)
    log:notice('write_fru_data_sig: Fru_id = %d.', fru_id)
    skynet.fork(function()
        local ok, ret = pcall(function(...)
            self.frudata_service:set_fru_info_by_id(fru_id, data)
        end)
        if not ok then
            log:error('write fru(%s) data failed, ret: %s', fru_id, ret)
        end
    end)
end

local function write_system_data(self, fru_id)
    log:notice('write_system_data_sig: Fru_id = %d.', fru_id)
    skynet.fork(function()
        local ok, ret = pcall(function(...)
            self.frudata_service:set_system_info_by_id(fru_id)
        end)
        if not ok then
            log:error('write fru(%s) system data failed, ret: %s', fru_id, ret)
        end
    end)
end

local function clear_elabel_data(self, fru_id, data)
    log:notice('clear_elabel_data_sig: Fru_id = %d.', fru_id)
    skynet.fork(function()
        local ok, ret = pcall(function(...)
            self.frudata_service:set_fru_info_by_id(fru_id, data)
            skynet.sleep(5) -- 两次I2C操作间隔50ms
            self.frudata_service:set_system_info_by_id(fru_id)
        end)
        if not ok then
            log:error('clear fru(%s) elabel data failed, ret: %s', fru_id, ret)
        end
    end)
end

local function update_elabel_data(self, fru_id)
    log:info('update_elabel_sig: Fru_id = %d.', fru_id)
    self.frudata_service:update_fru_data_by_id(fru_id)
    self.frudata_service:update_sys_data_by_id(fru_id)
end

function cfrudata:register_signal()
    fru_ipmi.write_fru_data_sig:on(function(fru_id, data)
        write_fru_data(self, fru_id, data)
    end)
    fru_ipmi.write_system_data_sig:on(function(fru_id)
        write_system_data(self, fru_id)
    end)
    fru_ipmi.clear_elabel_data_sig:on(function(fru_id, data)
        clear_elabel_data(self, fru_id, data)
    end)
    frudata_service.update_elabel_sig:on(function(fru_id)
        update_elabel_data(self, fru_id)
    end)
end

function cfrudata:Update(obj, intf_slot, ctx, property_name, value)
    local frudata_obj = intf_slot[obj]
    if not frudata_obj then
        log:error('Get frudata object failed')
        error(base_messages.InternalError())
    end
    local fruid = frudata_obj.FruId

    manage:update_non_standard_fru(fruid, property_name, value, frudata_obj)
    fru_persistence.get_instance():add(fruid, property_name, value)
    log:notice('Update frudata object successfully, fruId = %d', fruid)
end

local function UpdateComponentHealth(obj, ctx, value)
    local health_table = {
        [0] = 'Normal',
        [1] = 'Minor',
        [2] = 'Major',
        [3] = 'Critical'
    }
    if health_table[value] then
        obj.Health = value
    end
end

local function SetSysProductName(obj, ctx, fruid, product_name)
    if fruid ~= 0 then
        -- 只有fruid为0的电子标签有system域
        if ctx and not ctx:is_empty() then
            local format = string.format('Write fru%d E-label data failed', fruid)
            log:operation(ctx:get_initiator(), 'frudata', format)
        end
        log:error('Write fru%d E-label data failed', fruid)
        return
    end
    -- 6 1 为ProductName的区域
    if not common.validate_byte(6, 1, product_name) then
        if ctx and not ctx:is_empty() then
            local format = string.format('Write fru%d E-label data failed', fruid)
            log:operation(ctx:get_initiator(), 'frudata', format)
        end
        log:error('Input %s invalid', product_name)
        return
    end
    -- 6 1 为ProductName的区域
    frudata_intf.ipmi_write_elabel(fruid, 6, 1, 0, #product_name, product_name)
    fru_ipmi.updata_system(fruid)
end

local function GetComponentTypes(obj, ctx)
    local objs = class_mgnt('Component'):get_all()
    local res = {}
    for _, com_obj in pairs(objs) do
        if not res[com_obj['Type']] and common.type_table[com_obj['Type']] then
            res[com_obj['Type']] = common.type_table[com_obj['Type']]
        end
    end
    local ret = {}
    for key, value in pairs(res) do
        log:info('ComponentTypes: get type(%s), name(%s)', key, value)
        ret[#ret + 1] = { ComponentType = key, ComponentName = value }
    end

    return ret
end

function cfrudata:register_rpc_methods()
    self:ImplFruDataFrudataUpdate(function(obj, ...)
        return self:Update(obj, self.intf_slot, ...)
    end)
    self:ImplComponentComponentUpdateHealth(function(obj, ...)
        return UpdateComponentHealth(obj, ...)
    end)
    self:ImplFruDataFrudataSetSysProductName(function(obj, ...)
        return SetSysProductName(obj, ...)
    end)
    self:ImplFruDataFrudataSetProductAssetTag(function(obj, ...)
        return SetProductAssetTag(obj, ...)
    end)
    self:ImplComponentsComponentsGetComponentTypes(function(obj, ...)
        return GetComponentTypes(obj, ...)
    end)

    -- 初始化默认的 Frus 路径，保存对应的对象实体
    local default_system_id = 1
    if not self.systems[default_system_id] then
        self.systems[default_system_id] = self:CreateFrus(default_system_id)
        log:notice('register default system %d RPC resources.', default_system_id)
    end
end

function cfrudata:init_reboot()
    reboot.on_prepare(function(...)
        return self:on_reboot_prepare(...)
    end)
    reboot.on_action(function(...)
        return self:on_reboot_action(...)
    end)
    reboot.on_cancel(function(...)
        self:on_reboot_cancel()
    end)
end

function cfrudata:on_reboot_prepare()
    log:notice('frudata has no extra preparation for reboot.')
    return 0
end

function cfrudata:on_reboot_action()
    log:notice('frudata has no extra action for reboot.')
    return 0
end

function cfrudata:on_reboot_cancel()
    log:notice('frudata has no extra cancel for reboot.')
end

return cfrudata
