-- 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 log = require 'mc.logging'
local bios_enum = require 'bios.types.enums'
local bs = require 'mc.bitstring'
local s_pack = string.pack
local s_unpack = string.unpack
local prop_def = require "macros.property_def"
local json = require "cjson"
local resp_msg = require 'pojo.response_msg'
local vos = require 'utils.vos'
local file_sec = require 'utils.file'
local bios_factory = require 'factory.bios_factory'
local mc_initiator = require 'mc.initiator'

local CPLDRAM_CHAN_NUM = 13
local SYS_CHAN_NUM = 15

local base_util = {}

local E_OK = 1
local E_FAILED = -1

function base_util.judge_req(req)
    if not req then
        log:error("Param pointer is null!")
        local err_code = bios_enum.SmbiosErrCode.SMBIOS_ERR_INVALID_FIELD:value()
        return err_code
    end

    if not req.ManufactureId or not req.BiosId or not req.FileSelector or not req.Operation then
        log:error("judge_req: get_ipmi_field fail!")
        local err_code = bios_enum.SmbiosErrCode.SMBIOS_ERR_IV_LEN:value()
        return err_code
    end

    return nil
end

function base_util.judge_manu_id_valid(req, manu_id)
    if not manu_id then
        log:error("judge_manu_id_valid: manufacture_id is NULL!")
        return E_FAILED
    end

    if req.ManufactureId ~= manu_id then
        log:error("judge_manu_id_valid: manufacture_id:%s(ShouldBe:%s) is invalid!", req.ManufactureId, manu_id)
        return E_FAILED
    end

    return E_OK
end

function base_util.judge_bios_id(req, bios_id, multihost)
    if multihost then
        return
    end
    local req_bios_id = req.BiosId
    if req_bios_id > bios_id then
        log:error("BiosId:%d(MaxId:%d) is invalid!", req_bios_id, bios_id)
        local err_code = bios_enum.SmbiosErrCode.SMBIOS_ERR_INVALID_STATUS:value()
        return err_code
    end

    return nil
end

function base_util.validate(req, bios_id, manu_id, multihost)
    local err_code = base_util.judge_req(req)
    if err_code then
        return err_code
    end

    err_code = base_util.judge_bios_id(req, bios_id, multihost)
    if err_code then
        return err_code
    end

    local valid_flag = base_util.judge_manu_id_valid(req, manu_id)
    if valid_flag == E_FAILED then
        err_code = bios_enum.SmbiosErrCode.SMBIOS_ERR_INVALID_CMD:value()
        return err_code
    end

    return nil
end

function base_util.get_file_changed_judge_req(req)
    if not req then
        log:error("Param pointer is null!")
        local err_code = bios_enum.SmbiosErrCode.SMBIOS_ERR_INVALID_FIELD:value()
        return err_code
    end

    if not req.ManufactureId or not req.BiosId or not req.FileSelector then
        log:error("judge_req: get_ipmi_field fail!")
        local err_code = bios_enum.SmbiosErrCode.SMBIOS_ERR_IV_LEN:value()
        return err_code
    end

    return nil
end

function base_util.get_file_changed_validate(req, bios_id, manu_id, multihost)
    local err_code = base_util.get_file_changed_judge_req(req)
    if err_code then
        return err_code
    end
    err_code = base_util.judge_bios_id(req, bios_id, multihost)
    if err_code then
        return err_code
    end

    local valid_flag = base_util.judge_manu_id_valid(req, manu_id)
    if valid_flag == E_FAILED then
        err_code = bios_enum.SmbiosErrCode.SMBIOS_ERR_INVALID_CMD:value()
        return err_code
    end
end

function base_util.resolve_data(len, data)
    local bin_id = s_pack("I" .. len, data)
    local res = s_unpack(">I" .. len, bin_id)
    return res
end

function base_util.get_bios_wr_data(src_data)
    local bios_tail = bs.new('<<DataChecksum, OffsetToWrite:4/unit:8, FileData/string>>')
    local data = bios_tail:unpack(src_data)
    return data
end

function base_util.get_bios_rd_data(src_data)
    local bios_tail = bs.new('<<OffsetFromRead:4/unit:8, ReadLen>>')
    local data = bios_tail:unpack(src_data)
    return data
end

function base_util.get_finish_tail(src_data)
    local bios_tail = bs.new('<<DataChecksum, others/string>>')
    local info = bios_tail:unpack(src_data)
    return info
end

-- 类实例----按照format格式---->二进制流
function base_util.struct_to_binary(instance, format)
    local bs_format = bs.new(format)
    local binary = bs_format:pack(instance)
    return binary
end

