--
-- (C) 2014-24 - ntop.org
--

local top_talkers_utils = {}
top_talkers_utils.MAX_NUM_ENTRIES = 10
top_talkers_utils.THRESHOLD_LOW = .05
top_talkers_utils.TOP_ENABLED_KEY = "ntopng.prefs.ifid_%i.interface_top_talkers_creation"

local vlan_totals = {}
local asname_cache = {}
local hostname_cache = {}
local localhost_cache = {}
local ipaddr_cache = {}

-- ########################################################

local function getTopEnabledKey(ifid)
    return string.format(top_talkers_utils.TOP_ENABLED_KEY, ifid)
end

-- ########################################################

local function updateCache(cache, key, val)
    if ((key ~= nil) and (cache[key] == nil)) then
        cache[key] = val
    end
end

-- ########################################################

local function getCache(cache, key)
    return cache[key]
end

-- ########################################################

local function updateRes(res, vlan, what_key, what_value, direction, delta)
    if res == nil then
        res = {}
    end
    if res[vlan] == nil then
        res[vlan] = {}
    end
    if res[vlan][what_key] == nil then
        res[vlan][what_key] = {}
    end
    if res[vlan][what_key][direction] == nil then
        res[vlan][what_key][direction] = {}
    end
    if res[vlan][what_key][direction][what_value] == nil then
        res[vlan][what_key][direction][what_value] = 0
    end

    res[vlan][what_key][direction][what_value] = res[vlan][what_key][direction][what_value] + delta
end

-- ########################################################

local function sortRes(res)
    for vlan_k, vlan_val in pairs(res) do
        for what_key_k, what_key_val in pairs(vlan_val) do
            for direction_key, direction_val in pairs(what_key_val) do
                local total, count, other = 0, 0, 0

                for what_val_k, delta in pairsByValues(direction_val, rev) do
                    count = count + 1
                    total = total + delta

                    if delta <= 0 or count > top_talkers_utils.MAX_NUM_ENTRIES then
                        if delta > 0 then
                            other = other + delta
                        end
                        direction_val[what_val_k] = nil
                    end
                end

                count = 0
                for what_val_k, delta in pairs(direction_val) do
                    count = count + 1
                    -- at least 5
                    if (count > top_talkers_utils.MAX_NUM_ENTRIES / 2 and delta / total <
                        top_talkers_utils.THRESHOLD_LOW) then
                        if delta > 0 then
                            other = other + delta
                        end
                        direction_val[what_val_k] = nil
                    end
                end

                if other > 0 then
                    direction_val["Other"] = other
                end
            end
        end
    end
end

-- ########################################################

