-- Copyright (c) 2025 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 log = require 'mc.logging'
local bs = require 'mc.bitstring'
local ipmi = require 'ipmi'
local skynet = require 'skynet'
local comp_code = ipmi.types.Cc
local enums = require 'ipmi.enums'
local channel_type = enums.ChannelType
local c_optical_channel = require 'device.class.optical_channel'
local om_config = require 'hardware_config.OpticalModuleConfig'

local IDENTIFIER_DEFAULT<const> = 0x00
local INVALID_READING<const> = 0xFFFF
local ALARM_VALUE<const> = 0x8000

local optical_module_cpu = {}

local function send_ipmi_request(orm_obj, req_data)
    local err, rsp = ipmi.request(orm_obj:get_bus(), channel_type.CT_ME:value(),
        {DestNetFn = 0x30, Cmd = 0x98, Payload = req_data})

    if err ~= comp_code.Success then
        return err, nil
    end
    return err, rsp
end

local function sfp_read_eeprom_offset(orm_obj, offset, length)
    return pcall(function()
        if length == 0 then
            error('sfp_read_eeprom_offset size is 0, offset: %d', offset)
        end

        local sfp_format = bs.new('<<0xDB0700:3/unit:8, 0x22, chip_id, pfid, offset:2/unit:8, length>>')
        local req_data = sfp_format:pack({chip_id = orm_obj.CpuId, pfid = orm_obj.PfID,
            offset = offset, length = length})
        local cc, payload
        for _ = 1, 3 do
            cc, payload = send_ipmi_request(orm_obj, req_data)
            if cc == comp_code.Success then
                break
            end
            orm_obj.tasks:sleep_ms(1000)
        end
        if cc ~= comp_code.Success then
            error("ipmi get eeprom data failed, complete code: %s", cc)
        end

        local head = string.sub(payload, 1, 5)
        local rsp_format = bs.new('<<0xDB0700:3/unit:8, Status:1/unit:8, 0x5a>>')
        local rsp = rsp_format:unpack(head)
        local data = string.sub(payload, 6, -1)

        if rsp and rsp.Status == 0x00 then
            orm_obj.Presence = 1
        else
            log:info("optical module not supported")
        end

        return data
    end)
end

-- 获取光模块动态资源，返回数字，可以用掩码
local function get_optical_module_diagnostic(orm_obj, v)
    local offset = v.offset
    local length = v.length
    if not offset or not length then
        return
    end
    local ok, rsp = sfp_read_eeprom_offset(orm_obj, offset, length)
    if not ok then
        if log:getLevel() >= log.DEBUG then
            log:debug('get optical module asset failed, offset: %d, length: %d, error: %s', offset, length, rsp)
        end
        return
    end
    local ret_val = length > 1 and string.unpack('>H', rsp) or string.byte(rsp)
    local mask = v.mask
    if not ret_val then
        return
    end
    if mask then
        ret_val = ret_val & mask
    end
    if v.handle_func then
        return v.handle_func(ret_val)
    end

    return ret_val
end

local function get_optical_module_identifier(orm_obj)
    local identifier = get_optical_module_diagnostic(orm_obj, {offset = 0, length = 1})
    if identifier then
        return true, identifier
    end
    return nil
end

-- 获取光模块静态资源，返回字符串
local function get_optical_module_asset(orm_obj, v)
    local offset = v.offset
    local length = v.length
    if not offset or not length then
        return
    end
    local ok, rsp = sfp_read_eeprom_offset(orm_obj, offset, length)
    if not ok then
        if log:getLevel() >= log.DEBUG then
            log:debug('get optical module asset failed, offset: %d, length: %d, error: %s', offset, length, rsp)
        end
        return
    end

    if rsp == '' then
        return
    end
    local ret_val = string.gsub(rsp, "[ ]+$", "")
    if not ret_val then
        return
    end
    if v.handle_func then
        return v.handle_func(ret_val)
    end

    return ret_val
end

local function get_optical_module_static_info(orm_obj, identifier)
    if identifier == IDENTIFIER_DEFAULT or not om_config.static_info_list[identifier] then
        return
    end
    for k, v in pairs(om_config.static_info_list[identifier]) do
        skynet.sleep(150)  -- 等待1.5s减少cpu占用
        local set_prop = get_optical_module_asset(orm_obj, v)
        orm_obj[k] = set_prop or orm_obj[k]
    end