-- 二进制流----按照format格式---->类实例
function base_util.binary_to_struct(src_data, format)
    local format_bs = bs.new(format)
    local instance = format_bs:unpack(src_data)
    return instance
end

function base_util.clear_data_operate_res(data_opt)
    if not data_opt then
        log:error("clear_data_operate_res: data_opt is NULL!")
        return
    end

    data_opt.data_flag = prop_def.BIOS_FLAG_IDLE
    data_opt.data_len = 0
    data_opt.data_offset = 0

    if data_opt.data_buf then
        data_opt.data_buf = nil
    end
end

function base_util.judge_write_file_selector_valid(file_selector)
    if file_selector == prop_def.BIOS_FILE_DISPLAY_NUM or file_selector == prop_def.BIOS_FILE_OPTION_NUM or
        file_selector == prop_def.BIOS_FILE_CHANGE_NUM or file_selector == prop_def.BIOS_FILE_CLP_RESP or
        file_selector == prop_def.BIOS_FILE_SETUP_NUM or file_selector == prop_def.BIOS_FILE_SMBIOS_RAS or
        file_selector == prop_def.BIOS_FILE_REGISTRY_NUM or file_selector == prop_def.BIOS_FILE_CURRENT_VALUE_NUM or
        file_selector == prop_def.BIOS_FILE_SETTING_NUM or file_selector == prop_def.BIOS_FILE_RESULT_NUM or
        file_selector == prop_def.BIOS_FILE_CMES_NUM or file_selector == prop_def.BIOS_FILE_CLP or
        file_selector == prop_def.BIOS_FILE_CURRENT_SECUREBOOT_NUM then
        return true
    elseif file_selector == prop_def.BIOS_FILE_DEBUG_INFO or file_selector == prop_def.BIOS_FILE_FIRMWARE or
        file_selector == prop_def.BIOS_FILE_NETCONFIG or file_selector == prop_def.BIOS_FILE_SOL or
        file_selector == prop_def.BIOS_FILE_BLACKBOX then
        return false
    else
        log:error("write file_index:%d is invalid!", file_selector)
        return false
    end
end

local READ_WHITE_LIST = {
    [prop_def.BIOS_FILE_SILK_CONFIG_NUM] = 1,
    [prop_def.BIOS_FILE_NEW_SECUREBOOT_NUM] = 1,
    [prop_def.BIOS_FILE_PSUINFO_NUM] = 1,
    [prop_def.BIOS_FILE_DISPLAY_NUM] = 1,
    [prop_def.BIOS_FILE_OPTION_NUM] = 1,
    [prop_def.BIOS_FILE_CHANGE_NUM] = 1,
    [prop_def.BIOS_FILE_CLP] = 1,
    [prop_def.BIOS_FILE_SETUP_NUM] = 1,
    [prop_def.BIOS_FILE_REGISTRY_NUM] = 1,
    [prop_def.BIOS_FILE_CURRENT_VALUE_NUM] = 1,
    [prop_def.BIOS_FILE_SETTING_NUM] = 1,
    [prop_def.BIOS_FILE_RESULT_NUM] = 1
}

function base_util.judge_read_file_selector_valid(file_selector)
    if READ_WHITE_LIST[file_selector] ~= nil then
        return true
    elseif file_selector == prop_def.BIOS_FILE_DEBUG_INFO or file_selector == prop_def.BIOS_FILE_FIRMWARE or
        file_selector == prop_def.BIOS_FILE_NETCONFIG or file_selector == prop_def.BIOS_FILE_SOL or
        file_selector == prop_def.BIOS_FILE_BLACKBOX or file_selector == prop_def.BIOS_FILE_CLP_RESP or
        file_selector == prop_def.BIOS_FILE_CMES_NUM then
        return false
    else
        log:error("read file_index:%d is invalid!", file_selector)
        return false
    end
end

function base_util.get_file_json(file_path)
    if not file_path then
        log:debug("get_file_json: file_path is nil")
        return nil
    end

    local file, err = file_sec.open_s(file_path, "rb")
    if not file then
        log:debug("get_file_json: open %s file fail!, err: %s", file_path, err)
        return nil
    end

    local file_string = file:read("*a")
    if not file_string or #file_string == 0 or #file_string == 1 then
        file:close()
        log:debug("get_file_json: json len is 0!")
        return nil
    end

    local ok, file_jso = pcall(function()
        return json.decode(file_string)
    end)
    file:close()
    if not ok then
        return nil
    end
    return file_jso
end

function base_util.get_file_order_json(file_path)
    local file_string = base_util.get_file_data(file_path)
    if not file_string or #file_string == 0 or #file_string == 1 then
        log:debug("get_file_json: json len is 0!")
        return nil
    end

    return json.json_object_ordered_decode(file_string)