local function finalizeRes(res)
    for vlan_k, vlan_val in pairs(res) do
        for what_key_k, what_key_val in pairs(vlan_val) do
            for direction_key, direction_val in pairs(what_key_val) do
                for what_val_k, delta in pairs(direction_val) do
                    local label = what_val_k

                    if (what_val_k ~= "Other") and (what_val_k ~= "Hidden Hosts") then
                        if what_key_k == "hosts" then
                            label = getCache(hostname_cache, what_val_k)
                        elseif what_key_k == "asn" then
                            label = getCache(asname_cache, what_val_k)
                        end
                    end

                    if label == what_val_k then
                        -- Skip this label, as it is the same as the address
                        label = nil
                    end

                    direction_val[what_val_k] = {
                        address = what_val_k .. '',
                        value = delta,
                        label = label
                    }
                    if what_key_k == "hosts" then
                        local ip_addr = getCache(ipaddr_cache, what_val_k)
                        direction_val[what_val_k]["local"] = tostring(getCache(localhost_cache, what_val_k) or "false")

                        if ip_addr ~= what_val_k then
                            -- storing the IP address is necessary for MAC based keys to create the
                            -- correct URL
                            direction_val[what_val_k]["ipaddr"] = ip_addr
                        end
                    end
                end
            end
        end
    end

    -- Convert to the previous format
    local p = {}
    for vlan_k, vlan_val in pairs(res) do
        p[#p + 1] = vlan_val

        for what_key_k, what_key_val in pairs(vlan_val) do
            local s = {}
            for _, sender in pairs(what_key_val["sent"]) do
                s[#s + 1] = sender
            end

            local r = {}
            for _, receiver in pairs(what_key_val["rcvd"]) do
                r[#r + 1] = receiver
            end

            vlan_val[what_key_k] = {{
                senders = s,
                receivers = r
            }}
        end

        vlan_val["value"] = vlan_totals[vlan_k]
        vlan_val["address"] = vlan_k .. ""
    end

    return {
        vlan = p
    }
end

-- ########################################################

function top_talkers_utils.areTopEnabled(ifid)
    local k = getTopEnabledKey(ifid)

    return not (ntop.getPref(k) == "false")
end

-- ########################################################

function top_talkers_utils.disableTop(ifid)
    local k = getTopEnabledKey(ifid)
    ntop.setPref(k, "false")
end

-- ########################################################

function top_talkers_utils.enableTop(ifid)
    local k = getTopEnabledKey(ifid)
    ntop.delCache(k)
end

-- ########################################################

function top_talkers_utils.makeTopJson(_ifname)
    require "ntop_utils"
    -- Include here otherwise it's a useless waste of memory
    local json = require "dkjson"
    local callback_utils = require "callback_utils"

    local ifid = getInterfaceId(_ifname)
    local res = {}

    if not top_talkers_utils.areTopEnabled(ifid) then
        return nil
    end

    local in_time = callback_utils.foreachHost(_ifname, function(hostname, hoststats)
        local checkpoint = interface.checkpointHostTalker(ifid, hostname)
        local tskey = hoststats["tskey"]
        local vlan = hoststats["vlan"]

        updateCache(hostname_cache, tskey, hoststats["name"])
        updateCache(localhost_cache, tskey, hoststats["localhost"])
        updateCache(ipaddr_cache, tskey, hostname)
        updateCache(asname_cache, hoststats["asn"], hoststats["asname"])

        if ((checkpoint == nil) or (checkpoint["delta"] == nil)) then
            goto continue
        end

        for _, direction in pairs({"sent", "rcvd"}) do
            local delta = checkpoint["delta"][direction]

            vlan_totals[vlan] = (vlan_totals[vlan] or 0) + delta

            local os_key = "non-local os"
            if hoststats["localhost"] then
                os_key = "local os"
            end

            local country = interface.getHostCountry(hostname)

            for what_key, what_value in pairs({
                ["hosts"] = tskey,
                ["asn"] = hoststats["asn"],
                [os_key] = hoststats["os"],
                ["countries"] = ternary(not isEmptyString(country), country, nil),
                ["networks"] = hoststats["local_network_id"]
            }) do
                if hoststats.hiddenFromTop then
                    what_value = "Hidden Hosts"
                end

                updateRes(res, vlan, what_key, what_value, direction, delta)
            end
        end

        ::continue::
    end)

    if not in_time then
        print("[" .. _ifname ..
                  "] ERROR: Cannot complete top talkers generation in 1 minute. Is there a huge number of hosts in the system?")
    end

    sortRes(res)
    return json.encode(finalizeRes(res))
end

-- ########################################################

-- Computes label and url during visualization
function top_talkers_utils.enrichRecordInformation(class_key, rec, show_vlan)
    require "ntop_utils"
    require "label_utils"

    local url = ""
    local label = rec.label or rec.address
    local url_target = rec.address

    if (rec.address ~= "Other") and (rec.address ~= "Hidden Hosts") then
        if class_key == "hosts" then
            local address = rec.address

            if ends(address, "_v4") or ends(address, "_v6") then
                rec.visual_addr = visualTsKey(address)
                address = string.sub(address, 1, string.len(address) - 3)

                if (rec.ipaddr ~= nil) then
                    url_target = rec.ipaddr
                end
            end

            url_target = url_target .. "&tskey=" .. rec.address

            url = ntop.getHttpPrefix() .. '/lua/host_details.lua?always_show_hist=true&host='

            -- Use the host alias as label, if set
            local alt_name = ip2label(address, 0, 100)

            if not isEmptyString(alt_name) and (alt_name ~= address) then
                label = alt_name
            else
                local hinfo = hostkey2hostinfo(address)
                if not show_vlan then
                    hinfo.vlan = 0
                end
                alt_name = hostinfo2label(hinfo)

                if not isEmptyString(alt_name) and (alt_name ~= address) then
                    label = alt_name
                else
                    label = address
                end
            end
        elseif class_key == "asn" then
            url = ntop.getHttpPrefix() .. '/lua/hosts_stats.lua?asn='
        elseif class_key == "networks" then
            url = ntop.getHttpPrefix() .. '/lua/hosts_stats.lua?network='

            local network_name = nil
            if rec.address == "-1" then
                network_name = i18n("remote_networks")
            else
                network_name = getFullLocalNetworkName(ntop.getNetworkNameById(tonumber(rec.address)))
            end
            if not isEmptyString(network_name) then
                label = network_name
            end
        elseif class_key:contains("os") then
            url = ntop.getHttpPrefix() .. '/lua/hosts_stats.lua?os='
        end

        if not isEmptyString(url) then
            url = url .. url_target
        end
    end

    -- Update record information
    rec.url = url
    rec.label = label
end

-- ########################################################

function top_talkers_utils.enrichVlanInformation(vlan_tbl)
    local vlan_id = ternary(vlan_tbl.address, tostring(vlan_tbl.address), "0")
    vlan_tbl["label"] = vlan_id
    vlan_tbl["name"] = vlan_id
    vlan_tbl["url"] = ntop.getHttpPrefix() .. '/lua/hosts_stats.lua?vlan=' .. vlan_id
end

-- ########################################################

return top_talkers_utils
