-- 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 log = require 'mc.logging'
local ncsi_core = require 'ncsi.ncsi_core'
local signal = require 'mc.signal'
local network = require 'network.core'
local singleton = require 'mc.singleton'
local mdb = require 'mc.mdb'

local NCSI_BASIC_INIT_RETRY_TIME = 3
local NCSI_DISABLED = 0
local NCSI_ENABLED = 1
local DPU_CARD_PATH<const> = '/bmc/kepler/Systems/1/PCIeDevices/PCIeCards/DPUCards'
local DPU_CARD_INTERFACE<const> = 'bmc.kepler.Systems.DPUCard'
local PCIE_CARD_INTERFACE<const> = 'bmc.kepler.Systems.PCIeDevices.PCIeCard'
local NCSI_BASIC_INIT_MAX_RETRY<const> = 18
local NCSI_DISABLED_CHANNELTX_MAX_RETRY<const> = 10

local ncsi_comm = {}
ncsi_comm.__index = ncsi_comm

function ncsi_comm.new(db, bus)
    return setmetatable({
        db = db,
        bus = bus,
        sig_update_link_status = signal.new(),
        sig_relink_ncsi_port = signal.new(),
        check_ncsi_link_co = nil,
        ncsi_channel_fail_cnt = {},
        multi_vlan = {},
        first_init = true, -- 仅在第一次初始化失败的时候设置PortAvailable为false
        ncsi_channel_cnt = 0
    }, ncsi_comm)
end

function ncsi_comm:updata_link_status(db, port_id, link_status)
    local old_link_status =
        db:select(db.NcsiNCPortInfo):where({PortId = port_id}):first().LinkStatus
    local link_status_str = (link_status == 1 and 'Connected' or 'Disconnected')
    log:debug('ncsi: Port%d link status changed(%s > %s)', port_id, old_link_status, link_status_str)
    self.sig_update_link_status:emit(port_id, link_status_str)
end

function ncsi_comm:process_get_link_status_failed(db, ncsi_channel, id, is_sdi_power_off)
    local MAX_NCSI_CHANNEL_FAIL_CNT<const> = 3
    self.ncsi_channel_fail_cnt[ncsi_channel] =
        (self.ncsi_channel_fail_cnt[ncsi_channel] or 0) + 1
    log:debug('ncsi: Get link status fail.')
    if self.ncsi_channel_fail_cnt[ncsi_channel] > MAX_NCSI_CHANNEL_FAIL_CNT then
        self:updata_link_status(db, id, 0)
        if not is_sdi_power_off then
            self.ncsi_channel_fail_cnt[ncsi_channel] = 0
            log:notice('Failed to obtain LinkStatus for 3 consecutive times, need to relink.')
            self.sig_relink_ncsi_port:emit()
        end
    end
end

function ncsi_comm:ncsi_get_nc_port_link_status(db, port_num, eth_id, eth_name, is_sdi_power_off)
    local port_available, package_id, channel_id, ret, link_status
    for id = 1, port_num do
        port_available =
            db:select(db.NcsiNCPortInfo):where({PortId = id}):first().PortAvailable
        if not port_available then
            return
        end
        package_id = db:select(db.NcsiNCPortInfo):where({PortId = id}):first().PackageId
        channel_id = db:select(db.NcsiNCPortInfo):where({PortId = id}):first().ChannelId
        ret, link_status = ncsi_core.ncsi_get_link_status(package_id, channel_id, eth_name,
            eth_id)
        if ret == false then
            self:process_get_link_status_failed(db, eth_id .. channel_id, id, is_sdi_power_off)
        else
            self.ncsi_channel_fail_cnt[eth_id .. channel_id] = 0
            self:updata_link_status(db, id, link_status)
            log:debug('ncsi: Get NC port%d link %s', id, (link_status == 1 and 'up' or 'down'))
        end
    end
end

