-- 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 c_handler_base = require 'bma.handles.handler_base'
local c_drive = require 'drive.drive_object'
local log = require 'mc.logging'
local singleton = require 'mc.singleton'
local def = require 'common_def'
local pd_identify_service = require 'pd.pd_identify_service'
local method_misc = require 'method_misc'

local is_nil_or_null = c_handler_base.is_nil_or_null
local default_na = c_handler_base.default_na
local update_if_not_config = c_handler_base.update_if_not_config
local UPDATE_INTERVAL_OS_24_HOURS = (3600 * 24) --24小时更新一次
local POWER_ON_HOURS_LIMIT_30_DAYS = (30 * 24)
local smart_def = def.PD_SMART_DEF
local TASK_DUMP_LOG_INTERVAL = 30 * 60 * 1000 -- 检测日志收集触发条件的时间间隔设为30分钟

local TASK_DUMP_WRITE_AMP <const> = 'dump_write_amp'
local PD_PHYSICAL_SPEED <const> = {
    [1] = 1.5,
    [2] = 3.0,
    [3] = 6.0,
    [4] = 12.0,
    [5] = 2.5,
    [6] = 5.0,
    [7] = 8.0,
    [8] = 10.0,
    [9] = 16.0,
    [10] = 20.0,
    [11] = 30.0,
    [12] = 32.0,
    [13] = 40.0,
    [14] = 64.0,
    [15] = 80.0,
    [16] = 96.0,
    [17] = 128.0,
    [18] = 160.0,
    [19] = 256.0,
    [20] = 22.5
}

local BMA_SAS_SATA_STATUS <const> = {
    ['OFFLINE'] = 3,
    ['FAILED'] = 4,
    ['REBUILD'] = 5,
    ['Active'] = 13,
    ['Stand-by'] = 14,
    ['Sleep'] = 15,
    ['DST executing in background'] = 16,
    ['SMART Off-line Data Collection executing in background'] = 17,
    ['SCT command executing in background'] = 18
}

local c_handler_drives = class(c_handler_base)

function c_handler_drives:ctor()
end

function c_handler_drives:init()
    self:regist_odata_type('Drive')

    self:regist_class_type(c_drive)

    c_drive.on_add_object:on(function(object)
        if object.Presence == 1 then
            self:match_resource(object)
        end
    end)
end

local function get_bmaid_from_path(path)
    if type(path) ~= 'string' then
        return ""
    end

    local bma_id = path:match('([^/]+)%s*$')
    if not bma_id then
        return ""
    end

    if bma_id:sub(1, 3) ~= 'PCH' then
        return ""
    end

    return bma_id
end

-- 根据host_agent的丝印信息匹配storage的drive对象
function c_handler_drives:find_object(path, data)
    local bma_silk = ''
    log:notice('drive silk is %s', data.Oem.Huawei.SilkScreen)
    if not is_nil_or_null(data.Oem.Huawei.SilkScreen) then
        bma_silk = data.Oem.Huawei.SilkScreen
    else
        return nil
    end 

    -- 通过丝印匹配
    local str = ''
    local object = c_drive.collection:find(function(obj)
        str = string.format('HDDPlaneDisk%s', obj.Id)
        if str == bma_silk then
            return obj
        end
    end)

    if object then
        object.bma_id = get_bmaid_from_path(path)
        return object
    end

    return nil
end

function c_handler_drives:get_val_or_default(val, default_val)
    return is_nil_or_null(val) and default_val or val
end

function c_handler_drives:get_key_by_val(val_table, val, def_val)
    for k, v in pairs(val_table) do
        if v == val then
            return k
        end
    end

    return def_val
end

function c_handler_drives:check_hw_defined_smart(data)
    if data.Oem.Huawei.HuaweiSpecificLogPages and next(data.Oem.Huawei.HuaweiSpecificLogPages) and
        next(data.SmartDetails.HuaweiDefinedAttributeItemList) then
        return true    
    end

    return false
end

