-- 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 class = require 'mc.class'
local skynet = require 'skynet'
local c_handler_base = require 'bma.handles.handler_base'
local c_network_port = require 'device.class.network_port'
local c_network_adapter = require 'device.class.network_adapter'
local c_ipv4_address = require 'device.class.ipv4_address'
local c_ipv6_address = require 'device.class.ipv6_address'
local log = require 'mc.logging'
local singleton = require 'mc.singleton'
local event_mgmt = require 'event.event_mgmt'
local json = require 'cjson'
local c_network_bridge = require 'device.class.network_bridge'
local c_network_bonding = require 'device.class.network_bonding'

local is_na = c_handler_base.is_na
local get_oem_huawei = c_handler_base.get_oem_huawei
local is_nil_or_null = c_handler_base.is_nil_or_null
local is_invalid_str = c_handler_base.is_invalid_str
local default_na = c_handler_base.default_na
local update_if_not_config = c_handler_base.update_if_not_config
local update_if_valid = c_handler_base.update_if_valid
local update_number_if_valid = c_handler_base.update_number_if_valid
local prepare_ipv4_address = c_handler_base.prepare_ipv4_address
local prepare_ipv6_address = c_handler_base.prepare_ipv6_address
local get_odata_id = c_handler_base.get_odata_id
local decode_bdf = c_handler_base.decode_bdf
local update_network_card_model = c_handler_base.update_network_card_model
local update_network_card_manufacturer = c_handler_base.update_network_card_manufacturer
local update_network_port_netdev_functype = c_handler_base.update_network_port_netdev_functype

local BUSINESSPORT_CARD_TYPE_VIR<const> = 6
local BUSINESSPORT_CARD_TYPE_TEAM<const> = 7
local BUSINESSPORT_CARD_TYPE_BRIDGE<const> = 8
local BUSINESSPORT_CARD_TYPE_MEZZCARD<const> = 9

local ODATA_TYPE<const> = '@odata.type'

local oam_data_cache_list = {}

local NETDEV_FUNCTYPE_ETHERNET<const> = 1

local function get_busiport_type(odata_id)
    if not odata_id then
        return 0xFF
    elseif odata_id:find('/Team/') then
        return BUSINESSPORT_CARD_TYPE_TEAM
    elseif odata_id:find('/Bridge/') then
        return BUSINESSPORT_CARD_TYPE_BRIDGE
    else
        return BUSINESSPORT_CARD_TYPE_VIR
    end
end

local c_handler_eth = class(c_handler_base)

function c_handler_eth:ctor()
end

function c_handler_eth:init()
    self:regist_odata_type('EthernetInterface')
    self:regist_odata_type('OemInfiniBand')
    self:regist_odata_type('OemEthernetInterfaceOAM')

    self:regist_class_type(c_network_adapter)
    self:regist_class_type(c_network_port)
    self.add_object_complete = false

    c_network_port.on_add_object_complete:on(function ()
        self.add_object_complete = true
        self:match_resource()
    end)

    c_network_port.on_add_object:on(function(object)
        if object.BDF and #object.BDF > 0 then
            -- 已存在 BDF 的网口直接刷新 BMA 资源
            self:match_resource(object)
            c_network_port.port_register = true
            self:handle_cache_data()
        end

        -- 监听 BDF 变化，有些网口的 BDF 需要通过 NCSI 或其他协议动态获取
        object:listen('BDF', function(_, value)
            log:info('c_network_port: bdf update: bdf=%s, network adapter id=%s, port id=%s', value,
                object.NetworkAdapterId, object.PortID)

            -- 刷新 BMA 资源
            self:match_resource(object)
            c_network_port.port_register = true
            self:handle_cache_data()
        end)
    end)
end

local function is_virtual_port(oem_huawei)
    if is_nil_or_null(oem_huawei) then
        return false
    end

    if is_nil_or_null(oem_huawei.PortType) then
        return false
    end
    if oem_huawei.PortType ~= 'Physical' then
        return true
    end
    return false
end

local function is_need_update(oem_huawei)
    if is_virtual_port(oem_huawei) and is_invalid_str(oem_huawei.NICName) then
        return false
    end

    return true