-- 仅当环境上有SDI卡，且所有SDI卡下电时返回true，其余返回false
function ncsi_comm:check_sdi_power_off_and_skip(bus)
    local ok, obj_list = pcall(mdb.get_sub_objects, bus, DPU_CARD_PATH, PCIE_CARD_INTERFACE)
    local ret, dpu_obj
    if not ok or not next(obj_list) then
        return false
    end
    for _, obj in pairs(obj_list) do
        ret, dpu_obj = pcall(mdb.get_object, bus, obj.path, DPU_CARD_INTERFACE)
        if not ret then
            return false
        end
        if dpu_obj.SystemLoadedStatus == 3 then
            return false
        end
    end
    return true
end

function ncsi_comm:check_ncsi_link_task(db, bus)
    local is_sdi_power_off, ok, err, eth_id, eth_name, port_num
    while true do
        is_sdi_power_off = self:check_sdi_power_off_and_skip(bus)
        ok, err = pcall(function ()
            eth_id = db:select(db.NcsiNCInfo):first().EthId
            eth_name = 'eth' .. eth_id
            port_num = db:select(db.NcsiNCInfo):first().PortNum

            self:ncsi_get_nc_port_link_status(db, port_num, eth_id, eth_name, is_sdi_power_off)
        end)
        if not ok then
            log:debug('check_ncsi_link_task failed and return %s', err)
        end
        skynet.sleep(500)
    end
end

local function new_ifreq(eth_name)
    local req = network.ifreq.new()
    req:reset_zero()
    req.ifr_name = eth_name
    return req
end
local function get_eth_up_status(fd, eth_name)
    local ifreq = new_ifreq(eth_name)
    if ifreq:ioctl(fd, network.SIOCGIFFLAGS) == -1 then
        log:notice('ncsi: Get %s link status fail.', eth_name)
        return false
    end
    if (ifreq.ifr_flags & network.IFF_UP) == 1 then
        return true
    end
    return false
end
function ncsi_comm:ncsi_wait_eth_interface_up(eth_name)
    local CHECK_ETH_UP_TIMES = 60 -- 最多等待60s，网口不up，认为不支持ncsi
    local fd = network.socket(network.AF_INET, network.SOCK_DGRAM, 0)
    local ret
    if fd == -1 then
        local errno, errmsg = network.get_error()
        error(string.format('%s: %s[%d]', 'new socket failed', errmsg, errno))
    end

    for _ = 1, CHECK_ETH_UP_TIMES do
        ret = get_eth_up_status(fd, eth_name)
        if ret then
            network.close(fd)
            return true
        end
        skynet.sleep(100)
    end
    network.close(fd)
    return false
end

function ncsi_comm:ncsi_port_basic_init(db, port_num, eth_name)
    local port_info, package_id, ret
    local success_port_cnt = 0
    for id = 1, port_num do
        port_info = db:select(db.NcsiNCPortInfo):where({PortId = id}):first()
        for _ = 1, NCSI_BASIC_INIT_RETRY_TIME do
            _, package_id = ncsi_core.ncsi_paramter_init(port_info.ChannelId, eth_name,
                NCSI_DISABLED)
            ret = ncsi_core.ncsi_enable_channelTX(package_id, port_info.ChannelId, eth_name)
            if ret then
                log:notice('ncsi: Basic init success.(package_id = %d, channel_id = %d)',
                    package_id, port_info.ChannelId)
                port_info.PackageId = package_id
                port_info.PortAvailable = true
                success_port_cnt = success_port_cnt + 1
                ncsi_core.ncsi_disable_channelTX(package_id, port_info.ChannelId, eth_name)
                break
            end
            log:notice('ncsi port baisc init failed.(package_id = %d, channel_id = %d)',
                package_id, port_info.ChannelId)
            if self.first_init then
                port_info.PortAvailable = false
            end
        end
        port_info:save()
    end
    local ncsi_channel_cnt = self:get_ncsi_channel_cnt()
    if ncsi_channel_cnt then
        self.ncsi_channel_cnt = ncsi_channel_cnt
    end
    self.first_init = false
    return success_port_cnt
end

