-- 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 skynet = require 'skynet'
local json = require 'cjson'
local ipmi_struct = require 'network_adapter.ipmi.ipmi'
local client = require 'network_adapter.client'
local c_ncsi_channel_mgmt = require 'ncsi.ncsi_channel_mgmt'
local c_ncsi_nc_info = require 'ncsi.ncsi_ncinfo'
local ncsi_cmd = require 'ncsi.ncsi_protocol.ncsi_cmd'
local custom_messages = require 'messages.custom'
local log = require 'mc.logging'
local ipmi = require 'ipmi'
local ncsi_info = require 'ncsi.ncsi_info'
local ncsi_comm = require 'ncsi.ncsi_comm'
local ncsi_worker = require 'ncsi.ncsi_worker'
local ncsi_set_worker = require 'ncsi.ncsi_set_worker'
local lldp = require 'ncsi.ncsi_lldp'

local cc = ipmi.types.Cc

local PORT_TYPE_OCP = 'Ocp'
local PORT_TYPE_OCP2 = 'Ocp2'
local NCSI_BASIC_INIT_MAX_RETRY<const> = 18
local MANUFACTURE_ID<const> = 0x0007db
local NCSI_CHANEL_TO_OCP1 = 2
local NCSI_CHANEL_TO_OCP2 = 3
local MAX_OCP_PRESENCE_CNT = 6

local ncsi_service = {}
ncsi_service.__index = ncsi_service

function ncsi_service.new(db, bus)
    return setmetatable({
        db = db,
        bus = bus,
        ncsi_info = ncsi_info.new(db, bus),
        work = ncsi_worker.new(),
        set_work = ncsi_set_worker.new(),
        ncsi_comm = ncsi_comm.new(db, bus),
        lldp = lldp.new(db),
        ocp_presence_cnt = 0,
        init_ocp1_flag = false,
        init_ocp2_flag = false
    }, ncsi_service)
end

function ncsi_service:ncsi_channel_init()
    local retry_times = 0
    while retry_times < NCSI_BASIC_INIT_MAX_RETRY do
        if self.ncsi_comm:ncsi_basic_init(self.db) then
            break
        end
        skynet.sleep(1000)
        retry_times = retry_times + 1
        log:error('ncsi baisc init failed, retry times = %d', retry_times)
    end
    self.ncsi_info:ncsi_info_init()
    self.ncsi_comm:ncsi_init(self.db, self.bus)
end

local function update_port_type(db, type)
    local nc_info = db:select(db.NcsiNCInfo):first()
    local port_info
    for id = 1, nc_info.PortNum do
        port_info = db:select(db.NcsiNCPortInfo):where({PortId = id}):first()
        port_info.Type = type
        port_info:save()
    end
end

local function init_ocp1_channel_finish(self, ncsi_channel_mgmt)
    if not ncsi_channel_mgmt or ncsi_channel_mgmt.OCP1Presence ~= 1 then
        return false
    end

    if not self.init_ocp1_flag then
        log:notice('set ncsi channel to ocp1 start')
        ncsi_channel_mgmt.NCSIChannel = NCSI_CHANEL_TO_OCP1
        update_port_type(self.db, PORT_TYPE_OCP)
        self:ncsi_channel_init()
        self.init_ocp1_flag = true
    end

    if self.ocp_presence_cnt >= MAX_OCP_PRESENCE_CNT then
        return true
    end
    self.ocp_presence_cnt = self.ocp_presence_cnt + 1
    return false
end

local function init_ocp2_channel_finish(self, ncsi_channel_mgmt)
    --  OCP1在位，不用处理OCP2在位状态
    if not ncsi_channel_mgmt or ncsi_channel_mgmt.OCP1Presence == 1 or
        ncsi_channel_mgmt.OCP2Presence ~= 1 then
        return false
    end

    if not self.init_ocp2_flag then
        log:notice('set ncsi channel to ocp2 start')
        ncsi_channel_mgmt.NCSIChannel = NCSI_CHANEL_TO_OCP2
        update_port_type(self.db, PORT_TYPE_OCP2)
        self:ncsi_channel_init()
        self.init_ocp1_flag = false
        self.init_ocp2_flag = true
    end

    if self.ocp_presence_cnt >= MAX_OCP_PRESENCE_CNT then
        return true
    end
    self.ocp_presence_cnt = self.ocp_presence_cnt + 1
    return false