end

function base_util.write_file_data(file_path, file_json)
    if not file_path then
        log:error("write_file_json: file_path is nil")
        return E_FAILED
    end

    local file, err = file_sec.open_s(file_path, "w+b")
    if not file then
        log:debug("write_file_json: open %s file fail! err: %s", file_path, err)
        return E_FAILED
    end

    file:write(file_json)
    file:flush()
    file:close()
    return E_OK
end

function base_util.get_file_data(file_path)
    if not file_path then
        return nil
    end

    local file = file_sec.open_s(file_path, "rb")
    if not file then
        return nil
    end

    local file_string = file:read("*a")
    file:close()
    return file_string
end

function base_util.filter_msg_by_sys_channel(channel_num)
    if channel_num == CPLDRAM_CHAN_NUM or channel_num == SYS_CHAN_NUM then
        return true
    end

    return false
end

function base_util.is_file_exist(file_path)
    local file = file_sec.open_s(file_path, "r")
    if file then
        io.close(file)
        return true
    end
    return false
end

function base_util.calc_checksum(str)
    if type(str) ~= "string" then
        return
    end
    local arr = 0
    for i = 1, #str do
        arr = arr + string.sub(str, i, i):byte()
    end
    return arr & 0xFF
end

-- 表的深拷贝函数
function base_util.clone(obj)
    local lookup_table = {}
    local function copy(object)
        if type(object) ~= "table" then
            return object
        elseif lookup_table[object] then
            return lookup_table[object]
        end
        local new_table = {}
        lookup_table[object] = new_table
        for key, value in pairs(object) do
            new_table[copy(key)] = copy(value)
        end
        return setmetatable(new_table, getmetatable(object))
    end
    return copy(obj)
end

function base_util.get_tbl_len(tbl)
    if not tbl or type(tbl) ~= 'table' then
        return 0
    end
    local count = 0
    for _, _ in pairs(tbl) do
        count = count + 1
    end
    return count
end

function base_util.err_msg(err_code, data)
    return resp_msg.new(err_code, data)
end

function base_util.get_valid_version(version)
    local valid_version
    local v1, v2 = string.match(version, '(%d+).(%d+)')
    if not v1 or not v2 then
        return nil
    end

    valid_version = v1 .. '.' .. v2
    return valid_version
end

function base_util.cal_table_len(tbl)
    if not tbl then
        return 0
    end

    local len = 0
    for _, _ in pairs(tbl) do
        len = len + 1
    end

    return len
end

function base_util.unit_arr_to_bin(arr_data)
    if not arr_data or not arr_data[0] then
        return ''
    end

    local text = {}
    local len = #arr_data
    for i = 0, len, 1 do
        local bin = s_pack("I1", arr_data[i])
        text[i + 1] = bin
    end

    return table.concat(text)
end

function base_util.new_file(path)
    if vos.get_file_accessible(path) then
        return
    end

    local file = file_sec.open_s(path, "w+b")
    if file then
        file:close()
    end
    return
end

-- 校验特殊字符,包含可能导致shell命令注入的特殊字符
function base_util.check_special_character(path)
    local special_char = {
        [("\n"):byte()] = 1, [(";"):byte()] = 1, [("&"):byte()] = 1, [("$"):byte()] = 1,
        [("|"):byte()] = 1, [(">"):byte()] = 1, [("<"):byte()] = 1, [("`"):byte()] = 1,
        [("!"):byte()] = 1
    }
    local res = string.match(path, "\\\\")
    if res then
        return true
    end

    for i = 1, #path do
        local byte = string.sub(path, i, i):byte()
        if special_char[byte] then
            return true
        end
    end

    return false
end

-- 获取文件路径
function base_util.strip_file_path(path)
    return string.match(path, "(.+)/[^/]*%.%w+$")
end

-- 获取文件名
function base_util.strip_file_name(path)
    return string.match(path, ".+/([^/]*%.%w+)$")
end

-- 获取文件后缀名
function base_util.get_extension_name(path)
    return string.match(path, ".+%.(%w+)$")
end

-- 校验文件路径
function base_util.check_path(path)
    if not path or #path == 0 then
        return false
    end

    -- 校验tmp目录
    if not string.match(path, "/tmp/.*") then
        return false
    end

    -- 校验特殊字符,防止命令注入
    if base_util.check_special_character(path) then
        return false
    end

    -- 软连接判断
    if file_sec.check_realpath_before_open_s(path) == -1 then
        return false
    end

    return true
end

