-- 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.
--
-- Description: 采集board信息插件

local cjson = require 'cjson'
local log = require 'mc.logging'
local gsub = string.gsub

local DRIVE_PROTOCOL = {
    UNKNOWN = 0,
    PARALLEL_SCSI = 1,
    SAS = 2,
    SATA = 3,
    FIBER_CHANNEL = 4,
    SATA_OR_SAS = 5,
    PCIE = 6
}

local SASSATASSD_DEST_PROTO = {
    [DRIVE_PROTOCOL.SAS] = true,
    [DRIVE_PROTOCOL.SATA] = true,
    [DRIVE_PROTOCOL.SATA_OR_SAS] = true
}

local m = {}

function m.format_mainboard_version(BMCVersion, BiosVersion)
    local bmc = BMCVersion or 'N/A'
    local bios = BiosVersion or 'N/A'
    return "BMCVersion:" .. bmc .. ";BiosVersion:" .. bios
end

function m.get_mainboard_pn(content)
    if not content or #content == 0 then
        log:error('content fail')
        return "N/A"
    end

    local is_ok, json_obj = pcall(cjson.decode, content)
    if not is_ok then
        return "N/A"
    end
    
    local mainboard = json_obj.Motherboard
    if not mainboard then
        return "N/A"
    end

    local ProductName = mainboard.ProductName
    if not ProductName then
        return "N/A"
    end
    return ProductName
end

function m.get_sp_drive_info(content, name, location, field)
    if not content or #content == 0 then
        log:error('content fail')
        return "N/A"
    end

    local is_ok, json_obj = pcall(cjson.decode, content)
    if not is_ok then
        return "N/A"
    end

    local drives = json_obj.Drives
    if not drives then
        return "N/A"
    end

    local target_location = location or ""
    target_location = target_location:gsub("%s+", "")
    local target_position = target_location .. name

    for _, drive in pairs(drives) do
        local position = drive.Position
        if position == target_position then
            return drive[field] or "N/A"
        end
    end
    return "N/A"
end

function m.check_drive_type(type, Destination, content)
    if Destination.Presence == 0 then
        return false
    end

    local MediaType = m.get_sp_drive_info(content, Destination.Name, Destination.Location, 'MediaType')
    local Protocol = m.get_sp_drive_info(content, Destination.Name, Destination.Location, 'Protocol')

    if type == "HDD" then
        if MediaType == type or Destination.MediaType == 0 then
            return true
        end
        return false
    end

    if type == "SSD" then
        if MediaType == type or Destination.MediaType == 1 then
            return true
        end
        return false
    end

    if type == "SASSATASSD" then
        if MediaType == "SSD" or Destination.MediaType == 1 then
            if Protocol == "SATA" or Protocol == "SAS" or SASSATASSD_DEST_PROTO[Destination.Protocol] then
                return true
            end
        end
        return false
    end

    if type == "NVMeSSD" then
        if MediaType == "SSD" or Destination.MediaType == 1 then
            if Destination.Protocol == DRIVE_PROTOCOL.PCIE then
                return true
            end
        end
        return false
    end
    return false
end

local function get_mac(target_card)
    local mac = {}
    for _, Controller in pairs(target_card.Controllers or {}) do
        for _, func in pairs(Controller.Functions or {}) do
            if func.MacAddress then
                table.insert(mac, func.MacAddress)
            end
        end
    end
    return mac
end

