-- 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 queue = require 'skynet.queue'
local log = require 'mc.logging'
local context = require 'mc.context'
local frudata_intf = require 'frudata_intf'
local fru_persistence = require 'fru_persistence'
local fru_singleton = require 'fru_singleton'
local common = require 'common'
local custom_messages = require 'messages.custom'
local base_messages = require 'messages.base'

local UNIQUE_OFFSET <const> = 100
local UNIQUE_LENGTH <const> = 24
local FRU_HEADER_TAG <const> = '0x4652550'
local MAX_QUEUE_SIZE = 100

local manage = {}

local cs = queue()
local queue_size = 0

-- 重试获取BMC版本号，最多20次，每次间隔1秒，成功立即返回
local function get_bmc_version_with_retry(fru, log_prefix)
    local version = ''
    for i = 1, 20 do
        local ok, ver = pcall(fru.get_bmc_version, fru)
        if ok and ver and ver ~= '' then
            version = ver
            if log_prefix then
                log:notice('%s, bmc_ver = %s, retry_count = %d', log_prefix, version, i)
            end
            break
        end
        if i < 20 then
            skynet.sleep(100)  -- 延时1秒 (100 * 10ms)
        end
    end
    return version
end

function manage:init(obj)
    local fruid = obj.FruId
    -- 1, 1, 0 分别表示支持ipmi读写电子标签,默认是天池规范的Eep规范,是否有system域
    frudata_intf.frudata_init(fruid, '', '', 1, 1, 0)

    local fru = fru_singleton.get_instance()

    -- 写默认值
    self:write_prop(obj, 'MfgDate', '\x00')
    self:write_prop(obj, 'BoardManufacturer', common.DEFAULT_MANU)
    self:write_prop(obj, 'ManufacturerName', common.DEFAULT_MANU)

    obj.MfgDate = '1996/01/01 Mon 00:00:00'
    obj.BoardManufacturer = common.DEFAULT_MANU
    obj.ManufacturerName = common.DEFAULT_MANU

    local property_name, values = fru_persistence.get_instance():sync_db2dbus(fruid)
    if property_name and values then
        self:update_non_standard_fru(fruid, property_name, values, obj)
    end

    skynet.fork(function()
        local fru = fru_singleton.get_instance()
        local version = get_bmc_version_with_retry(fru, string.format('init non standard frudata, obj.FruId = %d', fruid))
        
        -- 写入属性到电子标签
        self:write_prop(obj, 'BoardFRUFileID', version)
        self:write_prop(obj, 'ProductFRUFileID', version)
        -- 更新对象属性
        obj.BoardFRUFileID = version
        obj.ProductFRUFileID = version
    end)
end