end

-- 根据网卡在位情况设置NCSI功能通路,优先级PCIe > OCP1 > OCP2
function ncsi_service:set_ncsi_channel_task()
    local ncsi_channel_mgmt
    while true do
        ncsi_channel_mgmt = c_ncsi_channel_mgmt.collection:find({Id = 1})
        if ncsi_channel_mgmt and ncsi_channel_mgmt.PCIePresence == 1 then
            log:notice('set ncsi channel to pcie')
            return
        end
        if init_ocp1_channel_finish(self, ncsi_channel_mgmt) then
            log:notice('set ncsi channel to ocp1 successfully')
            return
        end
        if init_ocp2_channel_finish(self, ncsi_channel_mgmt) then
            log:notice('set ncsi channel to ocp2 successfully')
            return
        end
        skynet.sleep(500)
    end
end

--[[ 根据platform.sr中配置的产品差异化进行EthId和EthName设置
如果未配置则最多等待180s，不影响原ncsi初始化流程
如果找到配置对象则复写EthId，重新进行ncsi初始化 ]]
function ncsi_service:set_ncsi_ethid_task()
    local retry_times = 0
    local nc_info
    local is_found_ncsincinfo_obj = false
    while retry_times < 180 and not is_found_ncsincinfo_obj do
        c_ncsi_nc_info.collection:fold(function(_, obj)
            if obj.NcsiEthName and obj.NcsiEthName ~= '' then
                nc_info = self.db:select(self.db.NcsiNCInfo):first()
                nc_info.NcsiEthName = obj.NcsiEthName
                nc_info:save()
                is_found_ncsincinfo_obj = true
            end
            if obj.NcsiEthId then
                is_found_ncsincinfo_obj = true
                log:notice('[ncsi service] find NcsiNCInfo obj')
                nc_info = self.db:select(self.db.NcsiNCInfo):first()
                nc_info.EthId = obj.NcsiEthId
                nc_info:save()
                log:notice('[ncsi service] set eth id to %s success', obj.NcsiEthId)
                local init_retry_times = 0
                while init_retry_times < NCSI_BASIC_INIT_MAX_RETRY do
                    if self.ncsi_comm:ncsi_basic_init(self.db) then
                        break
                    end
                    skynet.sleep(1000)
                    init_retry_times = init_retry_times + 1
                    log:error('ncsi basic init failed, retry times: %d', init_retry_times)
                end
            end
        end)
        skynet.sleep(100)
        retry_times = retry_times + 1
    end
end

function ncsi_service:init()
    skynet.fork(function ()
        self:set_ncsi_channel_task()
    end)
    skynet.fork(function ()
        self:set_ncsi_ethid_task()
    end)
    self.ncsi_info:wait_bmc_network_service()
    self.work:start_ncsi_recv_task(self.db)
    skynet.fork(function()
        local retry_times = 0
        ncsi_cmd.ncsi_new_parameter()
        -- 最多尝试3分钟，超过则认为ncsi初始化失败，当前环境不支持NCSI功能
        while retry_times < NCSI_BASIC_INIT_MAX_RETRY do
            if self.ncsi_comm:ncsi_basic_init(self.db) then
                break
            end
            skynet.sleep(1000) -- 间隔10s
            retry_times = retry_times + 1
            log:error('ncsi basic init failed, retry times: %d', retry_times)
        end
        self.ncsi_info:ncsi_info_init()
        self.ncsi_comm:ncsi_init(self.db, self.bus)
    end)
    self:register_signal_change_method()
    pcall(function()
        skynet.register_protocol({
            name = 'network_adapter_ncsi_reinit',
            id = 132,  -- 此id仅本模块使用，不重复即可
            pack = skynet.pack,
            unpack = skynet.unpack
        })
    end)
    skynet.dispatch('network_adapter_ncsi_reinit', function()
        log:error('receive network_adapter_ncsi_reinit, relink emit')
        self.ncsi_comm.sig_relink_ncsi_port:emit()
    end)
end

local NCSI_RX_CMD_HEAD_LEN<const> = 2
local NCSI_RX_2PORTS_DATA_LEN<const> = 4
local NCSI_RX_4PORTS_DATA_LEN<const> = 6

