-- 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 cjson = require('cjson')
local mdb_service = require('mc.mdb.mdb_service')
local gsub = string.gsub
local utils = require('mc.utils')
local utils_core = require('utils.core')
local file_sec = require('utils.file')
local safe_close = utils.safe_close_file

local skynet = require('skynet')

local m = {}

function m.trim(s)
    return gsub(gsub(s, '^%s+', ''), '%s+$', '')
end

function m.get_valid_string_property(val)
    if val == '' or val == 'N/A' or val == 'NA' or val == 'Unknown' or val == 'null' then
        return null
    end

    return val
end

function m.is_valid_managers_id(id)
    local path = '/bmc/kepler/Managers/' .. id
    local ok, rsp = pcall(mdb_service.is_valid_path, bus, path)
    if not ok then
        log:error('is_valid_managers_id failed, err(%s)', rsp.message)
        error(rsp)
    end

    return rsp.Result
end

function m.is_valid_certificate_id(manager_id, cert_id, certificates_list)
    local path
    if not certificates_list or certificates_list == nil then
        return false
    end
    if cert_id == 'InterChassisAuthCertificate' then
        local tmp_cert_id
        if #certificates_list == 0 then
            return false
        end
        for _, v in ipairs(certificates_list) do
            tmp_cert_id = tonumber(string.match(v, 'InterChassisAuth/Certificates/(%d+)'))
            if tmp_cert_id == 2 then
                break
            end
        end
        path = '/bmc/kepler/Managers/' .. manager_id .. '/InterChassisAuth/Certificates/' .. tmp_cert_id
    else
        path = '/bmc/kepler/Managers/' .. manager_id .. '/NetworkProtocol/HTTPS/Certificates/' .. cert_id
    end
    local ok, rsp = pcall(mdb_service.is_valid_path, bus, path)
    if not ok then
        log:error('is_valid_managers_id failed, err(%s)', rsp.message)
        error(rsp)
    end

    return rsp.Result
end

function m.is_valid_system_id(id)
    local path = '/bmc/kepler/Systems/' .. id

    local ok, rsp = pcall(mdb_service.is_valid_path, bus, path)
    if not ok then
        log:error('Invalid SystemId(%s), err(%s)', id, rsp.message)
        error(rsp)
    end

    return rsp.Result
end

function m.get_bios(id)
    local path = '/bmc/kepler/Systems/' .. id .. '/Bios'

    local ok, rsp = pcall(mdb_service.is_valid_path, bus, path)
    if not ok or not rsp.Result then
        return lua_nil
    end

    local res = {}
    res['@odata.id'] = '/redfish/v1/Systems/' .. id .. '/Bios'
    return res
end

function m.is_valid_chassis_id(id)
    local path = '/bmc/kepler/Chassis/' .. id

    local ok, rsp = pcall(mdb_service.is_valid_path, bus, path)
    if not ok then
        log:error('Invalid Chassis(%s), err(%s)', id, rsp.message)
        error(rsp)
    end

    return rsp.Result
end

function m.is_valid_ssu_chassis_id(id)
    local dev_path = '/bmc/dev'
    local dev_interface = 'bmc.dev.Chassis'

    local ok, rsp = pcall(mdb_service.get_sub_objects, bus, dev_path, 4, { dev_interface })
    if not ok then
        log:error('Invalid Chassis(%s), err(%s)', id, rsp.message)
        error(rsp)
    end
    local expansion_objs = rsp.SubObjects
    for path, v in pairs(expansion_objs) do
        ok, rsp = pcall(mdb.get_object, bus, path, dev_interface)
        if not ok then
            goto continue
        end
        if rsp.Id == 'SSU' .. id then
            return true
        end
        ::continue::
    end
    return false
end

function m.is_valid_path(path, ignore_case)
    local ok, rsp = pcall(mdb_service.is_valid_path, bus, path, ignore_case or false)
    if not ok then
        log:info('Invalid Path, err(%s)', rsp.message)
        error(rsp)
    end

    return rsp.Result
end