end


local function check_validity(input)
    return input ~= INVALID_READING and input ~= 0
end

local function update_channel_prop(orm_obj, v, channel_num, vv)
    local prop_name = vv.alias
    local set_prop = get_optical_module_diagnostic(orm_obj, vv)
    local upper = orm_obj[v.UpperThreshold] or 0
    local lower = orm_obj[v.LowerThreshold] or 0
    local alarm_val = 0
    if check_validity(upper) and check_validity(set_prop) and set_prop > upper then
        alarm_val = ALARM_VALUE
    elseif check_validity(lower) and check_validity(set_prop) and set_prop < lower then
        alarm_val = ALARM_VALUE
    end
    if not c_optical_channel.collection.objects then
        return
    end
    for _, channel in ipairs(c_optical_channel.collection.objects) do
        local om_obj = channel:get_parent()
        local channel_id = channel.Id
        if channel_id == channel_num and om_obj and om_obj.name == orm_obj.name then
            channel:update_prop_single(prop_name, set_prop, alarm_val)
        end
    end
    return set_prop
end

local function update_diag_prop_single(orm_obj, prop_name, v)
    local set_prop = get_optical_module_diagnostic(orm_obj, v)
    if set_prop ~= nil then
        orm_obj[prop_name] = set_prop
    end
end

local function update_diag_prop_muti(orm_obj, muti_props)
    for prop_name, set_prop in pairs(muti_props) do
        if set_prop ~= nil and next(set_prop) then
            orm_obj[prop_name] = set_prop
        end
    end
end

local function get_optical_module_diagnostic_info(orm_obj, identifier)
    if identifier == IDENTIFIER_DEFAULT or not om_config.diagnostic_info_list[identifier] then
        return
    end
    local muti_props = {}
    for k, v in pairs(om_config.diagnostic_info_list[identifier]) do
        skynet.sleep(150)  -- 等待1.5s减少cpu占用
        muti_props[k] = {}
        for channel, vv in pairs(v) do
            if type(vv) == 'table' then
                local channel_num = string.match(channel,'Channel(%d)') or ''
                local set_prop = update_channel_prop(orm_obj, v, tonumber(channel_num), vv)
                muti_props[k][tonumber(channel_num)] = set_prop
            else
                update_diag_prop_single(k, v)
            end
        end
    end
    update_diag_prop_muti(muti_props)
end

-- 下电或IMU异常获取不到信息，要恢复默认值
local function reset_diagnostic_info(orm_obj)
    for prop_name, v in pairs(om_config.diagnostic_info_list[IDENTIFIER_DEFAULT]) do
        orm_obj[prop_name] = v.default
    end
    if not c_optical_channel.collection.objects then
        return
    end
    for _, channel in ipairs(c_optical_channel.collection.objects) do
        local parent = channel:get_parent()
        if parent.name == orm_obj.name then
            channel:reset_diagnostic_info()
        end
    end
end

function optical_module_cpu.start_get_optical_info_task(orm_obj)
    local card = orm_obj.parent.parent
    -- 仅CPU直出光模块通过IMU获取
    if not card or card.Type ~= 1 then
        return
    end
    local retry_times = 0
    while not card or card.SocketId == '' and retry_times < 10 do
        retry_times = retry_times + 1
        orm_obj.tasks:sleep_ms(1000)
    end
    orm_obj.tasks:new_task(
        'get optical module asset task ' .. tostring(orm_obj.NetworkAdapterId) .. '-' ..
        tostring(orm_obj.PortID)):loop(function()
        orm_obj.CpuId = card.SocketId
        -- 兼容老版本PfID为默认时采用PortID
        orm_obj.PfID = orm_obj.PfID ~= 255 and orm_obj.PfID or orm_obj.PortID
        local ok, identifier = get_optical_module_identifier()
        if  ok then
            get_optical_module_static_info(orm_obj, identifier)
            get_optical_module_diagnostic_info(orm_obj, identifier)
        else
            reset_diagnostic_info(orm_obj)
        end
    -- 每5s更新光模块信息
    end):set_timeout_ms(5000)
end

return optical_module_cpu