end

local function get_virtual_port_bdf(odata_id)
    local _, bdf, _ = odata_id:match("/(%d+:%d+:%d+%.%d+)_(%d+:%d+:%d+%.%d+)_(%d+)")
    return bdf
end

local function get_port_bdf(data)
    local oem_huawei = get_oem_huawei(data)
    if not oem_huawei then
        return nil
    end
    if not is_virtual_port(oem_huawei) then
        return oem_huawei.BDFNumber.BDF
    end

    local odata_id = get_odata_id(data)
    if get_busiport_type(odata_id) == BUSINESSPORT_CARD_TYPE_VIR then
        local bdf = get_virtual_port_bdf(odata_id)
        if not bdf then
            bdf = oem_huawei.BDFNumber.BDF
        end
        return bdf, oem_huawei.BDFNumber.RootBDF

    end
    -- team和bridge使用URLId 或者NICName
    return  oem_huawei.URLId or oem_huawei.NICName
end

function c_handler_eth:create_network_port(na_obj_name, value)
    c_network_port.insert_or_update(na_obj_name, value):save()

    -- 等待np创建成功
    skynet.sleep(10)
    local obj = c_network_port.collection:find({
        PortID = value.PortID,
        NetworkAdapterId = value.NetworkAdapterId
    })
    return obj
end

function c_handler_eth:create_bridge_or_bonding_port(na, port_id, bdf, type)
    local value = {
        PortID = port_id,
        NetworkAdapterId = na.ID,
        NodeId = na.ID,
        Type = type,
        BDF = bdf
    }

    return self:create_network_port(na.na_object_name, value)
end

function c_handler_eth:create_virtual_network_port(na, bdf, type, name)
    local port_str = string.match(name, "v(%d)$") or '0'
    local id = tonumber(port_str) + na.NetworkPortCount
    local value = {
        PortID = id,
        NetworkAdapterId = na.ID,
        NodeId = name,
        Type = type,
        BDF = bdf
    }
    return self:create_network_port(na.na_object_name, value)
end

local function find_network_card(data)
    local bdf, rootbdf = get_port_bdf(data)
    local actual_bdf = rootbdf or bdf
    return c_network_adapter.collection:find({RootBDF = actual_bdf})
end

function c_handler_eth:create_network_card(data, bdf, type)
    local obj = find_network_card(data)
    if obj then
        return obj
    end

    c_network_adapter.insert_or_update({
        ID = data.Id,
        NodeId = data.Id,
        Type = type,
        RootBDF = bdf
    }):save()

    -- 等待na创建成功
    skynet.sleep(10)
    local obj = c_network_adapter.collection:find({
        ID = data.Id,
        NodeId = data.Id
    })
    return obj
end

function c_handler_eth:create_eth_object(data)
    local bdf = get_port_bdf(data)
    if not bdf then
        return
    end

    local object = c_network_port.collection:find(function(obj)
        return obj.BDF == bdf
    end)
    if object then
        return object
    end
    local odata_id = get_odata_id(data)
    local type = get_busiport_type(odata_id)
    local na = self:create_network_card(data, bdf, type)
    if not na then
        log:error("create bridge network card failed")
        return
    end
    if type == BUSINESSPORT_CARD_TYPE_VIR then
        return self:create_virtual_network_port(na, bdf, type, data.Name)
    end
    return self:create_bridge_or_bonding_port(na, 0, bdf, type)
end

local function is_valid_ports(ports)
    for _, port in pairs(ports) do
        if type(port) ~= 'string' then
            return false
        end
    end
    return true
end

function c_handler_eth:create_network_bridge_and_bonding(data)
    local odata_id = get_odata_id(data)
    local type = get_busiport_type(odata_id)
    -- 判断data.ports的合法性，避免iBMA传入ports为nil此处报错导致创建对象失败
    local ports = data.Ports
    if not is_valid_ports(ports) then
        log:notice('Ports is invalid')
        ports = {}
    end
    if type == BUSINESSPORT_CARD_TYPE_BRIDGE then
        c_network_bridge.insert_or_update({
            Id = data.Id,
            Name = data.Name,
            Ports = ports
        })
    end

    if type == BUSINESSPORT_CARD_TYPE_TEAM then
        c_network_bonding.insert_or_update({
            Id = data.Id,
            Name = data.Name,
            Ports = ports
        })
    end