function c_handler_drives:get_smart_value_by_attr_id(id, attribute_item_list)
    for _, attribute_item in pairs(attribute_item_list) do
        if attribute_item['ID#'] == id and attribute_item.VALUE then
            return attribute_item.VALUE
        end
    end

    return def.INVALID_U8
end

function c_handler_drives:get_wearout_raw_by_attr_id(id, attribute_item_list)
    for _, attribute_item in pairs(attribute_item_list) do
        if attribute_item['ID#'] == id and attribute_item.RAW_VALUE then
            return attribute_item.RAW_VALUE
        end
    end

    return def.INVALID_U8
end

function c_handler_drives:get_smart_raw_by_attr_id(id, attribute_item_list)
    for _, attribute_item in pairs(attribute_item_list) do
        if attribute_item['ID#'] == id and attribute_item.RAW_VALUE then
            return attribute_item.RAW_VALUE
        end
    end

    return def.INVALID_U32
end

function c_handler_drives:get_pd_wearout_by_hw_defined(attribute_item_list)
    local slc_wearout = self:get_wearout_raw_by_attr_id(smart_def.HW_DEFINED_SMART_ATTRIBUTE_ID_SLC_WEAROUT,
        attribute_item_list)

    local tlc_wearout = self:get_wearout_raw_by_attr_id(smart_def.HW_DEFINED_SMART_ATTRIBUTE_ID_TLC_WEAROUT,
        attribute_item_list)
    local wearout = slc_wearout > tlc_wearout and tlc_wearout or slc_wearout
    return wearout
end

function c_handler_drives:get_pd_wearout_info(obj, data)
    if obj.Protocol == def.DRIVE_PROTOCOL.SATA and data.AttributeItemList then
        if obj.estimated_remaining_lifespan_info.is_support_hw_defined == 1 then
            return self:get_pd_wearout_by_hw_defined(data.HuaweiDefinedAttributeItemList)
        end
        if smart_def.VENDOR_ATTR_ID[obj.Manufacturer] then
            return self:get_smart_value_by_attr_id(smart_def.VENDOR_ATTR_ID[obj.Manufacturer], 
                data.AttributeItemList)
        end
    end

    if obj.Protocol == def.DRIVE_PROTOCOL.SAS and not is_nil_or_null(data.SSMediaUsedEnduranceIndicator) and
        data.SSMediaUsedEnduranceIndicator <= smart_def.ONE_HUNDRED_PERCENT then
        return (smart_def.ONE_HUNDRED_PERCENT - data.SSMediaUsedEnduranceIndicator)
    end

    return def.INVALID_U8
end

local function get_avg_by_raw_value(data)
    local avg = string.match(data, "avg:(.*)")
    return avg and tonumber(avg) or def.INVALID_U32
end

function c_handler_drives:update_hw_smart_info(obj, attribute_item_list)
    obj.estimated_remaining_lifespan_info.slc_avg_ec = get_avg_by_raw_value(
        self:get_smart_raw_by_attr_id(smart_def.HW_DEFINED_SMART_ATTRIBUTE_ID_SLC_ERASE_COUNT, attribute_item_list))
    obj.estimated_remaining_lifespan_info.tlc_avg_ec = get_avg_by_raw_value(
        self:get_smart_raw_by_attr_id(smart_def.HW_DEFINED_SMART_ATTRIBUTE_ID_TLC_ERASE_COUNT, attribute_item_list))
    obj.estimated_remaining_lifespan_info.slc_pe_cycle =
        self:get_smart_raw_by_attr_id(smart_def.HW_DEFINED_SMART_ATTRIBUTE_ID_SLC_PE_CYCLE, attribute_item_list)
    obj.estimated_remaining_lifespan_info.tlc_pe_cycle =
        self:get_smart_raw_by_attr_id(smart_def.HW_DEFINED_SMART_ATTRIBUTE_ID_TLC_PE_CYCLE, attribute_item_list)
    obj.estimated_remaining_lifespan_info.slc_poh = obj.PowerOnHours
    obj.estimated_remaining_lifespan_info.tlc_poh = obj.PowerOnHours