local function ncsi_enable_available_port_rx(db, port_num, eth_name)
    local port_info
    local ret
    for id = 1, port_num do
        port_info = db:select(db.NcsiNCPortInfo):where({PortId = id}):first()
        ret = ncsi_core.ncsi_enable_channel(port_info.PackageId, port_info.ChannelId, eth_name)
        if ret then
            log:notice('ncsi: Set RXEnable success,package_id = %s,channel_id = %s)',
                port_info.PackageId, port_info.ChannelId)
            port_info.RxEnable = true
        end
        ncsi_core.ncsi_disable_channelTX(port_info.PackageId, port_info.ChannelId, eth_name)
        -- 禁用广播过滤和多播过滤，避免收不到广播包和多播包
        ncsi_core.ncsi_disable_brdcast_filter(port_info.PackageId, port_info.ChannelId, eth_name)
        ncsi_core.ncsi_disable_multi_filter(port_info.PackageId, port_info.ChannelId, eth_name)
    end
end

function ncsi_comm:ncsi_check_eth_interface_up(eth_name)
    local fd = network.socket(network.AF_INET, network.SOCK_DGRAM, 0)
    if fd == -1 then
        local errno, errmsg = network.get_error()
        error(string.format('%s: %s[%d]', 'new socket failed', errmsg, errno))
    end
    local ret = get_eth_up_status(fd, eth_name)
    if ret then
        network.close(fd)
        return true
    end
    network.close(fd)
    return false
end

function ncsi_comm:ncsi_relink_init(db)
    local nc_info = db:select(db.NcsiNCInfo):first()
    local eth_name = 'eth' .. (nc_info and nc_info.EthId or '0')

    log:notice('ncsi: Basic init.')

    local link_status = self:ncsi_check_eth_interface_up(eth_name)
    if not link_status then
        log:notice('ncsi: %s interface is not link up', eth_name)
        local port_infos = db:select(db.NcsiNCPortInfo):all()
        for _, port_info in pairs(port_infos) do
            port_info.PortAvailable = false
            port_info:save()
            log:notice('ncsi: %s is not available', port_info.Slikscreen)
        end
        return false
    end

    self:ncsi_port_basic_init(db, nc_info.PortNum, eth_name)
    ncsi_enable_available_port_rx(db, nc_info.PortNum, eth_name)
    return true
end

function ncsi_comm:ncsi_basic_init(db)
    local nc_info = db:select(db.NcsiNCInfo):first()
    local eth_name = 'eth' .. (nc_info and nc_info.EthId or '0')

    log:notice('ncsi: Basic init, eth name = %s', eth_name)

    local link_status = self:ncsi_wait_eth_interface_up(eth_name)
    if not link_status then
        log:notice('ncsi: %s interface is not link up', eth_name)
        local port_infos = db:select(db.NcsiNCPortInfo):all()
        for _, port_info in pairs(port_infos) do
            port_info.PortAvailable = false
            port_info:save()
            log:notice('ncsi: %s is not available', port_info.Slikscreen)
        end
        return false
    end
    local port_num = nc_info.PortNum
    -- 如果不是第一次初始化，ncsi_channel_cnt不为0则表示获取到了实际通道数，则以实际通道数为准，优化循环次数
    if not self.first_init and self.ncsi_channel_cnt ~= 0 then
        port_num =  self.ncsi_channel_cnt
    end
    local success_port_cnt = self:ncsi_port_basic_init(db, port_num, eth_name)
    log:notice('ncsi: Basic init success_port_cnt = %d, port_num = %d', success_port_cnt, port_num)
    ncsi_enable_available_port_rx(db, port_num, eth_name)
    if self.ncsi_channel_cnt ~= 0 then
        return self.ncsi_channel_cnt == success_port_cnt
    end
    return success_port_cnt == port_num
end

local function ncsi_get_db_nc_info(db, port_id)
    local nc_info = db:select(db.NcsiNCInfo):first()
    local port_info = db:select(db.NcsiNCPortInfo):where({PortId = port_id}):first()
    if nc_info and port_info then
        return true, nc_info, port_info
    end
    log:error('ncsi: Get db nc info fail.(port_id = %d)', port_id)
    return false, nil, nil