local NETWORK_MSG_SET_NCSI_RX_CHANNEL_STATE<const> = 13

local IPMI_LOM_NCSI_TYPE<const> = 0
local IPMI_PCIE_NCSI_TYPE<const> = 1
local IPMI_MANAGEMENT_TYPE<const> = 2
local IPMI_LOM2_NCSI_TYPE<const> = 3
local IPMI_OCP_NCSI_TYPE<const> = 4
local IPMI_OCP2_NCSI_TYPE<const> = 5
local IPMI_CARD_MAX_TYPE<const> = 6

local INVALID_VALUE<const> = 0xff

local ENABLED<const> = 1
local DISABLED<const> = 0

local IPMI_CARD_TYPE_STR_TO_NUM<const> = {
    Lom = IPMI_LOM_NCSI_TYPE,
    ExternalPCIe = IPMI_PCIE_NCSI_TYPE,
    Dedicated = IPMI_MANAGEMENT_TYPE,
    Lom2 = IPMI_LOM2_NCSI_TYPE,
    Ocp = IPMI_OCP_NCSI_TYPE,
    Ocp2 = IPMI_OCP2_NCSI_TYPE
}

local IPMI_CARD_TYPE_STR<const> = {
    [IPMI_LOM_NCSI_TYPE] = 'Lom',
    [IPMI_PCIE_NCSI_TYPE] = 'ExternalPCIe',
    [IPMI_MANAGEMENT_TYPE] = 'Dedicated',
    [IPMI_LOM2_NCSI_TYPE] = 'Lom2',
    [IPMI_OCP_NCSI_TYPE] = 'Ocp',
    [IPMI_OCP2_NCSI_TYPE] = 'Ocp2'
}

local function check_set_ncsi_rx_channel_data(ncsi_rx_channel_state, ipmi_card_type, port1_num, port2_num)
    if ncsi_rx_channel_state ~= DISABLED and ncsi_rx_channel_state ~= ENABLED then
        return false
    end

    if ipmi_card_type == IPMI_MANAGEMENT_TYPE then
        return false
    end

    if port1_num == port2_num and port1_num ~= INVALID_VALUE then
        return false
    end

    if ncsi_rx_channel_state == ENABLED and ipmi_card_type >= IPMI_CARD_MAX_TYPE then
        return false
    end

    return true
end

local function check_ncsi_port_exists(db, ipmi_card_type, port_num)
    -- MgmtPortId非0表示端口存在
    local obj = db:select(db.NcsiNCPortInfo):where(db.NcsiNCPortInfo.Type:eq(IPMI_CARD_TYPE_STR[ipmi_card_type]),
        db.NcsiNCPortInfo.PortId:eq(port_num), db.NcsiNCPortInfo.MgmtPortId:ne(0)):first()
    return obj ~= nil
end

local function check_valid_port_for_set_ncsi_rx(db, data, ipmi_card_type, data_len)
    local port_index
    local port_cnt = data_len - NCSI_RX_CMD_HEAD_LEN
    local port_num
    for i = 1, port_cnt do
        port_index = NCSI_RX_CMD_HEAD_LEN + i
        port_num = data:byte(port_index)
        -- ipmi命令请求的端口号需要加一对应属性序号
        if port_num == INVALID_VALUE or (not check_ncsi_port_exists(db, ipmi_card_type, port_num + 1)) then
            return false
        end
    end

    return true
end

local function set_ncsi_rx_channel_state_prop(ncsi_rx_channel_state, ipmi_card_type, ipmi_ports, port_obj)
    if ncsi_rx_channel_state == DISABLED then
        port_obj.RxEnable = false
        return
    end

    local type_str = IPMI_CARD_TYPE_STR[ipmi_card_type]
    for _, port_num in pairs(ipmi_ports) do
        -- ipmi命令请求的端口号需要加一对应属性序号
        if port_obj.Type == type_str and port_obj.PortId == port_num + 1 then
            port_obj.RxEnable = true
            return
        end
    end
    port_obj.RxEnable = false
end

