-- 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 context = require 'mc.context'
local storage_bus = require 'storage_bus'
local nvme_object = require 'nvme.nvme_object'
local common_def = require 'common_def'
local client = require 'storage.client'
local singleton = require 'mc.singleton'
local c_drive = require 'drive.drive_object'
local class = require 'mc.class'
local signal = require 'mc.signal'
local log = require 'mc.logging'
local skynet = require 'skynet'
local connector = require 'nvme.vpd_connector_collection'
local c_tasks = require 'mc.orm.tasks'
local c_drive_collection = class(nil, nil, true)

function c_drive_collection:ctor()
    self.on_add_drive = c_drive.on_add_object
    self.on_del_drive = c_drive.on_delete_object
    self.on_update_volume_list = signal.new()
    self.on_controller_commu_changed = signal.new()
    self.on_smbios_status_changed = signal.new()
    self.on_add_nvme = nvme_object.on_add_object
    self.on_del_nvme = nvme_object.on_delete_object
    self.nvme_list = {}
end

local function remove_item_from_table(nvme_list, nvme)
    for i, v in ipairs(nvme_list) do
        if v.Slot == nvme.Slot then
            table.remove(nvme_list, i)
            break
        end
    end
end

local function sort_nvme_obj_by_slot(nvme_list)
    table.sort(nvme_list, function (a, b)
        return a.Slot < b.Slot
    end)
end

function c_drive_collection:set_nvme_smart_info_to_defalut()
    for _, nvme_obj in pairs(self.nvme_list) do
        local drive = self:get_drive_by_drive_id(nvme_obj.Slot)
        if drive then
            drive.AvailableSpare = common_def.INVALID_U8
            drive.UsedPercentage = common_def.INVALID_U8
            drive.CriticalWarning = common_def.INVALID_U8
        end
    end
end

function c_drive_collection:process_smbios_status_changed()
    self.on_smbios_status_changed:on(function (smbios_status)
        if not smbios_status then
            c_drive.collection:fold(function(acc, obj, key)
                obj:set_default_values_when_os_off()
            end, {})
        end
        if not self.nvme_list or not next(self.nvme_list) then
            log:notice('NVMe list is empty.')
            return
        end
        self:clean_endpoint()
        self:set_nvme_smart_info_to_defalut()
        if smbios_status then
            for _, nvme_obj in pairs(self.nvme_list) do
                nvme_obj.on_smbios_status_changed:emit()
            end
        end
    end)
end