end

function c_handler_drives:update_est_lifespan(obj, data)
    obj.estimated_remaining_lifespan_info.drive_name = obj.Name
    obj.estimated_remaining_lifespan_info.media_type = def.MEDIA_TYPE_STR[obj.MediaType]
    obj.estimated_remaining_lifespan_info.manufacturer = obj.Manufacturer
    obj.estimated_remaining_lifespan_info.serial_number = obj.SerialNumber
    obj.estimated_remaining_lifespan_info.protocol = obj.Protocol
    if obj.estimated_remaining_lifespan_info.is_support_hw_defined == 1 then
        self:update_hw_smart_info(obj, data.HuaweiDefinedAttributeItemList)
    else
        obj.estimated_remaining_lifespan_info.remn_wearout = obj.PredictedMediaLifeLeftPercent
        obj.estimated_remaining_lifespan_info.power_on_hours = obj.PowerOnHours
    end
end

function c_handler_drives:get_written_info(id, attribute_item_list)
    for _, attribute_item in pairs(attribute_item_list) do
        if attribute_item['ID#'] == id and attribute_item.RAW_VALUE then
            return attribute_item.RAW_VALUE
        end
    end
    return def.STORAGE_INFO_INVALID_DWORD
end

function c_handler_drives:update_sata_write_amplification_by_vendor(obj, data)
    if smart_def.NAND_WRITTEN_ATTR_ID[obj.Manufacturer] then
        obj.write_amplification_info.vendor_nand_write =
            self:get_written_info(smart_def.NAND_WRITTEN_ATTR_ID[obj.Manufacturer], data.AttributeItemList)
    end

    if smart_def.HOST_WRITTEN_ATTR_ID[obj.Manufacturer] then
        obj.write_amplification_info.vendor_host_write =
            self:get_written_info(smart_def.HOST_WRITTEN_ATTR_ID[obj.Manufacturer], data.AttributeItemList)
    end

    obj.write_amplification_info.vendor_valid_flag = 1
end

function c_handler_drives:update_sata_write_amplification_by_hw_defined(obj, data)
    obj.write_amplification_info.hw_defined_nand_write_l =
        self:get_written_info(smart_def.HW_DEFINED_SMART_ATTRIBUTE_ID_TLC_NAND_WRITTEN_L,
        data.HuaweiDefinedAttributeItemList)
    obj.write_amplification_info.hw_defined_nand_write_h =
        self:get_written_info(smart_def.HW_DEFINED_SMART_ATTRIBUTE_ID_TLC_NAND_WRITTEN_H,
        data.HuaweiDefinedAttributeItemList)
    obj.write_amplification_info.hw_defined_host_write_l =
        self:get_written_info(smart_def.HW_DEFINED_SMART_ATTRIBUTE_ID_TLC_HOST_WRITTEN_L,
        data.HuaweiDefinedAttributeItemList)
    obj.write_amplification_info.hw_defined_host_write_h =
        self:get_written_info(smart_def.HW_DEFINED_SMART_ATTRIBUTE_ID_TLC_HOST_WRITTEN_H,
        data.HuaweiDefinedAttributeItemList)
end

function c_handler_drives:update_sas_write_amplification(obj, data)
    if is_nil_or_null(data.NandWrittenMiB) and is_nil_or_null(data.HostWrittenMiB) then
        return
    end
    if not is_nil_or_null(data.NandWrittenMiB) then
        obj.write_amplification_info.vendor_nand_write = data.NandWrittenMiB
    end
    if not is_nil_or_null(data.HostWrittenMiB) then
        obj.write_amplification_info.vendor_host_write = data.HostWrittenMiB
    end

    obj.write_amplification_info.vendor_valid_flag = 1
end

