-- 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 utils = require 'mc.utils'

local ETH_OBJ_PATH<const> = '/bmc/kepler/Managers/1/EthernetInterfaces'
local ETH_PORT_INTERFACE<const> = 'bmc.kepler.Managers.EthernetInterfaces.MgmtPort'
local ETH_GROUP_INTERFACE<const> = 'bmc.kepler.Managers.EthernetInterfaces.EthGroup'

local m = {}

function m.get_address_mode(DHCPv4UseDNSServers, DHCPv6UseDNSServers)
    if (not DHCPv4UseDNSServers) and (not DHCPv6UseDNSServers) then
        return 'Static'
    elseif DHCPv4UseDNSServers and (not DHCPv6UseDNSServers) then
        return 'IPv4'
    elseif DHCPv6UseDNSServers and (not DHCPv4UseDNSServers) then
        return 'IPv6'
    end

    return 'IPv4'
end

-- 资源树上的link地址和slaac地址是以fec0::01/64这样呈现的
-- 适配网页显示需要做转换
local function split_ipv6_address(ipv6_address)
    local strs = utils.split(ipv6_address, '/')
    return {addr = strs[1], prefix = strs[2]}
end

function m.get_ipv6_addresses(eth_obj, ipv6_obj)
    local ipv6_addresses = {}
    local pre_fix = ipv6_obj.PrefixLength == 0 and '' or ipv6_obj.PrefixLength
    ipv6_addresses[#ipv6_addresses + 1] = {
        IPAddress = ipv6_obj.IpAddr,
        IPAddressMode = ipv6_obj.IpMode,
        PrefixLength = pre_fix
    }
    local link_local = eth_obj.LinkLocalAddress
    if #link_local ~= 0 then
        local format_addr = split_ipv6_address(link_local)
        ipv6_addresses[#ipv6_addresses + 1] = {
            IPAddress = format_addr.addr,
            IPAddressMode = 'LinkLocal',
            PrefixLength = tonumber(format_addr.prefix)
        }
    end
    local slaac_list = eth_obj.SLAACAddressList
    for _, value in ipairs(slaac_list) do
        local format_slaac_addr = split_ipv6_address(value)
        ipv6_addresses[#ipv6_addresses + 1] = {
            IPAddress = format_slaac_addr.addr,
            IPAddressMode = 'SLAAC',
            PrefixLength = tonumber(format_slaac_addr.prefix)
        }
    end
    return ipv6_addresses
end

-- 用于web页面和资源树类型的相互转化
local type_switch_table = {
    mdb_to_web = {
        Lom = 'LOM',
        Dedicated = 'Dedicated',
        ExternalPCIe = 'ExternalPCIe',
        Ocp = 'OCP',
        Ocp2 = 'OCP2',
        Aggregation = 'Aggregation',
        FlexIO = 'FlexIO'
    },
    web_to_mdb = {
        LOM = 'Lom',
        Dedicated = 'Dedicated',
        ExternalPCIe = 'ExternalPCIe',
        OCP = 'Ocp',
        OCP2 = 'Ocp2',
        Aggregation = 'Aggregation',
        FlexIO = 'FlexIO'
    }
}

local function get_net_mode(ether_props, group_props)
    local activie_group_num = 0
    local net_mode = 'Fixed'
    local manager_ports = {}
    if ether_props.Status then
        activie_group_num = activie_group_num + 1
        net_mode = ether_props.NetMode
        manager_ports[ether_props.PortId] = true
    end
    for _, group_prop in pairs(group_props) do
        net_mode = group_prop.NetMode
        activie_group_num = activie_group_num + 1
        manager_ports[group_prop.ActivePortId] = true
    end
    if activie_group_num > 1 then
        return 'Fixed', manager_ports
    end

    return net_mode, manager_ports
end