function c_drive_collection:init()
    self.on_controller_commu_changed:on(function(mctp_state, controller_id)
        if mctp_state then
            c_drive.collection:fold(function(acc, obj, key)
                if obj.RefControllerId == controller_id and obj.identify_pd ~= false and obj.presence ~= 0 then
                    obj:start_update_tasks()
                end
            end, {})
        else
            c_drive.collection:fold(function(acc, obj, key)
                if obj.RefControllerId == controller_id and obj.identify_pd ~= false and obj.presence ~= 0 then
                    obj:stop_update_tasks()
                    obj:set_default_values()
                    obj.RefVolumeList = {}
                end
            end, {})
        end
    end)

    self:process_smbios_status_changed()

    self.on_add_drive:on(function(drive)
        drive.on_update_volume_list:on(function(obj)
            self.on_update_volume_list:emit(obj)
        end)
        drive.on_update_diagnose_metric:on(function(obj_name)
            local tmp_bus = storage_bus.get_instance().bus
            local drive_path = '/bmc/kepler/Systems/1/Storage/Drives/' .. obj_name
            tmp_bus:signal(drive_path, 'bmc.kepler.Metric', 'CollectSignal', 'a{ss}ss',
                context.get_context() or {}, 'disk.log', drive_path)
        end)
    end)

    self.on_add_nvme:on(function(nvme)
        log:notice('Nvme %s is inserted', nvme.Slot)
        table.insert(self.nvme_list, nvme)
        sort_nvme_obj_by_slot(self.nvme_list)
        local drive = self:get_drive_by_drive_id(nvme.Slot)
        local try_count = 0
        local MAX_RETRY_COUNT = 20
        while not drive do
            skynet.sleep(300)
            drive = self:get_drive_by_drive_id(nvme.Slot)
            try_count = try_count + 1
            if try_count > MAX_RETRY_COUNT then
                break
            end
        end
        if drive then
            connector.get_instance().on_add_nvme_object:emit(nvme)
            drive:update_nvme_systemid(nvme)
            log:notice('NVMe :%s update system_id:%s', nvme.Slot, nvme.system_id)
            drive:nvme_static_identified(nvme)
            drive:nvme_identified(nvme)
        end
    end)

    self.on_del_nvme:on(function(nvme)
        log:notice('Nvme %s is removed', nvme.Slot)
        remove_item_from_table(self.nvme_list, nvme)
        sort_nvme_obj_by_slot(self.nvme_list)
        local drive = self:get_drive_by_drive_id(nvme.Slot)
        if drive then
            drive:nvme_static_identified(nil)
            drive:nvme_identified(nil)
        end
    end)

    client:OnComponentPropertiesChanged(function(prop_value, path, interface)
        if prop_value['ReplaceFlag'] == nil or prop_value['ReplaceFlag']:value() ~= 1 then
            return
        end
        local fru_name
        client:ForeachComponentObjects(function(obj)
            if obj.path == path then
                fru_name = obj.Name
            end
        end)
        if not fru_name or fru_name == '' or fru_name == 'N/A' then
            log:error('not found component object, drive name:%s signal path:%s', fru_name, path)
            return
        end
        local drive = self:get_drive(fru_name)
        if not drive then
            log:debug('not found drive object, drive name:%s', fru_name)
            return
        end
        drive.CommandTimeoutTimes = 0
        drive.UnexpectedSenseTimes = 0
    end)
    self:start_update_pd_info_task()
end

-- 任务持续进行，通过enable_request控制是否发送插件请求
function c_drive_collection:start_update_pd_info_task()
    c_tasks.get_instance():new_task('update pd info task'):loop(function(task)
        c_drive.collection:fold(function(acc, obj, key)
            if obj.identify_pd ~= false and obj.presence ~= 0 and obj.RefControllerId ~= 0xff then
                pcall(obj.update_drive_common_info, obj)
            end
        end, {})
    end):set_timeout_ms(10000)
    c_tasks.get_instance():new_task('update pd smart info task'):loop(function(task)
        c_drive.collection:fold(function(acc, obj, key)
            if obj.identify_pd ~= false and obj.presence ~= 0 and obj.RefControllerId ~= 0xff then
                pcall(obj.update_drive_smart_info, obj)
            end
        end, {})
    end):set_timeout_ms(30000)
end

function c_drive_collection:clean_endpoint()
    for _, nvme_obj in pairs(self.nvme_list) do
        nvme_obj.nvme_mi_mctp_obj = false
    end
end

function c_drive_collection:get_drive(drive_name)
    return c_drive.collection:find({ Name = drive_name })
end

function c_drive_collection:get_drive_by_drive_id(drive_id)
    return c_drive.collection:find({ Id = drive_id })
end

function c_drive_collection:get_drive_by_physic_id(ref_controller_id, slot_num, enclosure_id)
    return c_drive.collection:find({
        RefControllerId = ref_controller_id,
        SlotNumber = slot_num,
        EnclosureId = enclosure_id
    })
end

function c_drive_collection:get_drive_by_id(ref_controller_id, id)
    return c_drive.collection:find({
        RefControllerId = ref_controller_id,
        Id = id
    })
end

function c_drive_collection:get_drive_by_device_id(ref_controller_id, device_id)
    return c_drive.collection:find({
        RefControllerId = ref_controller_id,
        device_id = device_id
    })
end