end

function c_handler_eth:find_object(_, data)
    local oem_huawei = get_oem_huawei(data)

    if not is_need_update(oem_huawei) then
        return nil
    end

    if is_virtual_port(oem_huawei) then
        self:create_network_bridge_and_bonding(data)
        return self:create_eth_object(data)
    end

    -- 先通过 BDF 信息匹配
    local BDF = get_port_bdf(data)
    local object = c_network_port.collection:find(function(obj)
        return obj.BDF == BDF
    end)
    if object then
        return object
    end

    return self:find_port_by_na(oem_huawei)
end

function c_handler_eth:find_port_by_na(oem_huawei)
    if not oem_huawei or not oem_huawei.BDFNumber or not oem_huawei.BDFNumber.RootBDF then
        return nil
    end

    local na = c_network_adapter.collection:find(decode_bdf(oem_huawei.BDFNumber.RootBDF))
    if not na then
        return nil
    end

    local port_num = nil
    if na.SpecialPcieCard then
        port_num = self:get_special_pcie_network_port_num(oem_huawei)
    elseif not is_nil_or_null(oem_huawei.BDFNumber.PortNum) then
        port_num = oem_huawei.BDFNumber.PortNum
    else
        port_num = decode_bdf(oem_huawei.BDFNumber.BDF).Function
    end
    return c_network_port.collection:find({PortID = port_num, NetworkAdapterId = na.NodeId})
end

local function get_netwrok_adapter_by_dbf(port)
    local bdf = port.BDF
    -- 虚拟网卡通过BDF 查找网卡
    local na = c_network_adapter.collection:find(function (obj)
        return obj.RootBDF == bdf
    end)

    return na
end

local function get_port_parent_card(port)
    local parent = port:get_parent()
    if not parent then
        return get_netwrok_adapter_by_dbf(port)
    end
    return c_network_adapter.collection:find({NodeId = parent.NodeId})
end

function c_handler_eth:get_special_pcie_network_port_num(oem_huawei)
    -- 如果为特殊网卡，port_num从倒数第二级BDF取
    -- PCIePath：/sys/devices/pci0000:ae/0000:ae:02.0/0000:af:00.0/0000:b0:01.0/0000:b2:00.0
    local port_num_str = oem_huawei.PCIePath:match(':(%w+).%w+%/[^/]+$')
    if not port_num_str then
        log:raise('c_handler_eth, match PCIePath.PortNum faild: PCIePath=%s', oem_huawei.PCIePath)
    end

    local port_num = tonumber(port_num_str, 16)
    if not port_num then
        log:raise('c_handler_eth, invalid PCIePath.PortNum, PCIePath=%s, PortNum=%s',
            oem_huawei.PCIePath, port_num_str)
    end

    return port_num
end

-- bma上报更新bmc的以太网卡（LOM、MEZZ、PCIE）属性，BMC能预配置的不从BMA更新
function c_handler_eth:update_network_card(port, data, oem_huawei)
    if port.Type == BUSINESSPORT_CARD_TYPE_VIR or port.WorkloadType == 1 then
        return
    end
    local na = get_port_parent_card(port)
    if not na then
        log:error(
            'update network card from bma failed: path=%s, @odata.id=%s, cannot find network card',
            port.path, get_odata_id(data))
        return
    end

    log:info('update network card from bma: path=%s, @odata.id=%s', na.path, get_odata_id(data))
    update_network_card_model(na, oem_huawei)
    update_network_card_manufacturer(na, data)

    -- 更新OS侧网卡名称
    na.DisplayName = update_if_not_config(na.DisplayName, default_na(oem_huawei.NICName))

    -- 更新OS侧网卡固件版本
    na.FirmwareVersion = update_if_not_config(na.FirmwareVersion,
        default_na(oem_huawei.FirmwareVersion))

    -- 更新OS侧网卡驱动名称
    if oem_huawei.DriverInfo ~= nil and oem_huawei.DriverInfo.DriverName ~= nil then
        na.DriverName = default_na(oem_huawei.DriverInfo.DriverName)
    end

    -- 更新OS侧网卡驱动版本
    if oem_huawei.DriverInfo ~= nil and oem_huawei.DriverInfo.DriverVersion ~= nil then
        na.DriverVersion = default_na(oem_huawei.DriverInfo.DriverVersion)
    end

    -- 更新OS侧网卡描述信息
    na.Description = update_if_not_config(na.Description, default_na(data.Description))

    -- 更新OS侧ROOTBDF
    if oem_huawei.BDFNumber and oem_huawei.BDFNumber.RootBDF then
        na.RootBDF = default_na(oem_huawei.BDFNumber.RootBDF)
    end