local function handler_network_port(ports, port_id, manager_ports, management_network_port, management_network_ports, 
    members)
    local port_number, member
    for _, value in ipairs(ports) do
        port_number = value.DevicePortId
        member = {}
        if value.Id == port_id then
            management_network_port.PortNumber = port_number
            management_network_port.Type = value.Type
            management_network_port.PortName = "Eth" .. tostring(value.EthId)
        end

        if value.Type == 'Dedicated' then
            if value.Silkscreen == 'Mgmt' then
                member.PortName = 'Mgmt'
            else
                member.PortName = 'Port' .. value.DevicePortId .. '(' .. value.Silkscreen .. ')'
            end
        else
            member.PortName = 'Port' ..  tostring(value.DevicePortId)
        end

        if manager_ports[value.Id] then
            local working_port = {}
            working_port.PortNumber = port_number
            working_port.Type = value.Type
            working_port.PortName = "Eth" .. tostring(value.EthId)
            table.insert(management_network_ports, working_port)
        end

        member.PortNumber = port_number
        member.LinkStatus = value.LinkStatus
        member.AdaptiveFlag = value.AdaptiveFlag
        local type = type_switch_table.mdb_to_web[value.Type]
        if type then
            members[type][#members[type] + 1] = member
        end
    end
end

local function get_group_props(group_paths)
    local ok, eth_group_obj
    local group_props = {}
    for _, group_path in ipairs(group_paths) do 
        ok, eth_group_obj = pcall(mdb.get_object, bus, group_path,  'bmc.kepler.Managers.EthernetInterfaces.EthGroup') 
        if ok and eth_group_obj.OutType == 2 and eth_group_obj.Status then
            group_props[eth_group_obj.GroupId] = {
                ['ActiveEthId'] = eth_group_obj.ActiveEthId,
                ['ActivePortId'] = eth_group_obj.ActivePortId,
                ['NetMode'] = eth_group_obj.NetMode
            }
        end
    end
    return group_props
end

function m.get_network_port(ether_props, port_id, ports, group_paths)
    local network_port = {}
    local management_network_port = {}
    local management_network_ports = {}
    local members = {
        LOM = {},
        Dedicated = {},
        ExternalPCIe = {},
        OCP = {},
        OCP2 = {},
        Aggregation = {},
        FlexIO = {}
    }
    local group_props = get_group_props(group_paths)
    local net_mode, manager_ports = get_net_mode(ether_props, group_props)

    handler_network_port(ports, port_id, manager_ports, management_network_port, management_network_ports, members)

    network_port = {
        Mode = net_mode,
        ManagementNetworkPort = management_network_port,
        ManagementNetworkPorts = management_network_ports,
        Members = members
    }

    return network_port
end

function m.is_multi_ethernet_interface_supported(group_paths)
    local multi_ethernet_interface_supported = false
    for _, group_path in ipairs(group_paths) do
        local ok, ethGroup_obj = pcall(mdb.get_object, bus, group_path, ETH_GROUP_INTERFACE)
        if ok and ethGroup_obj.OutType == 2 then
            multi_ethernet_interface_supported = true
        end
    end
    return multi_ethernet_interface_supported
end

local function process_network_port(value, port_id, management_network_ports, network_port_members)
    if value.Id == port_id then
        local port_number = value.DevicePortId
        local management_network_port = {}
        local member = {}
        local members = {
            LOM = {},
            Dedicated = {},
            ExternalPCIe = {},
            OCP = {},
            OCP2 = {},
            Aggregation = {},
            FlexIO = {}
        }
        if value.Type == 'Dedicated' then
            if value.Silkscreen == 'Mgmt' then
                member.PortName = 'Mgmt'
            else
                member.PortName = 'Port' .. value.DevicePortId .. '(' .. value.Silkscreen .. ')'
            end
        else
            member.PortName = 'Port' ..  tostring(value.DevicePortId)
        end
        management_network_port.PortNumber = port_number
        management_network_port.Type = value.Type
        management_network_port.PortName = "Eth" .. tostring(value.EthId)
        table.insert(management_network_ports, management_network_port)

        member.PortNumber = port_number 
        member.LinkStatus = value.LinkStatus
        member.AdaptiveFlag = value.AdaptiveFlag
        local type = type_switch_table.mdb_to_web[value.Type]
        if type then
            members[type][#members[type] + 1] = member
        end
        table.insert(network_port_members, members)
    end
end

local function get_ethernet_group(ethGroup_obj, management_network_ports, network_port_members)
    local IPv6Infos = {}
    local IPv6Info = {
        IPAddressMode = ethGroup_obj.Ipv6Mode,
        IPAddress = ethGroup_obj.Ipv6Addr,
        PrefixLength = ethGroup_obj.PrefixLength
    } 
    table.insert(IPv6Infos, IPv6Info)
    local ethernet_interface_group = {
        NetworkPort = {
            Mode = ethGroup_obj.NetMode,
            ManagementNetworkPorts = management_network_ports,
            Members = network_port_members
        },
        NetworkProtocols = {
            ProtocolMode = ethGroup_obj.IpVersion,
            IPv4Config = {
                IPAddressMode = ethGroup_obj.IpMode,
                IPAddress = ethGroup_obj.IpAddr,
                SubnetMask = ethGroup_obj.SubnetMask,
                Gateway = ethGroup_obj.DefaultGateway,
                PermanentMACAddress = ethGroup_obj.Mac
            },
            IPv6Config = {
                Gateway = ethGroup_obj.Ipv6DefaultGateway,
                IPv6Addresses = IPv6Infos
            }
        }
    }
    return ethernet_interface_group
end

local function get_vlan_type(eth_obj, ports)
    local port_id = eth_obj.PortId
    for _, value in ipairs(ports) do
        if port_id == value.Id then
            if value.Type == "Dedicated" then
                return "DedicatedVLAN"
            else
                return "NcsiVLAN"
            end
        end
    end
end

local function get_ethernet_interface_group(eth_obj, ipv4_obj, ipv6_obj, 
    management_network_ports, network_port_members, vlan_type)
    local IPv6Infos = {}
    local IPv6Info = {
        IPAddressMode = ipv6_obj.IpMode,
        IPAddress = ipv6_obj.IpAddr,
        PrefixLength = ipv6_obj.PrefixLength
    }
    table.insert(IPv6Infos, IPv6Info)
    local ethernet_interface_group = {
        NetworkPort = {
            Mode = eth_obj.NetMode,
            ManagementNetworkPorts = management_network_ports,
            Members = network_port_members
        },
        NetworkProtocols = {
            ProtocolMode = eth_obj.IpVersion,
            IPv4Config = {
                IPAddressMode = ipv4_obj.IpMode,
                IPAddress = ipv4_obj.IpAddr,
                SubnetMask = ipv4_obj.SubnetMask,
                Gateway = ipv4_obj.DefaultGateway,
                PermanentMACAddress = eth_obj.Mac
            },
            IPv6Config = {
                Gateway = ipv6_obj.DefaultGateway,
                IPv6Addresses = IPv6Infos
            }
        }
    }
    if vlan_type and vlan_type ~= "" then
        ethernet_interface_group[vlan_type] = {
            Enabled = eth_obj.VLANEnable,
            VLANId = eth_obj.VLANId
        }
    end

    return ethernet_interface_group
end

local function insert_eth_group(group_path, ports, is_multi_net, eth_groups)
    local ok, ethGroup_obj = pcall(mdb.get_object, bus, group_path, ETH_GROUP_INTERFACE)
    local management_network_ports = {}
    local network_port_members = {}
    if ok and ethGroup_obj.OutType == 2 then
        is_multi_net = true
        if ethGroup_obj.Status then
            local active_port_id = ethGroup_obj.ActivePortId
            for _, value in ipairs(ports) do
                process_network_port(value, active_port_id, management_network_ports, network_port_members)
            end 
            local ethernet_group = get_ethernet_group(ethGroup_obj, management_network_ports,
            network_port_members)
            table.insert(eth_groups, ethernet_group)
        end
    end

    return is_multi_net
end

function m.get_ethernet_interface_groups(eth_obj, ipv4_obj, ipv6_obj, ports, group_paths)
    local eth_groups = {}
    local interface_ports = {}
    local interface_members = {}
    local is_multi_net = false
    local vlan_type = get_vlan_type(eth_obj, ports)

    for _, group_path in ipairs(group_paths) do
        is_multi_net = insert_eth_group(group_path, ports, is_multi_net, eth_groups)
    end

    if is_multi_net then
        for _, value in ipairs(ports) do
            process_network_port(value, eth_obj.PortId, interface_ports, interface_members)
        end

        local ethernet_interface_group = get_ethernet_interface_group(eth_obj, ipv4_obj, ipv6_obj,
        interface_ports, interface_members, vlan_type)
        table.insert(eth_groups, ethernet_interface_group)
    end

    return eth_groups
end

function m.set_dedicated_vlan(enabled, vlan_id)
    if enabled == false and vlan_id == nil then
        return {enabled, 0, 1}
    end
    if enabled == true and vlan_id == nil then
        error(base_messages.PropertyMissing("VLANId"))
    end
    if enabled == nil and vlan_id ~= nil and vlan_id ~= 0 then
        return {true, vlan_id, 1}
    end
    if enabled == nil and vlan_id ~= nil and vlan_id == 0 then
        return {true, vlan_id, 1}
    end
    return {enabled, vlan_id, 1}
end

function m.check_ipv4_address_mode(ipv4_config)
    if ipv4_config.IPAddressMode and tostring(ipv4_config.IPAddressMode) == 'DHCP' and
        (ipv4_config.IPAddress or ipv4_config.SubnetMask or ipv4_config.Gateway) then
        error(custom_messages.IPv4InfoConflictwithDHCP())
        return false
    end

    return true
end

function m.check_ipv6_address_mode(ipv6_config)
    if ipv6_config.IPv6Addresses and ipv6_config.IPv6Addresses[1] and ipv6_config.IPv6Addresses[1].IPAddressMode then
        local ipv6_address = ipv6_config.IPv6Addresses[1]
        if tostring(ipv6_address.IPAddressMode) == 'DHCPv6' and
            (ipv6_address.IPAddress or ipv6_address.PrefixLength) then
            error(custom_messages.IPv6InfoConflictwithDHCPv6())
            return false
        end
    end

    return true
end

function m.check_ipv6_config(ipv6_config)
    if ipv6_config.Gateway or
        (ipv6_config.IPv6Addresses and ipv6_config.IPv6Addresses[1] and ipv6_config.IPv6Addresses[1].IPAddress) or
        (ipv6_config.IPv6Addresses and ipv6_config.IPv6Addresses[1] and ipv6_config.IPv6Addresses[1].PrefixLength) then
        return true
    end

    return false
end

local function get_portid_by_type(port_id, port)
    local port_obj = mdb.get_sub_objects(bus, ETH_OBJ_PATH, ETH_PORT_INTERFACE)
    local type = port.Type
    local device_port_id = port.PortNumber
    if type ~= nil then
        local type_valid = false
        for _, obj in pairs(port_obj) do
            if type_switch_table.web_to_mdb[type] == obj.Type then
                type_valid = true
                break
            end
        end
        if not type_valid then
            error(custom_messages.InvalidValue(type, "Type"))
        end
    end

    if not type or not device_port_id then
        return port_id
    end
    for _, obj in pairs(port_obj) do
        if device_port_id == obj.DevicePortId and type_switch_table.web_to_mdb[type] == obj.Type then
            port_id = obj.Id
            return port_id
        end
    end
    error(custom_messages.PortNotExist(type, device_port_id))
end

function m.set_network_config(network_port, ncsi_VLAN, eth_obj)
    local mode = eth_obj.NetMode
    local port_id = eth_obj.PortId
    local vlan_enable = eth_obj.VLANEnable
    local vlan_id = eth_obj.VLANId
    if network_port then
        if network_port.Mode then
            mode = tostring(network_port.Mode)
        end
        if network_port.ManagementNetworkPort then
            port_id = get_portid_by_type(port_id, network_port.ManagementNetworkPort)
        end
    end
    if ncsi_VLAN then
        if ncsi_VLAN.VLANId then
            vlan_id = ncsi_VLAN.VLANId
        end
        if ncsi_VLAN.Enabled ~= nil then
            vlan_enable = ncsi_VLAN.Enabled
        else
            vlan_enable = true
        end
        -- vlan关闭时，id设置为0
        if not vlan_enable then
            vlan_id = 0
        end
        if ncsi_VLAN.Enabled == true and ncsi_VLAN.VLANId == nil then
            error(base_messages.PropertyMissing("VLANId"))
        end
    end

    return {mode, port_id, vlan_enable, vlan_id}
end

function m.is_port_null(type, members)
    local port_obj = mdb.get_sub_objects(bus, ETH_OBJ_PATH, ETH_PORT_INTERFACE)
    local match = false
    for _, dedicated_port_info in ipairs(members) do
        local device_port_id = dedicated_port_info.PortNumber
        for _, obj in pairs(port_obj) do
            if device_port_id and device_port_id == obj.DevicePortId and tostring(type) == obj.Type then
                match = true
            end
        end

        if not match then
            error(custom_messages.PortNotExist(type, device_port_id))
        end

        match = false
    end

    return true
end

return m