end

function ncsi_comm:ncsi_comm_set_nc_vlan(db, port_id, ori_vlan_id, vlan_selector)
    local ret, nc_info, port_info = ncsi_get_db_nc_info(db, port_id)
    if not ret then
        return false
    end
    local eth_name = 'eth' .. nc_info.EthId
    local package_id = port_info.PackageId
    local vlan_state = (nc_info.VlanState and 1 or 0)
    local vlan_id = ori_vlan_id and ori_vlan_id or nc_info.VlanId
    local vlan_filter_selector = vlan_selector and vlan_selector or 1 -- 1 : default vlan_id

    log:notice('ncsi: Set network controller vlan.(vlan_state = %d, vlan_id = %d)', vlan_state,
        vlan_id)

    -- 遍历所有网口，设置vlan
    local port_infos = db:select(db.NcsiNCPortInfo):all()
    local result = true
    for _, port in pairs(port_infos) do
        if port.PortAvailable and
            not ncsi_core.ncsi_set_vlan_filter(package_id, port.ChannelId, vlan_filter_selector,
            vlan_state, vlan_id, eth_name) then
            log:notice('ncsi: Set vlan failed, channelId = %s', port.ChannelId)
            result = false
        end
    end
    return result
end

function ncsi_comm:ncsi_comm_set_nc_multi_vlan(db, port_id)
    local ret, nc_info, port_info = ncsi_get_db_nc_info(db, port_id)
    if not ret then
        return false
    end
    local eth_name = 'eth' .. nc_info.EthId
    local channel_id = port_info.ChannelId
    local package_id = port_info.PackageId
    local vlan_state = 1 -- 1: multi vlan 总是使能状态,不受对外ncsi vlan使能状态影响
    local vlan_filter_selector
    for idx, vlan_id in ipairs(self.multi_vlan) do
        log:notice('ncsi:set multi vlan id %s', vlan_id)
        vlan_filter_selector = idx + 1 -- vlan selector 1 默认用于对外NCSI vlan id过滤
        ncsi_core.ncsi_set_vlan_filter(package_id, channel_id, vlan_filter_selector,
            vlan_state, vlan_id, eth_name)
    end
end

function ncsi_comm:ncsi_active_nc_port(db, active_port_id)
    local ret, nc_info, port_info = ncsi_get_db_nc_info(db, active_port_id)
    if not ret then
        log:notice('ncsi: Active port id is invalid.(port_id = %d)', active_port_id)
        return false
    end
    local eth_name = 'eth' .. nc_info.EthId
    local channel_id = port_info.ChannelId
    local package_id = port_info.PackageId

    log:notice('ncsi: Active network controller port(%d)', active_port_id)

    ncsi_core.ncsi_paramter_init(channel_id, eth_name, NCSI_ENABLED)

    ret = ncsi_core.ncsi_enable_channel(package_id, channel_id, eth_name)
    if not ret then
        return false
    end

    ret = ncsi_core.ncsi_enable_channelTX(package_id, channel_id, eth_name)
    if not ret then
        return false
    end

    ret = ncsi_core.ncsi_disable_brdcast_filter(package_id, channel_id, eth_name)
    if not ret then
        return false
    end

    ret = ncsi_core.ncsi_disable_multi_filter(package_id, channel_id, eth_name)
    if not ret then
        log:notice('ncsi disable multi filter failed, not return false')
    end

    self:ncsi_comm_set_nc_vlan(db, active_port_id)
    self:ncsi_comm_set_nc_multi_vlan(db, active_port_id)
    return true
end