function c_handler_drives:check_manufacture_statisfied_top3(obj)
    if obj.Manufacturer == smart_def.PD_VENDOR_NAME_INTEL or obj.Manufacturer == smart_def.PD_VENDOR_NAME_HUAWEI or
        obj.Manufacturer == smart_def.PD_VENDOR_NAME_SAMSUNG then
        return true
    end
    return false
end

function c_handler_drives:check_collect_est_lifespan_time(obj)
    local last = obj.est_lifespan_last_up_time
    local current = math.floor(os.time())
    if last == 0 then
        obj.write_amplification_info.first_start_flag = 1
        obj.est_lifespan_last_up_time = current
        return true
    end

    if ((current > last) and (current - last) > UPDATE_INTERVAL_OS_24_HOURS) or
        ((last > current) and (def.INVALID_U32 - last + current) > UPDATE_INTERVAL_OS_24_HOURS) then
        obj.est_lifespan_last_up_time = current
        return true
    end
    return false
end

function c_handler_drives:update_sata_write_amplification(obj, data)
    if obj.estimated_remaining_lifespan_info.is_support_hw_defined == 1 then
        self:update_sata_write_amplification_by_hw_defined(obj, data)
        return
    end
    self:update_sata_write_amplification_by_vendor(obj, data)
end

function c_handler_drives:update_smart_info(obj, data)
    local smart_details = data.SmartDetails
    if self:check_hw_defined_smart(data) then
        obj.estimated_remaining_lifespan_info.is_support_hw_defined = 1
        obj.write_amplification_info.hw_defined_valid_flag = 1
    end

    if obj.PowerOnHours >= POWER_ON_HOURS_LIMIT_30_DAYS then
        obj.write_amplification_info.update_support_flag = 1
    end

    obj.PredictedMediaLifeLeftPercent = self:get_pd_wearout_info(obj, smart_details)
    if obj.Protocol == def.DRIVE_PROTOCOL.SATA then
        if obj.estimated_remaining_lifespan_info.is_support_hw_defined == 1 or
            self:check_manufacture_statisfied_top3(obj) then
            self:update_est_lifespan(obj, smart_details)
            self:update_sata_write_amplification(obj, smart_details)
            return
        end
    end
    if obj.Protocol == def.DRIVE_PROTOCOL.SAS and (obj.Manufacturer == smart_def.PD_VENDOR_NAME_HUAWEI or
        obj.Manufacturer == smart_def.PD_VENDOR_NAME_AJ) then
        self:update_est_lifespan(obj, smart_details)
        self:update_sas_write_amplification(obj, smart_details)
    end
end

function c_handler_drives:update_hw_info(obj, data)
    if not is_nil_or_null(data.FirmwareVersion) then
        obj.Revision = self:get_val_or_default(data.FirmwareVersion, def.INVALID_STRING)
    end

    if not is_nil_or_null(data.VendorID) then
        obj.ManufacturerId = tonumber(data.VendorID)
    end

    if not is_nil_or_null(data.Temperature) then
        obj.TemperatureCelsius = self:get_val_or_default(data.Temperature, def.INVALID_U8)
    end

    if not is_nil_or_null(data.PowerOnHours) then
        obj.PowerOnHours = self:get_val_or_default(data.PowerOnHours, def.INVALID_U32)
    end

    if not is_nil_or_null(data.PowerState) then
        obj.PowerState = self:get_val_or_default(data.PowerState, def.INVALID_U8)
    end

    -- 如果硬盘厂商为ATA则根据硬盘ManufacturerOUI判断厂商名
    if not is_nil_or_null(data.ManufacturerOUI) and obj.Manufacturer == 'ATA' then
        obj.Manufacturer = method_misc:get_manufacturer_name_by_oui(data.ManufacturerOUI)
    end
end