function ncsi_service:set_nsci_rx_msg(ncsi_rx_channel_state, ipmi_card_type, ipmi_ports)
    -- 获取当前使用的端口id
    local used_id
    client:ForeachEthernetInterfacesObjects(function(obj)
        used_id = obj.PortId
    end)

    -- 根据端口id获取当前使用的网口信息
    local used_obj = used_id and
        self.db:select(self.db.NcsiNCPortInfo):where(self.db.NcsiNCPortInfo.MgmtPortId:eq(used_id)):first()
    local used_port = used_obj and {type = used_obj.Type, port_num = used_obj.PortId} or {}

    -- 遍历收集所有可用的网口，并且设置网口的RX使能属性
    local db_ports = {}
    self.db:select(self.db.NcsiNCPortInfo):fold(function(port_obj)
        db_ports[port_obj.PortId] = {
            type = port_obj.Type,
            port_num = port_obj.PortId,
            package_id = port_obj.PackageId,
            channel_id = port_obj.ChannelId
        }

        -- 当前使用的网口不禁用
        if ncsi_rx_channel_state == DISABLED and used_port and port_obj.Type == used_port.type and
            port_obj.PortId == used_port.port_num then
            return
        end

        set_ncsi_rx_channel_state_prop(ncsi_rx_channel_state, ipmi_card_type, ipmi_ports, port_obj)
    end)

    local network_msg = {
        cmd = NETWORK_MSG_SET_NCSI_RX_CHANNEL_STATE,
        ncsi_rx_channel_state = ncsi_rx_channel_state,
        ipmi_card_type = ipmi_card_type,
        ipmi_ports = ipmi_ports,
        db_ports = db_ports,
        used_port = used_port
    }
    return self.set_work:send(json.encode(network_msg))
end

function ncsi_service:set_ncsi_rx_channel_from_ipmi(req, ctx)
    local nc_info = self.db:select(self.db.NcsiNCInfo):first()
    if nc_info == nil or not nc_info.Enable then
        log:error('NC-SI is not supported')
        ipmi.ipmi_operation_log(ctx, 'NetworkAdapter', 'Set NC-SI RX channels failed');
        error(custom_messages.IPMISubfuncInvalid())
    end
    local data_len = req.Length
    if data_len ~= NCSI_RX_2PORTS_DATA_LEN and data_len ~= NCSI_RX_4PORTS_DATA_LEN then
        ipmi.ipmi_operation_log(ctx, 'NetworkAdapter', 'Set NC-SI RX channels failed');
        error(custom_messages.IPMIInvalidFieldRequest())
    end

    if req.Flag ~= 0 or req.Offset ~= 0 then
        ipmi.ipmi_operation_log(ctx, 'NetworkAdapter', 'Set NC-SI RX channels failed');
        error(custom_messages.IPMIInvalidFieldRequest())
    end

    local ncsi_rx_channel_state, ipmi_card_type, port1_num, port2_num = req.Data:byte(1, 4)
    if not check_set_ncsi_rx_channel_data(ncsi_rx_channel_state, ipmi_card_type, port1_num, port2_num) then
        ipmi.ipmi_operation_log(ctx, 'NetworkAdapter', 'Set NC-SI RX channels failed');
        error(custom_messages.IPMIInvalidFieldRequest())
    end

    if ncsi_rx_channel_state == ENABLED and
        (not check_valid_port_for_set_ncsi_rx(self.db, req.Data, ipmi_card_type, data_len)) then
        ipmi.ipmi_operation_log(ctx, 'NetworkAdapter', 'Set NC-SI RX channels failed');
        error(custom_messages.IPMIInvalidFieldRequest())
    end

    local port3_num = req.Data:byte(5) or INVALID_VALUE
    local port4_num = req.Data:byte(6) or INVALID_VALUE
    if not self:set_nsci_rx_msg(ncsi_rx_channel_state, ipmi_card_type,
        {port1_num, port2_num, port3_num, port4_num}) then
        ipmi.ipmi_operation_log(ctx, 'NetworkAdapter', 'Set NC-SI RX channels failed');
        error(custom_messages.IPMICommandResponseCannotProvide())
    end

    if ncsi_rx_channel_state == ENABLED then
        if data_len == NCSI_RX_4PORTS_DATA_LEN then
            ipmi.ipmi_operation_log(ctx, 'NetworkAdapter',
                'Enabled NC-SI RX channels for %s ports %d and %d and %d and %d successfully',
                IPMI_CARD_TYPE_STR[ipmi_card_type] or '', port1_num + 1, port2_num + 1, port3_num + 1, port4_num + 1)
        else
            ipmi.ipmi_operation_log(ctx, 'NetworkAdapter',
                'Enabled NC-SI RX channels for %s ports %d and %d successfully',
                IPMI_CARD_TYPE_STR[ipmi_card_type] or '', port1_num + 1, port2_num + 1)
        end
    else
        ipmi.ipmi_operation_log(ctx, 'NetworkAdapter', 'Disabled NC-SI RX channels for all inactive ports successfully')
    end
    local rsp = ipmi_struct.SetIPMCConfiguration.rsp
    rsp.CompletionCode = cc.Success
    rsp.ManufactureId = MANUFACTURE_ID
    return rsp
