-- 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_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.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_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_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

    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_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

return m