end

function c_handler_eth:update_network_port_bdf(port, data, oem_huawei)
    if not data['@odata.id'] then
        return
    end

    local odata_id = get_odata_id(data)
    if get_busiport_type(odata_id) == BUSINESSPORT_CARD_TYPE_VIR then
        -- 对于MZ311等mellanox
        -- 网卡，同一个bdf+port下有多个网口，url后面会跟上port号，与bdf不一致，直接保持BDF,会造成VLAN无法识别
        -- 对于这种网口，直接从url中获取BDF
        -- url示例: /redfish/v1/Sms/1/Systems/1/EthernetInterfaces/0000:80:02.2_0000:85:00.0_1
        local _, bdf = odata_id:match('/([^/_]+)_([^/_]+_%x+)$') -- 含有两个"_"
        if not bdf then
            -- 板卡支持获取BDF信息
            port.BDF = default_na(oem_huawei.BDFNumber.BDF)
        else
            port.BDF = bdf
        end
    elseif oem_huawei.URLId ~= nil then
        -- 存在URLId属性则使用该属性，否则使用NICName
        port.BDF = default_na(oem_huawei.URLId)
    elseif oem_huawei.NICName ~= nil then
        -- team/bridge采用NICName作为索引
        port.BDF = default_na(oem_huawei.NICName)
    end
end

function c_handler_eth:update_ip_addresses(port, ip_class, addresses)
    local addrs = {}
    for _, addr in ipairs(addresses) do
        ip_class.insert_or_update(port.PortID, port.NetworkAdapterId, port.na_object_name,
            port.np_object_name, addr):save()
        addrs[addr.Address] = true
    end

    local existing_ips = ip_class.get_addresses(port.PortID, port.NetworkAdapterId)
    for _, addr in ipairs(existing_ips) do
        if not addrs[addr.Address] then
            addr:dtor()
        end
    end
end

function c_handler_eth:get_vlan_is_onboot(dst, v)
    if is_nil_or_null(v) then
        return dst
    end

    if v == 'yes' then
        return true
    elseif v == 'no' then
        return false
    else
        log:error('get vlan is onboot failed')
        return false
    end
end

function c_handler_eth:update_physical_port_to_bridge_and_bonding(port)
    c_network_bridge.update_by_physical_port_name(port.Name, port.NodeId)
    c_network_bonding.update_by_physical_port_name(port.Name, port.NodeId)
end

local function update_macaddr_and_permanent_macaddr(port, data)
    -- 更新永久MAC 地址信息,如果永久mac已经有合法值,则不更新
    local MAC_ADDR_DEFAULT<const> = '00:00:00:00:00:00'
    local INVALID_DATA_STRING<const> = 'N/A'
    if port.PermanentMACAddress == MAC_ADDR_DEFAULT or
        port.PermanentMACAddress == INVALID_DATA_STRING then
        port.PermanentMACAddress = update_if_valid(port.PermanentMACAddress, string.upper(data.MACAddress))
    end

    -- 更新实际MAC地址,bma上报为小写,需转换为大写
    if data.MACAddress then
        port.MACAddress = update_if_valid(port.MACAddress, string.upper(data.MACAddress))
    end
end