end

local function get_ncsi_channel_state(db, rsp, data_len)
    local ncsi_rx_channel_state = DISABLED
    local ipmi_card_type = INVALID_VALUE
    local ports = {}
    db:select(db.NcsiNCPortInfo):fold(function(port_obj)
        if not port_obj.RxEnable or #ports >= data_len - NCSI_RX_CMD_HEAD_LEN then
            return
        end

        ncsi_rx_channel_state = ENABLED
        ipmi_card_type = IPMI_CARD_TYPE_STR_TO_NUM[port_obj.Type]
        table.insert(ports, port_obj.PortId - 1)
    end)

    while #ports < data_len - NCSI_RX_CMD_HEAD_LEN do
        table.insert(ports, INVALID_VALUE)
    end

    rsp.Data = string.char(ncsi_rx_channel_state, ipmi_card_type) .. string.char(table.unpack(ports))
end

function ncsi_service:get_ncsi_rx_channel_from_ipmi(req, ctx)
    local data_len = req.Length
    if req.Offset ~= 0 or data_len ~= NCSI_RX_2PORTS_DATA_LEN and data_len ~= NCSI_RX_4PORTS_DATA_LEN then
        log:debug('Invalid parameter,offset=%d,length=%d', req.Offset, data_len)
        error(custom_messages.IPMIInvalidFieldRequest())
    end

    local rsp = ipmi_struct.GetIPMCConfiguration.rsp
    get_ncsi_channel_state(self.db, rsp, data_len)
    rsp.CompletionCode = cc.Success
    rsp.ManufactureId = MANUFACTURE_ID
    return rsp
end

function ncsi_service:register_signal_change_method()
    self.ncsi_info.sig_mac_addr_changed:on(function(new_mac_addr)
        log:notice('[ncsi_service], sig mac addr changed, new_mac_addr=%s.', new_mac_addr)
        self.ncsi_comm:ncsi_sync_eth_mac(self.db, new_mac_addr)
        self.ncsi_comm:ncsi_init(self.db, self.bus)
    end)

    self.ncsi_info.sig_vlan_info_changed:on(function()
        log:notice('[ncsi_service], sig vlan info changed.')
        self.ncsi_comm:ncsi_set_vlan_info(self.db)
    end)

    self.ncsi_info.sig_eth_intf_add:on(function()
        log:notice('[ncsi_service], sig eth intf add.')
        skynet.fork(function()
            local retry_times = 0
            while retry_times < NCSI_BASIC_INIT_MAX_RETRY do
                if self.ncsi_comm:ncsi_basic_init(self.db) then
                    break
                end
                skynet.sleep(1000)
                retry_times = retry_times + 1
                log:error('ncsi basic init failed, retry times: %d', retry_times)
            end
            self.ncsi_info:ncsi_info_init()
            self.ncsi_comm:ncsi_init(self.db, self.bus)
        end)
    end)

    self.ncsi_info.sig_multi_vlan_added:on(function(vlan_id)
        log:notice('[ncsi_service], sig multi vlan add. %s', vlan_id)
        self.ncsi_comm:ncsi_update_multi_vlan(self.db, vlan_id)
    end)

    self.ncsi_comm.sig_update_link_status:on(function(port_id, link_status)
        log:debug('[ncsi_service], sig update link status, port_id=%d, link_status=%s.', port_id, link_status)
        self.ncsi_info:ncsi_uptade_link_status(port_id, link_status)
    end)

    self.ncsi_comm.sig_relink_ncsi_port:on(function()
        log:notice('[ncsi_service], sig relink ncsi port.')
        self.ncsi_comm:ncsi_init(self.db, self.bus)
    end)
end

return ncsi_service