function m.cstr_to_luastr(cstr)
    -- 从C获取的字符串是以\0结束的,需要以非贪婪模式获取\0前的字符
    local s = cstr or ''
    return string.match(s, '(.-)\0') or s
end

function m.is_path_exist(interface, querys)
    local params = { interface, cjson.encode(querys), true }

    local ok, rsp = pcall(mdb_service.get_path, bus, table.unpack(params))
    if not ok then
        return false
    end

    return rsp.Path ~= ''
end

function m.is_valid_board_id(paths, id)
    for _, v in ipairs(paths) do
        local ok, obj = pcall(mdb.get_object, bus, v, 'bmc.kepler.Systems.Board')
        if not ok then
            return false
        end
        local nodeid = gsub(obj.NodeId, '%s+', '')
        if string.lower(nodeid) == string.lower(id) then
            return true
        end
    end
    return false
end

function m.get_status(health, state, is_oem)
    if health == 255 then
        return null
    end

    local status = {}
    status.State = state or null

    local health_severity = {
        [0] = { 'OK', 'Informational' },
        [1] = { 'Warning', 'Minor' },
        [2] = { 'Warning', 'Major' },
        [3] = { 'Critical', 'Critical' },
    }

    local hs = health_severity[health]
    status.Health = hs and hs[1] or null

    if is_oem then
        status.Oem = {}
        status.Oem['{{OemIdentifier}}'] = {}
        status.Oem['{{OemIdentifier}}'].Severity = hs and hs[2] or null
    else
        status.Severity = hs and hs[2] or null
    end

    return status
end

function m.get_associated_resource(protocol, resource_id)
    if protocol ~= 6 then
        return null
    end

    return string.format('CPU%s', resource_id)
end

function m.is_valid_bios_attr_id(id)
    id = string.lower(id)
    local obj = mdb.get_object(bus, '/bmc/kepler/Systems/1/Bios', 'bmc.kepler.Systems.Bios')
    local version = obj.RegistryVersion
    local a, b, c = string.match(version, 'V(%d).(%d)(%d)')
    if a ~= nil and b ~= nil and c ~= nil then
        return id == string.format('v%s_%s_%s', a, b, c)
    else
        return false
    end
end

function m.is_valid_bios_attrjson_id(id)
    id = string.lower(id)
    local obj = mdb.get_object(bus, '/bmc/kepler/Systems/1/Bios', 'bmc.kepler.Systems.Bios')
    local version = obj.RegistryVersion
    local a, b, c = string.match(version, 'V(%d).(%d)(%d)')
    if a ~= nil and b ~= nil and c ~= nil then
        return id == string.format('v%s_%s_%s.json', a, b, c)
    else
        return false
    end
end

function m.is_valid_event_id(id)
    id = string.lower(id)
    local obj = mdb.get_object(bus, '/bmc/kepler/Systems/1/Events', 'bmc.kepler.Systems.Events')
    local major_version = obj.MajorVersion
    local minor_version = obj.MinorVersion
    local aux_version = obj.AuxVersion
    if major_version and minor_version and aux_version then
        return id == string.format('v%s_%s_%s', major_version, minor_version, aux_version)
    else
        return false
    end
end

function m.is_valid_event_message_id(id)
    local obj = mdb.get_object(bus, '/bmc/kepler/Systems/1/Events', 'bmc.kepler.Systems.Events')
    local major_version = obj.MajorVersion
    local minor_version = obj.MinorVersion
    local aux_version = obj.AuxVersion
    if major_version and minor_version and aux_version then
        return id == string.format('v%s_%s_%s.json', major_version, minor_version, aux_version)
    else
        return false
    end
end