function manage:write_prop(frudata_obj, property, value)
    if common.property[property] then
        local _, result = pcall(frudata_intf.ipmi_write_elabel, frudata_obj.FruId, common.property[property][1],
            common.property[property][2], 0, #value, value)
        if result ~= 0 then
            log:error('Write fru%d E-label data(RAW:%s) failed', frudata_obj.FruId, value)
        end
    end
end

function manage:update_fru_uid(fru_id, uid)
    fru_singleton.get_instance().fru_uids[fru_id] = uid
    local class_mgnt = require 'mc.class_mgnt'
    local objs = class_mgnt('Fru'):get_all()
    for _, obj in pairs(objs) do
        if obj.FruId == fru_id then
            obj.UniqueId = uid
            log:notice('set fru(%d) uid(%s) successfully', fru_id, uid)
            break
        end
    end
end

function manage:fru_check_and_fix_eeprom_fru_head(obj, eep_header, ctx)
    local fru_header_tag = string.format('0x%X%X%X%X', eep_header:byte(1, 4))
    if fru_header_tag ~= FRU_HEADER_TAG then
        log:warn('fru(%d) read eeprom head tag failed, %s', obj.FruId, fru_header_tag)
        obj.EepromWp = common.EEPROM_WRITE_PROTECT.CLOSE
        obj.FruDev:Write(ctx, 0, 'FRU') -- 回写FRU到eep头
        obj.EepromWp = common.EEPROM_WRITE_PROTECT.OPEN
    end
end

function manage:tc_fru_get_offset_and_uid(obj, eep_header, ctx)
    local fru_offset_l = tonumber(string.byte(eep_header, 14)) -- E2P头中14/15位为fru域的偏移
    local fru_offset_h = tonumber(string.byte(eep_header, 15))
    local fru_offset = ((fru_offset_h << 8) + fru_offset_l) * 8
    -- 天池规范组件uid在eep中有配置
    local uid = obj.FruDev:Read(ctx, UNIQUE_OFFSET, UNIQUE_LENGTH)
    return fru_offset, uid
end

function manage:frudev_read_from_eeprom(obj)
    local ctx = context.get_context_or_default()
    local fru_id = obj.FruId
    log:notice('read eeprom data begin, obj.FruId = %d', fru_id)
    local eep_header = obj.FruDev:Read(ctx, 0, 17) -- 读取E2P的头
    log:info('read eeprom data end, obj.FruId = %d', fru_id)

    local fru_offset = 0
    local system_data, uid = '', ''
    local file_len = common.FRU_FILE_LEN

    if obj.StorageType == 'EepromV2' then
        self:fru_check_and_fix_eeprom_fru_head(obj, eep_header, ctx)
    elseif obj.StorageType == 'TianChi' then
        fru_offset, uid = self:tc_fru_get_offset_and_uid(obj, eep_header, ctx)
    elseif obj.StorageType == 'EepromStandard' then
        file_len = 1024 -- FruDev支持不同大小的器件，而不是FruData
    end

    if fru_id == 0 then
        local sys_offset_l = tonumber(string.byte(eep_header, 16)) -- E2P头中16/17位为system域的偏移
        local sys_offset_h = tonumber(string.byte(eep_header, 17))
        local system_offset = ((sys_offset_h << 8) + sys_offset_l) * 8
        system_data = obj.FruDev:Read(ctx, system_offset, common.SYSTEM_DATA_LEN)
        log:notice('read eeprom system data, frudata system offset = %d, system_data = [%s]',
            system_offset, system_data)
    end

    local fru_file = obj.FruDev:Read(ctx, fru_offset, file_len)
    log:notice('read eeprom data, frudata(%d) offset = %d, fru_file = [%s]', fru_id, fru_offset, fru_file)
    return fru_file, system_data, fru_offset, uid
end

function manage:update_non_standard_fru(fru_id, property_name, value, obj)
    if not property_name or not value then
        log:error('property name or value is nil')
        error(base_messages.InvalidObject())
    end

    if #property_name ~= #value then
        log:error('property name length and value length is not same')
        error(custom_messages.WrongArrayLength(tostring(#value)))
    end
    local data, result
    for i, property in pairs(property_name) do
        -- 过滤属性
        if not common.property[property] then
            log:warn('not support property(%s)', property)
            goto continue
        end
        if not common.validate_byte(common.property[property][1], common.property[property][2], value[i]) then
            log:error('%s have special characters', property)
            goto continue
        end
        log:notice('fru[%d] property_name: %s, value:%s', fru_id, property, value[i])

        -- MfgDate需要特殊转换
        data = (property == 'MfgDate') and common.string_to_char(value[i]) or value[i]
        _, result = pcall(frudata_intf.ipmi_write_elabel, fru_id, common.property[property][1],
            common.property[property][2], 0, #data, data)
        if result ~= 0 then
            log:error('Write fru%d E-label data(RAW:%s) failed', fru_id, data)
        end

        ::continue::
    end

    self:update_fru_data_to_dbus(fru_id, obj)
end

local function manufactur_and_version_init(fru_id, table, obj)
    local default_manu = common.DEFAULT_MANU
    skynet.fork(function()
        if table.BoardManufacturer == '' then
            -- 2 1 为board_manufacturer的区域
            frudata_intf.ipmi_write_elabel(fru_id, common.FRU_AREA.BOARD, common.FRU_FIELD.BOARD.MANUFACTURER, 0,
                #default_manu, default_manu)
        end
        if table.ProductManufacturer == '' then
            -- 3 0 为product_manufacturer的区域
            frudata_intf.ipmi_write_elabel(fru_id, common.FRU_AREA.PRODUCT, common.FRU_FIELD.PRODUCT.MANUFACTURER, 0,
                #default_manu, default_manu)
        end

        -- 只有fruid为0的电子标签初始化时需要同步当前BMC版本
        if fru_id == 0 and fru_singleton.get_instance().is_fru_main_board_init then
            local fru = fru_singleton.get_instance()
            local version = get_bmc_version_with_retry(fru, string.format('manufactur_and_version_init, fru_id = %d', fru_id))

            -- 版本号获取成功则写入，失败则只记录警告，不影响其他操作
            if version ~= '' then
                frudata_intf.ipmi_write_elabel(0, common.FRU_AREA.BOARD, common.FRU_FIELD.BOARD.FRUFILEID, 0,
                    #version, version)
                frudata_intf.ipmi_write_elabel(0, common.FRU_AREA.PRODUCT, common.FRU_FIELD.PRODUCT.FRUFILEID, 0,
                    #version, version)
                obj.BoardFRUFileID = version
                obj.ProductFRUFileID = version
            end
            fru_singleton.get_instance().is_fru_main_board_init = false
        end
    end)
end

-- 判断是否为闰年
local function is_leap_year(year)
    return (year % 4 == 0 and year % 100 ~= 0) or (year % 400 == 0)
end

-- 获取指定月份的天数
local function get_days_in_month(year, month)
    local month_days = {
        31, 28, 31, 30, 31, 30,
        31, 31, 30, 31, 30, 31
    }
    
    -- 如果是2月且是闰年，返回29天
    if month == 2 and is_leap_year(year) then
        return 29
    end
    return month_days[month] or 31
end

-- 判断Borad域的扩展域是否存在合法的Manufactured字段
local function has_manufactured_field(fru_id, board_cunstom_info)
    local manufactured_date = board_cunstom_info:match("Manufactured=(%d%d%d%d%-%d%d%-%d%d)")
    if manufactured_date == nil then
        return nil
    end
    local year, month, day = manufactured_date:match("(%d+)-(%d+)-(%d+)")
    year = tonumber(year)
    month = tonumber(month)
    day = tonumber(day)
    if month < 1 or month > 12 then
        return nil
    end
    if day < 1 or day > get_days_in_month(year, month) then
        return nil
    end
    log:notice("fru(%d)  Manufactured = %s",fru_id, manufactured_date)
    return os.time({year = year, month = month, day = day, hour = 0, min = 0, sec = 0})
end

-- 根据扩展域时间更新MfgDate
local function update_mfg_date_by_manufactured(manufactured_timestamp, fru_id, obj)
    local base_timestamp = os.time({year = 1996, month = 1, day = 1, hour = 0, min = 0, sec = 0})
    local minutes_diff = math.floor((manufactured_timestamp - base_timestamp) / 60)
    local req_data
    if minutes_diff > 0xFFFFFF or minutes_diff < 0 then
        req_data = '\x00\x00\x00'
        obj.MfgDate = '1996/01/01 Mon 00:00:00'
        log:notice("Mfg_Date has been updated to %s",obj.MfgDate)
    else
        local byte1 = minutes_diff & 0XFF
        local byte2 = (minutes_diff >> 8) & 0XFF
        local byte3 = (minutes_diff >> 16) & 0XFF
        req_data = string.char(byte1) .. string.char(byte2) .. string.char(byte3)
        obj.MfgDate = obj.BoardCustomInfo:match("Manufactured=(%d+-%d+-%d+)")
        obj.MfgDate = obj.MfgDate:gsub("-", "/")
        local weekday = os.date("%w", manufactured_timestamp)
        local weekday_abbr = {
            ["0"] = "Sun",
            ["1"] = "Mon",
            ["2"] = "Tue",
            ["3"] = "Wed",
            ["4"] = "Thu",
            ["5"] = "Fri",
            ["6"] = "Sat"
        }
        obj.MfgDate = obj.MfgDate.." "..weekday_abbr[weekday].." 00:00:00"
        log:notice("Mfg_Date has been updated to %s",obj.MfgDate)
    end
    local ok, result = pcall(frudata_intf.ipmi_write_elabel, fru_id, common.FRU_AREA.BOARD,
                common.FRU_FIELD.BOARD.MFGDATE, 0x00, 0x03, req_data)
    if ok then
        log:notice('execute frudata_intf.ipmi_write_elabel successfully, ret: %s', result)
    else
        log:error('execute frudata_intf.ipmi_write_elabel failed, ret: %s', result)
    end
end

function manage:update_fru_data_to_dbus(fru_id, obj)
    local ok, chassis_type, mfg_date, chassis_part_num, chassis_serial_num,
    board_manufacturer, board_product_name, board_serial_num,
    board_part_num, board_file_id, product_manufacturer, product_name,
    product_part_num, product_version, product_serial_num,
    product_asset_tag, product_file_id, chassis_ext, board_ext,
    product_ext = pcall(frudata_intf.get_fru_info, fru_id)
    if not ok then
        log:error('get fru(%d) info faild.', fru_id)
        return
    end

    -- 需要初始化的属性
    local table = {
        BoardManufacturer = board_manufacturer,
        ProductManufacturer = product_manufacturer,
        BoardFileId = board_file_id,
        ProductFileId = product_file_id
    }
    obj.ChassisType = chassis_type or ''
    obj.ChassisPartNumber = chassis_part_num or ''
    obj.ChassisSerialNumber = chassis_serial_num or ''
    obj.ChassisCustomInfo = chassis_ext or ''
    obj.MfgDate = mfg_date or '1996/01/01 Mon 00:00:00'
    obj.BoardManufacturer = (board_manufacturer == '' and common.DEFAULT_MANU) or board_manufacturer
    obj.BoardProductName = board_product_name or ''
    obj.BoardSerialNumber = board_serial_num or ''
    obj.BoardPartNumber = board_part_num or ''
    obj.BoardFRUFileID = board_file_id or ''
    obj.BoardCustomInfo = board_ext or ''
    obj.ManufacturerName = (product_manufacturer == '' and common.DEFAULT_MANU) or product_manufacturer
    obj.ProductName = product_name or ''
    obj.ProductPartNumber = product_part_num or ''
    obj.ProductVersion = product_version or ''
    obj.ProductSerialNumber = product_serial_num or ''
    obj.AssetTag = product_asset_tag or ''
    obj.ProductFRUFileID = product_file_id or ''
    obj.ProductCustomInfo = product_ext or ''

    -- 根据Board域扩展域受否存在Manufactured字段判断是否需要写入MfgDate
    local manufactured_timestamp
    ok, manufactured_timestamp = pcall(has_manufactured_field, fru_id, obj.BoardCustomInfo)
    if ok and manufactured_timestamp ~= nil then
        update_mfg_date_by_manufactured(manufactured_timestamp, fru_id, obj)
    end
    manufactur_and_version_init(fru_id, table, obj)
end

function manage:update_sys_data_to_dbus(fru_id, obj)
    local sys_ver, sys_mfg_name, sys_product_name, sys_sn =
        frudata_intf.get_system_info(fru_id)
    log:notice('update_sys_data_to_dbus sys_ver = %s, fru_id = %d', sys_ver, fru_id)
    obj.SystemManufacturer = sys_mfg_name or ''
    obj.SystemProductName = sys_product_name or ''
    if sys_ver and sys_ver ~= 0 then
        obj.SystemVersion = tostring(sys_ver)
    else
        obj.SystemVersion = ''
    end
    obj.SystemSerialNumber = sys_sn or ''
end

function manage:write_fru_area_to_eep(fru_id, data, obj, fruinfo_manage)
    local offset = 0
    if fruinfo_manage[fru_id] then
        offset = fruinfo_manage[fru_id].FruOffset
    else
        log:error('fruinfo_manage[%d] == nil', fru_id)
    end

    cs(function()
        if queue_size >= MAX_QUEUE_SIZE then
            log:notice('write fru area to eep failed, queue is full. fru_id = %d', fru_id)
            return
        end
        queue_size = queue_size + 1

        log:notice('write fru area to eep start: fru_id = %d, FruOffset = %d, EepromWp_state = %d',
            fru_id, offset, obj.EepromWp)
        obj.EepromWp = common.EEPROM_WRITE_PROTECT.CLOSE -- 关闭写保护
        obj.FruDev:Write(context.new(), offset, data)
        obj.EepromWp = common.EEPROM_WRITE_PROTECT.OPEN  -- 开启写保护
        log:notice('write fru area to eep end: fru_id = %d, data = [%s]', fru_id, data)

        if queue_size > 0 then
            queue_size = queue_size - 1
        end
    end)
end

function manage:write_system_area_to_eep(fru_id, obj, fruinfo_manage)
    if fru_id ~= 0 then
        return
    end
    local offset = 0
    if fruinfo_manage[fru_id] then
        offset = fruinfo_manage[fru_id].SystemOffset
    else
        log:error('fruinfo_manage[%d] == nil', fru_id)
    end
    local data = frudata_intf.get_tc_system_area(fru_id)
    log:notice('SystemOffset = %d, EepromWp_state = %d, data_len = %d', offset, obj.EepromWp, #data)

    cs(function()
        if queue_size >= MAX_QUEUE_SIZE then
            log:notice('write system area to eep failed, queue is full. fru_id = %d', fru_id)
            return
        end
        queue_size = queue_size + 1

        log:notice('write system area to eep start: fru_id = %d', fru_id)
        obj.EepromWp = common.EEPROM_WRITE_PROTECT.CLOSE -- 关闭写保护
        obj.FruDev:Write(context.new(), offset, data)
        log:notice('write system area to eep end: fru_id = %d, data = [%s]', fru_id, data)
        obj.EepromWp = common.EEPROM_WRITE_PROTECT.OPEN -- 开启写保护

        if queue_size > 0 then
            queue_size = queue_size - 1
        end
    end)
end

manage.is_leap_year = is_leap_year
manage.get_days_in_month = get_days_in_month
manage.has_manufactured_field = has_manufactured_field
manage.update_mfg_date_by_manufactured = update_mfg_date_by_manufactured

return manage