function c_handler_eth:update_network_port(port, data, oem_huawei)
    if port and port.WorkloadType and port.WorkloadType == 1 then
        return
    end
    -- 更新OS侧网口名称
    log:info('update network port from bma: path=%s, @odata.id=%s', port.path, get_odata_id(data))

    port.Name = update_if_valid(port.Name, oem_huawei.NICName)
    self:update_physical_port_to_bridge_and_bonding(port)

    -- 更新OS侧网口工作状态,bma优先级最高，简单校验合法性后直接赋值
    local link_status = data.LinkStatus
    if link_status ~= nil and link_status ~= json.null and link_status ~= 'NoLink' and
        link_status ~= 'LinkUp' and link_status ~= 'LinkDown' then
        link_status = 'N/A'
    end
    port.LinkStatus = update_if_valid(port.LinkStatus, link_status)
    port:set_link_status_numeric(link_status)

    -- 更新BMA ID信息 @odata.id AgentID
    port.AgentID = update_if_valid(port.AgentID, get_odata_id(data))

    -- 更新OS侧网口速率
    port.SpeedMbps = update_if_valid(port.SpeedMbps, data.SpeedMbps)

    -- 更新OS侧自协商模式
    port.AutoSpeedNegotiation = update_if_valid(port.AutoSpeedNegotiation, data.AutoNeg)

    -- 更新OS侧网口IPv4信息
    local ipv4_address = prepare_ipv4_address(data.IPv4Addresses)
    self:update_ip_addresses(port, c_ipv4_address, ipv4_address)

    -- 更新OS侧网口IPv6信息
    local ipv6_address = prepare_ipv6_address(data.IPv6Addresses)
    self:update_ip_addresses(port, c_ipv6_address, ipv6_address)
    port:update_ipv6_default_gateway(ipv6_address)

    -- 更新永久mac地址和实际mac地址信息
    update_macaddr_and_permanent_macaddr(port, data)

    -- 更新BDF信息
    self:update_network_port_bdf(port, data, oem_huawei)

    -- 更新工作模式信息
    port.WorkMode = update_if_valid(port.WorkMode, oem_huawei.WorkMode)

    -- 更新team网口的链路监控频率
    port.LinkMonitorPeriodMS = update_number_if_valid(port.LinkMonitorPeriodMS, oem_huawei.MIILinkMonitoringFrequencyMS)

    -- 更新IB网口的UUID
    port.UUID = update_if_valid(port.UUID, oem_huawei.UUID)

    -- 更新IsOnBoot属性，网口和vlan共用
    port.AutoConnectOnBoot = self:get_vlan_is_onboot(port.AutoConnectOnBoot, oem_huawei.OnBoot)

    -- 更新网口信息
    port.FunctionType = update_if_valid(port.FunctionType, oem_huawei.PortType)

    -- 更新网络设备功能类型
    update_network_port_netdev_functype(port, data, NETDEV_FUNCTYPE_ETHERNET)

    -- 更新OS侧网卡固件版本
    port.FirmwareVersion = update_if_valid(port.FirmwareVersion, oem_huawei.FirmwareVersion)

    -- 更新OS侧网卡驱动名称
    if oem_huawei.DriverInfo ~= nil and oem_huawei.DriverInfo.DriverName ~= nil then
        port.DriverName = default_na(oem_huawei.DriverInfo.DriverName)
    end

    -- 更新OS侧网卡驱动版本
    if oem_huawei.DriverInfo ~= nil and oem_huawei.DriverInfo.DriverVersion ~= nil then
        port.DriverVersion = default_na(oem_huawei.DriverInfo.DriverVersion)
    end
end

function c_handler_eth:add(_, data, object)
    local oem_huawei = get_oem_huawei(data)
    if not oem_huawei then
        log:raise('c_handler_eth: prepare redfish data failed')
        return
    end

    -- 如果虚拟网口名称无效则直接返回OK不处理
    if not is_need_update(oem_huawei) then
        return
    end

    self:update_network_card(object, data, oem_huawei)
    self:update_network_port(object, data, oem_huawei)
end

function c_handler_eth:update_bridge_or_bonding_ports(object, data)
    if not data.Ports then
        return
    end
    local value = {
        Id = object.NetworkAdapterId,
        Ports = data.Ports
    }
    if object.Type == BUSINESSPORT_CARD_TYPE_BRIDGE then
        c_network_bridge.update_ports(value)
    end

    if object.Type == BUSINESSPORT_CARD_TYPE_TEAM then
        c_network_bonding.update_ports(value)
    end