local function get_info_by_field(field, target_card)
    if field == 'MacAddress' then
        local mac = get_mac(target_card)
        return #mac ~= 0 and table.concat(mac, ";") or "N/A"
    elseif field == 'FirmwareVersion' or field == 'Manufacturer' then
        if target_card.Controllers[1] then
            local result = target_card.Controllers[1][field] or "N/A"
            return #result > 0 and result or "N/A"
        end
    elseif field == 'DeviceId' then
        if target_card.Controllers[1] then
            local func = target_card.Controllers[1].Functions
            if func and func[1] then
                return func[1].DeviceId or "N/A"
            end
        end
    elseif field == 'SysId' then
        if target_card.Controllers[1] then
            local func = target_card.Controllers[1].Functions
            if func and func[1] then
                local subvid = func[1].SubsystemVendorId or ''
                local subdid = func[1].SubsystemId or ''
                local result = gsub(subvid, "^0[xX]", "") .. ' ' .. gsub(subdid, "^0[xX]", "")
                return result or "N/A"
            end
        end
    -- 配置字段里面有N/A和""，""是后面的业务需要处理的，而null这是deviceinfo.json的采集结果，需要转换成N/A
    -- 以下分支处理的是不为null的时候，需要输出target_card[field]，但如果target_card[field] == nil时也是"N/A"
    -- 这函数所有分支都匹配不上的，就默认"N/A"
    elseif target_card[field] ~= null then
        return target_card[field] or "N/A"
    end
    return "N/A"
end

function m.get_sp_pciecard_info(content, name, field)
    if not content or #content == 0 then
        log:error('content fail')
        return "N/A"
    end

    local is_ok, json_obj = pcall(cjson.decode, content)
    if not is_ok then
        return "N/A"
    end
    local PCIeCards = json_obj.PCIeCards
    if not PCIeCards then
        return "N/A"
    end

    local target_card = nil
    for _, PCIeCard in pairs(PCIeCards) do
        local position = PCIeCard.Position
        if position == name then
            target_card = PCIeCard
            break
        end
    end

    if not target_card then
        return "N/A"
    end

    return get_info_by_field(field, target_card) or "N/A"
end

function m.format_num(input)
    local result = {}
    for _, obj in pairs(input or {}) do
        local link = obj.Link
        local num = link:match('^%d+') or '1'
        table.insert(result, num)
    end
    return #result ~= 0 and table.concat(result, ',') or 'N/A'
end

function m.format_raid_level(input)
    local result = {}
    for _, obj in pairs(input or {}) do
        local num = obj:match('%d+')
        if num then
            table.insert(result, num)
        end
    end
    return #result ~= 0 and 'RAID(' .. table.concat(result, '/') .. ')' or 'N/A'
end

local hba_card_type_id = {
    LSI_3108_WITH_IT = 2,
    LSI_3008_WITH_IT = 5,
    LSI_3408_WITH_IT = 10,
    LSI_3416_WITH_IT = 11,
    HI1880_SP186_M_16i = 96,
    HI1880_SP186_M_32i = 97,
    HI1880_SP186_M_40i = 95,
    HI1880_SPR120 = 104,
    HI1880_SPR130 = 105,
    HI1880_SPR140 = 106,
    HI1880_SPR110 = 112,
    HI1880_SP186_M_8i = 113
}

function m.check_raid_type(destination, cardtype)
    if not destination or not cardtype then
        return false
    end

    local is_hba = 0
    for _, id in pairs(hba_card_type_id) do
        if id == destination.TypeId then
            is_hba = 1
            break
        end
    end

    if cardtype == "HBA" and (is_hba == 1 or destination.WorkMode == 'HBA') then
        return true
    end
    if cardtype == "RAID" and is_hba == 0 then
        return true
    end
    return false
end

function m.add_common_info(input, common)
    for i = 1, #input do
        input[i].ASSET_NUMBER = #common.AssetTag > 0 and common.AssetTag or 'N/A'
        input[i].F_BARCODE = #common.ProductSerialNumber > 0 and common.ProductSerialNumber or 'N/A'
        input[i].MANUFACTURER = #common.ManufacturerName > 0 and common.ManufacturerName or 'N/A'
    end
    return input
end

function m.is_deviceinfo_valid(content)
    if not content then
        log:error("deviceinfo unvalid")
        return false
    end
    return true
end

function m.get_device_info_rsp(content)
    if not content or #content == 0 then
        return false
    end

    local is_ok = pcall(cjson.json_object_ordered_decode, content)
    if not is_ok then
        log:error("Decode content failed")
        return false
    end

    return true
end

return m