function m.unique_array(arr)
    local set = {}
    local res = {}
    for _, v in ipairs(arr) do
        if not set[v] then
            res[#res + 1] = v
            set[v] = true
        end
    end
    return res
end

function m.split_string(str, delimiter)
    return utils.split(str, delimiter)
end

-- 将二维数组扁平化为一维数组
function m.flatten_2d_table(t)
    local ok, ret = pcall(function()
        local ret = {}
        for _, v in ipairs(t) do
            for _, vv in ipairs(v) do
                ret[#ret + 1] = vv
            end
        end
        return ret
    end)
    if not ok then
        log:error('Flatten table failed, err=%s', tostring(ret))
        return {}
    end
    return ret
end

-- 去掉首字母转换成数字
function m.get_valid_lane(val)
    if val == '' or val == 'N/A' or val == 'NA' or val == 'Unknown' or val == 'null' then
        return null
    end

    return tonumber(string.sub(val, 2))
end

-- 判断字符串是否以特定的字符为后缀
function m.ends_with(str, ending)
    return str:sub(-#ending) == ending
end

function m.get_res_file_content(res_path, file_type)
    res_path = string.lower(res_path)
    if not utils_core.is_file(res_path) then
        return
    end

    -- 读取json或xml文件并解析
    local file = file_sec.open_s(res_path, 'r')
    if not file then
        return
    end
    local content = safe_close(file, function()
        return file:read('*all')
    end)

    if file_type ~= 'json' then
        return content
    end

    local is_ok, json_obj = pcall(cjson.json_object_ordered_decode, content)
    if not is_ok then
        json_obj = cjson.json_object_new_object()
    end

    return json_obj
end

function m.get_schema_file(file_name, file_type)
    file_name = string.lower(file_name)
    file_type = string.lower(file_type)

    local file_dir = 'default_dir'
    if file_type == 'xml' then
        file_dir = 'csdl'
    elseif file_type == 'json' then
        file_dir = 'json_schema'
    end

    local RESOURCE_PREFIX = '/data/opt/bmc/redfish/static_resource'
    local STANDARD_PREFIX = '/redfish/v1/schemastore/en/dmtf/' .. file_dir
    local CUSTOM_PREFIX = '/redfish/v1/schemastore/en/oem/openubmc/' .. file_dir

    local res_path = RESOURCE_PREFIX .. STANDARD_PREFIX .. '/' .. file_name
    if utils_core.is_file(res_path) then
        return m.get_res_file_content(res_path, file_type)
    else
        res_path = RESOURCE_PREFIX .. CUSTOM_PREFIX .. '/' .. file_name
        if utils_core.is_file(res_path) then
            return m.get_res_file_content(res_path, file_type)
        end
    end
end

function m.get_content_from_tpl(path, file_type)
    local RESOURCE_PREFIX = skynet.getenv('RESOURCE_PREFIX') or '/opt/bmc/apps/redfish/interface_config/static_resource'
    local RESOURCE_SUFFIX = file_type == 'json' and '/index.json' or '/index.xml'
    -- 请求url不区分大小写 全部转换成小写防止获取资源失败
    path = string.lower(path)
    -- 部分路径以.json结尾 此时不需要拼接后缀
    local suffix = m.ends_with(path, '.json') and '' or RESOURCE_SUFFIX
    -- 拼接资源路径
    local res_path = RESOURCE_PREFIX .. path .. suffix

    return m.get_res_file_content(res_path, file_type)
end

function m.get_xml_content(path)
    path = path:lower()
    if path:find('%$metadata') then
        path = path:gsub('%$', '')
    end
    return m.get_content_from_tpl(path, 'xml')
end

-- 读取对应路径下的index.json文件并解析
function m.get_json_content(path, pattern)
    local content = m.get_content_from_tpl(path, 'json')
    if pattern and not content then
        local pattern_tmp = string.gsub(pattern, ':([a-z]+)', 'template') -- 将url中:id这类模式匹配的字符串替换成template
        content = m.get_content_from_tpl(pattern_tmp, 'json')
        if not content then
            pattern_tmp = string.gsub(pattern_tmp, 'template', '1', 1) -- 静态资源路径中的systemid和managerid这类写死为1
            content = m.get_content_from_tpl(pattern_tmp, 'json')
        end
    end
    return content or cjson.json_object_new_object()
end

-- 对于传入的带时区的时间,需要相反操作
-- 如传入2024-11-26T08:04:33+06:00,需要-6h回到UTC+00:00时区时间
local OP_MAP = {
    ['+'] = function(a, b)
        return a - b
    end,
    ['-'] = function(a, b)
        return a + b
    end,
}

-- 处理BMC时区的影响
-- 如BMC时区+06:00,os.time处理时间戳后需要+6h回到UTC+00:00时区时间
local LOCAL_OP_MAP = {
    ['-'] = function(a, b)
        return a - b
    end,
    ['+'] = function(a, b)
        return a + b
    end,
}

-- @param date_time 形如"2024-11-26T08:04:33+06:00"的字符串
-- @param local_offset 本地时区偏移量,例如"+08:00"
-- @return 时间戳
function m.str2timestamp(date_time, local_offset)
    if not date_time or #date_time == 0 then
        log:error('Input data cannot be empty.')
        return 0
    end
    local ok, y, mon, d, _hour, _min, _sec, op, h, _m = pcall(function()
        return string.match(date_time, '(%d+)-(%d+)-(%d+)T(%d+):(%d+):(%d+)(.)(%d+):(%d+)')
    end)
    if not ok then
        log:error('The format of the input data is incorrect.')
        return 0
    end
    local timestamp = utils.get_timestamp(y, mon, d, _hour, _min, _sec)
    local diff_time = h * 3600 + _m * 60
    timestamp = OP_MAP[op](timestamp, diff_time)

    if not local_offset or #local_offset == 0 or local_offset == cjson.null then
        return timestamp
    end
    ok, op, h, _m = pcall(string.match, local_offset, '(.)(%d+):(%d+)')
    if not ok then
        log:error('The format of the local_offset is incorrect.')
        return 0
    end
    diff_time = h * 3600 + _m * 60
    return LOCAL_OP_MAP[op](timestamp, diff_time)
end

function m.get_registry_prefix()
    return custom_messages.RegistryPrefix
end

function m.get_min_pcie_type(pcie_type1, pcie_type2)
    if string.match(pcie_type1, '^Gen[1-6]$') == nil or string.match(pcie_type2, '^Gen[1-6]$') == nil then
        return null
    elseif pcie_type1 < pcie_type2 then
        return pcie_type1
    else
        return pcie_type2
    end
end

-- 将ISO 8601持续时间格式字符串转换为秒数
function m.iso8601_duration_to_seconds(duration_str)
    -- 初始化各部分为0
    local date_part = { years = 0, months = 0, days = 0 }
    local time_part = { hours = 0, minutes = 0, seconds = 0 }

    -- 1. 分离日期和时间部分
    local date_str, time_str = duration_str:match('^P([^T]*)T?(.*)$')
    if not date_str then
        date_str = duration_str:sub(2)
        time_str = ''
    end

    -- 2. 解析日期部分(年、月、日)
    for unit, value in date_str:gmatch('(%d+)([YMD])') do
        unit = tonumber(unit)
        if value == 'Y' then
            date_part.years = unit
        elseif value == 'M' then
            date_part.months = unit
        elseif value == 'D' then
            date_part.days = unit
        end
    end

    -- 3. 解析时间部分(小时、分钟、秒)
    if time_str and time_str ~= '' then
        for key, value in time_str:gmatch('(%d+)([HMS])') do
            local num = tonumber(key)
            if value == 'H' then
                time_part.hours = num
            elseif value == 'M' then
                time_part.minutes = num
            elseif value == 'S' then
                time_part.seconds = num
            end
        end
    end

    -- 4. 计算总秒数(使用近似值)
    -- 日期部分转换
    local date_seconds = date_part.years * 365 * 24 * 60 * 60 -- 年
    date_seconds = date_seconds + date_part.months * 30 * 24 * 60 * 60 -- 月
    date_seconds = date_seconds + date_part.days * 24 * 60 * 60 -- 日

    -- 时间部分转换
    local time_seconds = time_part.hours * 60 * 60 -- 小时
    time_seconds = time_seconds + time_part.minutes * 60 -- 分钟
    time_seconds = time_seconds + time_part.seconds -- 秒

    local total_seconds = date_seconds + time_seconds
    -- 最大允许设置8小时,换算秒数为28800秒
    if total_seconds > 28800 then
        local err = custom_messages.PropertyValueOutOfRange(duration_str, 'EjectTimeout')
        err.RelatedProperties = { '#/EjectTimeout' }
        error(err)
    end
    return total_seconds
end

-- 将秒数转换为ISO 8601持续时间格式字符串
function m.seconds_to_iso8601_duration(seconds)
    if type(seconds) ~= 'number' or seconds < 0 then
        return 'null'
    end

    -- 分解各个时间单位
    local remaining = seconds

    -- 计算年(假设1年=365天)
    local years = math.floor(remaining / (365 * 24 * 60 * 60))
    remaining = remaining % (365 * 24 * 60 * 60)

    -- 计算月(假设1月=30天)
    local months = math.floor(remaining / (30 * 24 * 60 * 60))
    remaining = remaining % (30 * 24 * 60 * 60)

    -- 计算天
    local days = math.floor(remaining / (24 * 60 * 60))
    remaining = remaining % (24 * 60 * 60)

    -- 计算小时
    local hours = math.floor(remaining / (60 * 60))
    remaining = remaining % (60 * 60)

    -- 计算分钟
    local minutes = math.floor(remaining / 60)
    remaining = remaining % 60

    -- 剩余的秒数
    local secs = remaining

    -- 构建ISO 8601持续时间字符串
    local parts = {}

    -- 日期部分(年、月、日)
    if years > 0 then
        table.insert(parts, years .. 'Y')
    end
    if months > 0 then
        table.insert(parts, months .. 'M')
    end
    if days > 0 then
        table.insert(parts, days .. 'D')
    end

    -- 时间部分(小时、分钟、秒)
    local time_parts = {}
    if hours > 0 then
        table.insert(time_parts, hours .. 'H')
    end
    if minutes > 0 then
        table.insert(time_parts, minutes .. 'M')
    end
    if secs > 0 or (#time_parts == 0 and #parts > 0) then
        table.insert(time_parts, secs .. 'S')
    end

    -- 如果有时间部分,添加T分隔符
    if #time_parts > 0 then
        table.insert(parts, 'T')
        for _, part in ipairs(time_parts) do
            table.insert(parts, part)
        end
    end
    -- 如果没有部分,表示0秒
    if #parts == 0 then
        return 'PT0S'
    end

    -- 组合所有部分
    return 'P' .. table.concat(parts)
end

function m.check_any_property_is_exsit(properties)
    -- 入参不存在,或者入参个数为0,返回false
    if not properties or properties == cjson.null or #properties == 0 then
        return false
    end
    for _, prop in ipairs(properties) do
        if prop ~= cjson.null then
            return true
        end
    end
    return false
end

function m.check_file_exist(file_name, file_type)
    file_name = string.lower(file_name)
    file_type = string.lower(file_type)
    local RESOURCE_PREFIX = '/data/opt/bmc/redfish/static_resource'
    local base_paths = {
        RESOURCE_PREFIX .. '/redfish/v1/schemastore/en/dmtf/' .. file_type,
        RESOURCE_PREFIX .. '/redfish/v1/schemastore/en/oem/openubmc/' .. file_type,
    }

    for _, path_prefix in ipairs(base_paths) do
        local full_path = path_prefix .. '/' .. file_name
        local res = utils_core.is_file(full_path)
        if res == true then
            return true
        end
    end
    return false
end

function m.is_has_active_and_standby(local_state, remote_id_paths)
    local has_active = false
    local has_standby = false
    -- 检查本板状态
    if local_state == 1 then
        has_active = true
    elseif local_state == 0 then
        has_standby = true
    end

    -- 检查远端状态
    for _, remote_id_path in ipairs(remote_id_paths) do
        local ok, obj = pcall(mdb.get_object, bus, remote_id_path, 'bmc.kepler.Managers.ActiveStandby.Status')
        if ok then
            local state = obj.ActiveStatus
            if state == 1 then
                has_active = true
            elseif state == 0 then
                has_standby = true
            end
        end
    end
    -- 同时存在主和备返回 true
    return (has_active and has_standby)
end

return m