function c_handler_drives:update_drive_info(obj, data)
    if not is_nil_or_null(data.Manufacturer) then
        obj.Manufacturer = self:get_val_or_default(data.Manufacturer, def.INVALID_STRING)
    end

    if not is_nil_or_null(data.Model) then
        obj.Model = self:get_val_or_default(data.Model, def.INVALID_STRING)
    end

    if not is_nil_or_null(data.Protocol) then
        obj.Protocol = def.DRIVE_PROTOCOL[data.Protocol] or def.INVALID_U8
    end

    if not is_nil_or_null(data.MediaType) then
        obj.MediaType = self:get_key_by_val(def.MEDIA_TYPE_STR, data.MediaType, def.INVALID_U8)
    end

    if not is_nil_or_null(data.SerialNumber) then
        obj.SerialNumber = self:get_val_or_default(data.SerialNumber, def.INVALID_STRING)
    end

    if not is_nil_or_null(data.CapacityBytes) then
        obj.CapacityMiB = data.CapacityBytes // (1024 * 1024)
    end

    obj.RotationSpeedRPM = self:get_val_or_default(data.RotationSpeedRPM, def.INVALID_U16)

    if not is_nil_or_null(data.CapableSpeedGbs) then
        obj.CapableSpeedGbs = self:get_key_by_val(PD_PHYSICAL_SPEED,
            data.CapableSpeedGbs, def.INVALID_U8)
    end

    if not is_nil_or_null(data.NegotiatedSpeedGbs) then
        obj.NegotiatedSpeedGbs = self:get_key_by_val(PD_PHYSICAL_SPEED,
            data.NegotiatedSpeedGbs, def.INVALID_U8)
    end

    obj.MediaErrorCount = self:get_val_or_default(data.MediaErrorCount, def.INVALID_U32)
    obj.OtherErrorCount = self:get_val_or_default(data.OtherErrorCount, def.INVALID_U32)
    obj.PredictedFailCount = self:get_val_or_default(data.PredFailCount, def.INVALID_U32)

    if not is_nil_or_null(data.Status) then
        obj.FirmwareStatus = BMA_SAS_SATA_STATUS[data.Status] or def.INVALID_U8
    end

    if data.Oem and data.Oem.Huawei then
        self:update_hw_info(obj, data.Oem.Huawei)
        if def.MEDIA_TYPE_STR[obj.MediaType] == 'SSD' and data.SmartDetails then
            self:update_smart_info(obj, data)
        end
    end
end

function c_handler_drives:update_drive(obj, data)
    log:notice("Update %s info by bma", obj.Name)
    self:update_drive_info(obj, data)

    log:debug("Update drive info(revision: %s)",data.FirmwareVersion)
    update_if_not_config(obj.Revision, default_na(data.FirmwareVersion))
    obj:update_asset_data_info()
end

function c_handler_drives:start_dump_task(obj, data)
    if not (obj.write_amplification_info.vendor_valid_flag == 1 or
        obj.write_amplification_info.hw_defined_valid_flag == 1) then
        return
    end
    obj:new_task({ TASK_DUMP_WRITE_AMP, obj.ObjectName }):loop(function(task)
        if task.is_exit then
            return
        end
        if not self:check_collect_est_lifespan_time(obj) then
            return
        end

        obj:record_write_amp_log()
        if task.is_exit then
            return
        end
    end):set_timeout_ms(TASK_DUMP_LOG_INTERVAL)
end

function c_handler_drives:stop_dump_task(obj)
    obj:stop_task({ TASK_DUMP_WRITE_AMP, obj.ObjectName })
end

-- BMA断开后只恢复从BMA获取的动态信息为默认值
function c_handler_drives:reset_drive(obj)
    log:notice("Reset %s info by bma", obj.Name)
    obj:set_default_values()
    self:stop_dump_task(obj)
    pd_identify_service.get_instance():set_subhealth_info_default_values(obj)
end

function c_handler_drives:add(_, data, object)
    self:update_drive(object, data)
    self:start_dump_task(object, data)
end

function c_handler_drives:update(_, data, object)
    self:update_drive(object, data)
end

function c_handler_drives:delete(_, _, object)
    self:reset_drive(object)
end

function c_handler_drives:reset()
    for object, _ in pairs(self.objects) do
        self:reset_drive(object)
    end
end

return singleton(c_handler_drives)
