-- 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 class = require 'mc.class'
local log = require 'mc.logging'
local org_freedesktop_dbus = require 'sd_bus.org_freedesktop_dbus'
local c_tasks = require 'mc.orm.tasks'
local signal = require 'mc.signal'
local object_path = require 'dbus.object_path'
local GVariant = require 'mc.gvariant'
local json = require 'cjson'
local skynet_queue = require 'skynet.queue'
local mdb_service = require 'mc.mdb.mdb_service'
local client = require 'storage.client'

local match_rule = org_freedesktop_dbus.MatchRule
local interfaces_added = org_freedesktop_dbus.ObjMgrInterfacesAdded
local interfaces_removed = org_freedesktop_dbus.ObjMgrInterfacesRemoved
local unescape = object_path.unescape
local escape = object_path.escape
local is_variant = GVariant.is_variant
local variant_type = GVariant.type
local json_null = json.null

local SMS_INTERFACE<const> = 'bmc.kepler.sms'
local SMS_REDFISH_INTERFACE<const> = 'bmc.kepler.sms.redfish'
local HOST_AGENT_APP<const> = 'bmc.kepler.host_agent'
local STORAGE_PATH<const> = '/bmc/kepler/Systems/1/Sms/1/ComputerSystem/Systems/1/Storage'
local ODATA_ID<const> = '@odata.id'
local BMA_STATUS_PATH<const> = '/bmc/kepler/Systems/1/Sms'
local BMA_STATUS_INTERFACE<const> = 'bmc.kepler.Systems.Sms.SmsStatus'

local c_bma_mdb_mgmt = class(nil, nil, true)

function c_bma_mdb_mgmt:ctor(bma)
    self.bus = bma.bus
    self.bma = bma
    self.on_add = signal.new()
    self.on_update = signal.new()
    self.on_delete = signal.new()
    self.on_reset = signal.new()
    self.queue = skynet_queue()
    self.tasks = c_tasks.new()
    self.reload_queue = {}
end

function c_bma_mdb_mgmt:init()
    self.tasks:timeout_ms(1000, function()
        self:start()
    end)
end

function c_bma_mdb_mgmt:start()
    if not self.bus then
        return
    end
    log:notice('Start load bma object.')
    self:monitor_bma()
    self:start_load_all_bma_resource_task()
end

function c_bma_mdb_mgmt:start_load_all_bma_resource_task()
    -- 启动 BMA 资源加载任务
    local ok, state
    local task = self.tasks:new_task('load bma mdb object task'):loop(function(task)
        -- 检查bma状态是否为0，如果是1则代表bma不通，即使有资源也不获取
        ok, state = pcall(function()
            return self.bus:call(HOST_AGENT_APP, BMA_STATUS_PATH,
                'org.freedesktop.DBus.Properties', 'Get', 'ss', BMA_STATUS_INTERFACE, 'State')
        end)
        if not ok or not state then
            return
        end
        if state:value() ~= 0 then
            self.tasks:sleep_ms(1000)
            return
        end

        self:load_all_bma_resource(STORAGE_PATH)

        -- 加载完成后任务退出，后续资源变化会有更新通知
        task:stop()
    end)

    -- 加载 BMA 资源任务退出后，开始监听是否需要动态刷新数据的信号
    task.on_task_exit:on(function()
        log:info('c_bma_mdb_mgmt: monitor reload bma resource signal')
        self.bma.on_need_resource:on(function(path)
            self:add_reload_path(path)
        end)
    end)
end