-- 以控制器Id为下标，注意不能多线程访问
local drive_list = {}

function c_drive_collection:get_drives_by_controller_id(controller_id)
    if drive_list[controller_id] == nil then
        drive_list[controller_id] = {}
    else
        for k, _ in pairs(drive_list[controller_id]) do
            drive_list[controller_id][k] = nil
        end
    end
    c_drive.collection:fold(function(acc, obj, key)
        if obj.RefControllerId == controller_id and obj.identify_pd ~= false and obj.presence ~= 0 then
            table.insert(drive_list[controller_id], obj.Name)
        end
    end, {})
    return drive_list[controller_id]
end

function c_drive_collection:check_and_get_drive(drive_name)
    if not type(drive_name) == "string" then
        log:error("[Storage]drive_name type invalid")
        return
    end
    local drive = self:get_drive(drive_name)

    return drive
end

function c_drive_collection:get_identified_drives()
    local drive_name = {}
    c_drive.collection:fold(function(acc, obj, key)
        if obj.RefControllerId ~= 0xff and obj.identify_pd ~= false and obj.presence ~= 0 then
            table.insert(drive_name, obj.Name)
        end
    end, {})
    return drive_name
end

-- 查找指定RAID控制器的物理盘信息，并dump物理盘信息到文件中
function c_drive_collection:dump_controller_physical_drive(ctrl_index, fp_w)
    if self:get_count() == 0 then
        fp_w:write('Not Found Physical Drives.\n')
        return
    end

    local count = 0
    c_drive.collection:fold(function(acc, obj, key)
        if obj.Presence ~= 1 then
            goto continue
        end
        if obj.RefControllerId == ctrl_index then
            count = count + 1
            obj:dump_physical_drive(fp_w)
         end

        ::continue::
    end, {})
    if count == 0 then
        fp_w:write('None of physical drives is in position.\n')
    end
end

function c_drive_collection:get_count()
    local count = 0
    c_drive.collection:fold(function(acc, obj, key)
        count = count + 1
    end, {})
    return count
end

function c_drive_collection:get_manufacturer(device_number)
    local drive_obj = c_drive.collection:find({
        Id = device_number - 1
    })

    if not drive_obj then
        log:error("cannot find drive_obj, device_number:%s", device_number)
        return ''
    end

    return drive_obj.Manufacturer
end

function c_drive_collection:update_drive_ref_controller_type_id(ctrl_index, type_id)
    c_drive.collection:fold(function(acc, obj, key)
        if obj.RefControllerId == ctrl_index then
            obj.RefControllerTypeId = type_id
        end
   end, {})
end

function c_drive_collection:get_all_drives()
    local drives = {}
    c_drive.collection:fold(function(acc, obj, key)
        table.insert(drives, obj)
   end, {})
   return drives
end

-- 匹配drive对象关联的nvme对象，匹配不到返回nil
function c_drive_collection:get_nvme_by_drive_name(drive)
    local index = string.match(drive.Name, "Disk(%d+)")
    index = tonumber(index)
    if not index then
        log:info("Invalid drive name, drive name is %s", drive.Name)
        return nil
    end
    for _, obj in pairs(self.nvme_list) do
        if obj.Slot == index then
            return obj
        end
    end
    -- 匹配不到时打印nvme列表
    local nvme_slot_list = {}
    for _, obj in pairs(self.nvme_list) do
        table.insert(nvme_slot_list, obj.Slot)
    end
    log:info("Get nvme failed, drive name is %s, nvme list is %s", drive.Name, table.concat(nvme_slot_list, "-"))
    return nil
end

function c_drive_collection:get_presence_on_drives(drive_name)
    local drives = {}
    c_drive.collection:fold(function(acc, obj, key)
        if obj.Presence == 1 then
            if not drive_name or obj.Name == drive_name then
                table.insert(drives, obj)
            end
        end
   end, {})
   return drives
end

return singleton(c_drive_collection)