local function ncsi_deactive_nc_port(db, deactive_port)
    local nc_info = db:select(db.NcsiNCInfo):first()
    local port_info = db:select(db.NcsiNCPortInfo):where({PortId = deactive_port}):first()
    local eth_name = 'eth' .. (nc_info and nc_info.EthId or '0')
    if port_info == nil then
        log:error('ncsi: Deactive port id is invalid.(port_id = %d)', deactive_port)
        return
    end
    local channel_rx_state = port_info.RxEnable
    local channel_id = port_info.ChannelId
    local package_id = port_info.PackageId

    log:notice('ncsi: Deactive network controller port(%d)', deactive_port)
    if not channel_rx_state then
        ncsi_core.ncsi_disable_channel(package_id, channel_id, eth_name)
        ncsi_core.ncsi_enable_brdcast_filter(package_id, channel_id, eth_name)
        ncsi_core.ncsi_enable_multi_filter(package_id, channel_id, eth_name)
    end
    local retry_times = 0
    while retry_times < NCSI_DISABLED_CHANNELTX_MAX_RETRY do
        if ncsi_core.ncsi_disable_channelTX(package_id, channel_id, eth_name) then
            break
        end
        skynet.sleep(10) -- 100ms
        retry_times = retry_times + 1
        log:error("ncsi: Disable channel Tx failed, retry times = %s", retry_times)
    end
end

function ncsi_comm:init_ncsi_port(db, bus)
    local mac = db:select(db.NcsiNCInfo):first().Mac
    local active_port_id = db:select(db.NcsiNCInfo):first().ActivePort

    log:notice('ncsi: Init ncsi port.')
    ncsi_core.ncsi_common_init()
    ncsi_core.ncsi_eth_mac_init(mac)
    skynet.fork(function()
        local retry_times = 0
        while retry_times < NCSI_BASIC_INIT_MAX_RETRY do
            if self:ncsi_basic_init(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_active_nc_port(db, active_port_id)
        if self.check_ncsi_link_co == nil then
            self.check_ncsi_link_co = skynet.fork(function()
                self:check_ncsi_link_task(db, bus)
            end)
        end
    end)
end

function ncsi_comm:ncsi_change_active_port(db, active_state, deactive_port_id, active_port_id)
    log:notice(
        'ncsi:, active port changed, active_state=%s, old_active_port=%d, cur_active_port=%d.',
        active_state, deactive_port_id, active_port_id)
    ncsi_deactive_nc_port(db, deactive_port_id)
    if active_state then
        self:ncsi_active_nc_port(db, active_port_id)
    end
end

function ncsi_comm:ncsi_init(db, bus)
    local ncsi_status = db:select(db.NcsiNCInfo):first().Enable
    if not ncsi_status then
        log:notice('ncsi: Ncsi disabled')
        return
    end
    self:init_ncsi_port(db, bus)
end

function ncsi_comm:ncsi_sync_eth_mac(db, mac)
    log:notice('ncsi: Sync eth mac: %s', mac)
    ncsi_core.ncsi_eth_mac_init(mac)
    local active_port_id = db:select(db.NcsiNCInfo):first().ActivePort
    local eth_name = 'eth' .. db:select(db.NcsiNCInfo):first().EthId
    local channel_id =
        db:select(db.NcsiNCPortInfo):where({PortId = active_port_id}):first().ChannelId
    log:notice('ncsi: active_port_id=%s, eth_name=%s, channel_id=%s', active_port_id, eth_name,
        channel_id)
    ncsi_core.ncsi_paramter_init(channel_id, eth_name, NCSI_ENABLED)
end

function ncsi_comm:ncsi_set_vlan_info(db)
    local active_port_id = db:select(db.NcsiNCInfo):first().ActivePort
    self:ncsi_comm_set_nc_vlan(db, active_port_id)
    self:ncsi_comm_set_nc_multi_vlan(db, active_port_id)
end

function ncsi_comm:ncsi_update_multi_vlan(db, vlan_id)
    local active_port_id = db:select(db.NcsiNCInfo):first().ActivePort
    for _, id in ipairs(self.multi_vlan) do
        if vlan_id == id then
            return
        end
    end

    self.multi_vlan[#self.multi_vlan + 1] = vlan_id
    self:ncsi_comm_set_nc_multi_vlan(db, active_port_id)
end

function ncsi_comm:get_ncsi_channel_cnt()
    return ncsi_core.get_ncsi_channel_cnt()
end

return singleton(ncsi_comm)