local MAX_FAILED_COUNT<const> = 3
function c_bma_mdb_mgmt:load_all_bma_resource(start_path, max_level)
    local path_queue = {start_path}
    local failed_count = 0
    local level = 0

    while #path_queue > 0 and (not max_level or level < max_level) do
        local path = path_queue[#path_queue]
        local err, objects = self.bus:pcall(HOST_AGENT_APP, path,
            'org.freedesktop.DBus.ObjectManager', 'GetManagedObjects', '')
        if not err then
            path_queue[#path_queue] = nil
            failed_count = 0
            for sub_path, object in pairs(objects) do
                path_queue[#path_queue + 1] = sub_path
                self:load_bma_resource(sub_path, object)
            end
            level = level + 1
        elseif err.name == 'org.freedesktop.DBus.Error.ServiceUnknown' then
            log:error('c_bma_mdb_mgmt: err=%s, exit', err)
            return
        elseif failed_count < MAX_FAILED_COUNT then
            failed_count = failed_count + 1
            log:error('c_bma_mdb_mgmt: load bma mdb objects failed, path=%s, err=%s, retry', path,
                err)
            self.tasks:sleep_ms(1000)
        else
            path_queue[#path_queue] = nil
            log:error('c_bma_mdb_mgmt: load bma mdb objects failed, path=%s, err=%s, skip', path,
                err)
        end
    end
end

function c_bma_mdb_mgmt:add_reload_path(path)
    if self.reload_queue[path] then
        return
    end

    self.reload_queue[path] = true
    self.reload_queue[#self.reload_queue + 1] = path
    self:start_reload_task()
end

function c_bma_mdb_mgmt:start_reload_task()
    if self.reload_task then
        return
    end

    local queue = self.reload_queue
    local path
    self.reload_task = self.tasks:new_task('reload bma resource'):loop(function()
        while #queue > 0 do
            -- 排序，顶层资源优先加载
            table.sort(queue)
            path = queue[1]
            self:reload_bma_resource(escape(path))
            table.remove(queue, 1)
            queue[path] = nil
        end

        self.reload_task:stop()
        self.reload_task = nil
    end)
end

function c_bma_mdb_mgmt:reload_bma_resource(es_path)
    log:info('c_bma_mdb_mgmt: reload bma resource, %s', es_path)

    local ok, err = pcall(function()
        -- 优先使用 host_agent 资源树对象的 to_json 方法
        local err, data =
            self.bus:pcall(HOST_AGENT_APP, es_path, SMS_INTERFACE, 'to_json', 'as', {})
        if not err then
            self:add_resource(es_path, json.decode(data))
            return
        end

        if err.name ~= 'org.freedesktop.DBus.Error.UnknownMethod' then
            error(err)
        end

        -- 如果 host_agent 不支持 to_json 方法则使用 mdb_mgmt 获取
        data = self:get_bma_resource_from_mdb_mgmt(es_path)
        if data then
            self:load_bma_resource(es_path, data)
            return
        end

        error('get resource failed')
    end)

    if not ok then
        log:info('c_bma_mdb_mgmt: reload bma resource failed, path=%s, %s', es_path, err)
    end
end

local function is_json_array(val)
    local v_type = variant_type(val)
    return #v_type > 1 and v_type == 'av'
end

local function get_value(val)
    if type(val) == 'userdata' and is_variant(val) then
        if not is_json_array(val) then
            return get_value(val:value())
        end

        local res = get_value(val:value())
        for k, v in ipairs(res) do
            res[k] = json.decode(v:value())
        end
        return res
    elseif val == 'null' then
        return json_null
    else
        return val
    end
end

local function load_data(result, iface, props)
    local data = result
    if iface ~= SMS_INTERFACE and iface ~= SMS_REDFISH_INTERFACE then
        local sub = iface:sub(#SMS_REDFISH_INTERFACE + 2)
        if not sub then
            return
        end

        for name in sub:gmatch('([^.]+)') do
            if data[name] == nil then
                data[name] = {}
            end
            data = data[name]
        end
    end

    for name, val in pairs(props) do
        local un_es_name = unescape(name)
        local value = get_value(val)
        if un_es_name == ODATA_ID then
            data[un_es_name] = unescape(value)
        else
            data[un_es_name] = value
        end
    end
end

function c_bma_mdb_mgmt:load_bma_resource(path, path_interfaces)
    local data = {}
    local iface_count = 0
    for iface, props in pairs(path_interfaces) do
        load_data(data, iface, props)
        iface_count = iface_count + 1
    end

    if iface_count > 0 then
        self:add_resource(path, data)
    end
end

function c_bma_mdb_mgmt:update_bma_resource(path, iface, props)
    local data = {}
    load_data(data, iface, props)
    self:update_resource(path, data)
end

function c_bma_mdb_mgmt:add_resource(path, all_path_data)
    self.queue(function()
        local ues_path = unescape(path)
        log:debug('c_bma_mdb_mgmt: add bma object: %s', ues_path)
        self.on_add:emit(ues_path, all_path_data)
    end)
end

function c_bma_mdb_mgmt:update_resource(path, data)
    self.queue(function()
        local ues_path = unescape(path)
        log:debug('c_bma_mdb_mgmt: update bma object: %s', ues_path)
        self.on_update:emit(ues_path, data)
    end)
end

function c_bma_mdb_mgmt:delete_resource(path)
    self.queue(function()
        local ues_path = unescape(path)
        log:debug('c_bma_mdb_mgmt: delete bma object: %s', ues_path)
        self.on_delete:emit(ues_path)
    end)
end

function c_bma_mdb_mgmt:register_bma_resource_signal()
    local add_sig =
        match_rule.signal(interfaces_added.name, interfaces_added.interface):with_path_namespace(STORAGE_PATH)
    self.add_slot = self.bus:match(add_sig, function(msg)
        local path, interface_and_props = msg:read('oa{sa{sv}}')
        self:load_bma_resource(path, interface_and_props)
    end)
    local del_sig =
        match_rule.signal(interfaces_removed.name, interfaces_removed.interface):with_path_namespace(STORAGE_PATH)
    self.del_slot = self.bus:match(del_sig, function(msg)
        local path, _ = msg:read('oas')
        self:delete_resource(path)
    end)
    local prop_ch_sig = match_rule.signal('PropertiesChanged'):with_interface(
        org_freedesktop_dbus.SD_BUS_PROPERTIES):with_path_namespace(STORAGE_PATH)
    self.prop_changed_slot = self.bus:match(prop_ch_sig, function(msg)
        local interface, props = msg:read()
        local path = msg:path()
        self:update_bma_resource(path, interface, props)
    end)
    log:info('c_bma_mdb_mgmt: monitor bma resource signal')
end

function c_bma_mdb_mgmt:unregister_bma_resource_signal()
    self.add_slot:free()
    self.del_slot:free()
    self.prop_changed_slot:free()
    self.add_slot = nil
    self.del_slot = nil
    self.prop_changed_slot = nil
end

function c_bma_mdb_mgmt:monitor_bma()
    self:register_bma_resource_signal()
    client:OnSmsStatusPropertiesChanged(function(values, _, _)
        local state = values.State:value()
        log:info('BMA status change to %s', state)
        if state == 1 then
            self:unregister_bma_resource_signal()
            self.on_reset:emit()
        elseif state == 0 then
            -- 重新加载bma资源
            local task = self.tasks:new_task('reload bma mdb object task'):loop(function(task)
                self:load_all_bma_resource(STORAGE_PATH)
                task:stop()
            end)
            task.on_task_exit:on(function()
                self:register_bma_resource_signal()
            end)
        end
    end)
end

-- 从 mdb_mgmt 服务读取指定资源
function c_bma_mdb_mgmt:get_bma_resource_from_mdb_mgmt(es_path)
    for _ = 1, MAX_FAILED_COUNT do
        -- 通过 mdb_mgmt 拿到对象的所有 interface
        local ok, rsp = pcall(mdb_service.get_object, self.bus, es_path, {})
        if ok then
            local ifaces = rsp.Object[HOST_AGENT_APP]
            if not ifaces then
                break
            end

            -- 遍历获取所有数据
            local data = {}
            for _, iface in ipairs(ifaces) do
                data[iface] = self.bus:call(HOST_AGENT_APP, es_path,
                    'org.freedesktop.DBus.Properties', 'GetAll', 's', iface)
            end
            return data
        end

        if rsp.name ~= 'org.freedesktop.DBus.Error.ServiceUnknown' then
            break
        end

        self.tasks:sleep_ms(500)
    end
    return nil
end

return c_bma_mdb_mgmt