function base_util.get_bin_period(cfg_list)
    if not cfg_list or not cfg_list[1] then
        return prop_def.BIOS_PERIOD2
    end

    local cfg = cfg_list[1]
    if not cfg.idex then
        return prop_def.BIOS_PERIOD2
    end

    if cfg.idex == prop_def.KUNPENGBEXID then
        return prop_def.BIOS_PERIOD3
    end

    return prop_def.BIOS_PERIOD2
end

function base_util.clear_file(path)
    if not vos.get_file_accessible(path) then
        return false
    end

    local file = file_sec.open_s(path, "w+b")
    if file then
        file:close()
    end
    return true
end

function base_util.safe_get_file_json(file_path)
    if not file_path then
        log:info("safe_get_file_json: file_path is nil")
        return nil
    end

    local file, err = file_sec.open_s(file_path, "rb")
    if not file then
        log:debug("safe_get_file_json: open %s file fail!, err: %s", file_path, err)
        return nil
    end

    local file_string = file:read("*a")
    if not file_string or #file_string == 0 then
        file:close()
        log:info("safe_get_file_json: json len is 0!")
        return {}
    end

    local ok, file_jso = pcall(function()
        return json.decode(file_string)
    end)
    if not ok then
        log:error("safe_get_file_json: decode data fail!")
        file:close()
        return nil
    end
    file:close()
    return file_jso
end

function base_util.get_system_id(ctx)
    if not ctx then
        return 1
    end
    return ctx.HostId or 1
end

function base_util.get_obj_id(path)
    return tonumber(string.match(path, '/bmc/kepler/Systems/(%d+)'))
end

local bdf_props = {
    OCPCardBDF = 1,
    PcieCardBDF = 1,
    PcieDiskBDF = 1
}

function base_util.dump_prop(prop, val)
    if type(val) == 'table' then
        local log_val = ""
        for _, v in pairs(val) do
            log_val = log_val .. tostring(v) .. " "
        end
        return log_val
    end

    if bdf_props[prop] and type(val) == 'string' then
        local info = ""
        local data
        for i = 1, #val do
            data = s_unpack('B', string.sub(val, i, i))
            info = info .. tostring(data) .. ' '
        end
        return info
    end
    return val
end

function base_util.get_data_path(system_id, file_name)
    if system_id == prop_def.DEFAULT_SYSTEM_ID then
        return prop_def.BIOS_DATA_PATH .. '/' .. file_name
    end
    return prop_def.BIOS_CONFIG_PATH .. '/' .. system_id .. '/'.. file_name
end

function base_util.get_conf_path(system_id, file_name)
    if system_id == prop_def.DEFAULT_SYSTEM_ID then
        return prop_def.BIOS_CONFIG_PATH .. '/' .. file_name
    end
    return prop_def.BIOS_CONFIG_PATH .. '/' .. system_id .. '/'.. file_name
end

function base_util.get_bios_cached_path(system_id)
    if system_id == prop_def.DEFAULT_SYSTEM_ID then
        return prop_def.UPGRADE_PATH .. '/' .. prop_def.CACHED_BIOS_PKG
    else
        return prop_def.UPGRADE_PATH .. '/' .. system_id .. '/' .. prop_def.CACHED_BIOS_PKG
    end
end

function base_util.get_bios_cached_dir(system_id)
    if system_id == prop_def.DEFAULT_SYSTEM_ID then
        return prop_def.UPGRADE_PATH
    else
        return prop_def.UPGRADE_PATH .. '/' .. system_id
    end
end

function base_util.get_krun_cached_path(krun_id)
    if krun_id == prop_def.DEFAULT_SYSTEM_ID then
        return prop_def.UPGRADE_PATH .. '/' .. prop_def.CACHED_KRUN_PKG
    else
        return prop_def.UPGRADE_PATH .. '/' .. krun_id .. '/' .. prop_def.CACHED_KRUN_PKG
    end
end

function base_util.get_krun_cached_dir(krun_id)
    if krun_id == prop_def.DEFAULT_SYSTEM_ID then
        return prop_def.UPGRADE_PATH
    else
        return prop_def.UPGRADE_PATH .. '/' .. krun_id
    end
end

function base_util.record_operation(ctx, system_id, msg)
    local op_initiator
    if ctx and ctx.has_initiator and ctx:has_initiator() then
        op_initiator = ctx:get_initiator()
    else
        op_initiator = mc_initiator.new('N/A', 'N/A', 'localhost')
    end
    local bios_ser = bios_factory.get_service('bios_service')
    if bios_ser and bios_ser:is_multihost() then
        log:system(system_id):operation(op_initiator, 'BIOS', msg)
    else
        log:operation(op_initiator, 'BIOS', msg)
    end
end

return base_util