end

function c_handler_eth:update(_, data, object)
    local oem_huawei = get_oem_huawei(data) or {}
    self:update_network_card(object, data, oem_huawei)
    self:update_network_port(object, data, oem_huawei)
    self:update_bridge_or_bonding_ports(object, data)
end

function c_handler_eth:delete_ip_address(port)
    local ipv4 = c_ipv4_address.get_addresses(port.PortID, port.NetworkAdapterId)
    for _, addr in ipairs(ipv4) do
        c_ipv4_address.collection:del_object(addr)
    end
    local ipv6 = c_ipv6_address.get_addresses(port.PortID, port.NetworkAdapterId)
    for _, addr in ipairs(ipv6) do
        c_ipv6_address.collection:del_object(addr)
    end
end

function c_handler_eth:reset_ip_address(port)
    local ipv4 = c_ipv4_address.get_addresses(port.PortID, port.NetworkAdapterId)
    for _, addr in ipairs(ipv4) do
        addr:dtor()
    end
    local ipv6 = c_ipv6_address.get_addresses(port.PortID, port.NetworkAdapterId)
    for _, addr in ipairs(ipv6) do
        addr:dtor()
    end
end

function c_handler_eth:reset_network_bridge_or_bonding(port, na)
    local id = port.NetworkAdapterId
    local obj = nil
    if port.Type == BUSINESSPORT_CARD_TYPE_BRIDGE then
        obj = c_network_bridge.collection:find({Id = id})
    elseif port.Type == BUSINESSPORT_CARD_TYPE_TEAM then
        obj = c_network_bonding.collection:find({Id = id})
    else
        return
    end
    if not obj then
        return
    end

    obj:destroy()
    port:destroy()
    na:destroy()
end

function c_handler_eth:reset_network_port(port)
    port:reset_bma_info()
    if port.Type == BUSINESSPORT_CARD_TYPE_VIR then
        port:destroy()
        return
    end

    local na = get_port_parent_card(port)
    if not na then
        log:debug('reset_network_port failed: path=%s, cannot find network card', port.path)
        return
    end
    na:reset_bma_info()
    self:reset_network_bridge_or_bonding(port, na)
end

function c_handler_eth:reset()
    for object, _ in pairs(self.objects) do
        self:reset_ip_address(object)
        self:reset_network_port(object)
    end

    self.objects = {}
end

function c_handler_eth:delete(_, _, object)
    self:reset_network_port(object)
    self:delete_ip_address(object)
    self.objects[object] = nil
end

function c_handler_eth:check_oam_lost_link_state(data)
    if not data[ODATA_TYPE] or not data[ODATA_TYPE]:find('OemEthernetInterfaceOAM') then
        return
    end
    local id = data['Id']
    if not c_network_port.port_register then
        oam_data_cache_list[id] = data
        return
    end
    oam_data_cache_list[id] = nil -- 防止重入
    local state = data['OAMLostLinkState']
    local bdf = string.sub(id, string.find(id, "_") + 1)
    -- 匹配网口
    local port_obj = c_network_port.collection:find({ BDF = bdf })
    if not port_obj then
        log:debug('check oam lost link state failed [not port object]')
        return
    end
    local parent = port_obj:get_parent()
    local adapter_obj = c_network_adapter.collection:find({ NodeId = parent.NodeId })
    if not adapter_obj then
        log:debug('check oam lost link state failed [not adapter object]')
        return
    end
    state = (state == json.null or not state) and 0 or tonumber(state)         -- BMA数据异常看做0
    event_mgmt:check_oam_lost_link_state_alarm(state, adapter_obj.DeviceLocator, tostring(port_obj.PortID))
end

function c_handler_eth:handle_cache_data()
    if #oam_data_cache_list == 0 then
        return
    end
    for id, data in pairs(oam_data_cache_list) do
        if self:check_oam_lost_link_state(data) then
            oam_data_cache_list[id] = nil
        end
    end
end

return singleton(c_handler_eth)
