--
-- (C) 2013-24 - ntop.org
--
local dirs = ntop.getDirs()
package.path = dirs.installdir .. "/scripts/lua/modules/?.lua;" .. package.path
require("lua_utils")
local json = require("dkjson")
local discover = require "discover_utils"

local OFFLINE_LOCAL_HOSTS_KEY = "ntopng.hosts.offline.ifid_%s"
local inactive_hosts_utils = {}

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

-- Function used to check if the host has to be filtered out or not
-- return true in case the host is okey, false in case it has to be filtered out
local function check_filters(host_info, filters)
    local mac_manufacturer = ntop.getMacManufacturer(host_info.mac) or {}
    local filters_ok = true

    for filter, value in pairs(filters or {}) do
        if filter == "manufacturer" then
            if mac_manufacturer.extended ~= value then
                filters_ok = false
                goto skip
            end
        elseif tostring(host_info[filter]) ~= tostring(value) then
            filters_ok = false
            goto skip
        end
    end

    ::skip::
    return filters_ok
end

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

-- This function return a list of inactive hosts, with all the informations
function inactive_hosts_utils.getInactiveHosts(ifid, filters)
    local redis_hash = string.format(OFFLINE_LOCAL_HOSTS_KEY, ifid)
    local available_keys = ntop.getHashKeysCache(redis_hash) or {}
    local networks_stats = interface.getNetworksStats()
    local host_list = {}

    for redis_key, _ in pairs(available_keys) do
        local host_info_json = ntop.getHashCache(redis_hash, redis_key)
        local network_name = ""

        if not isEmptyString(host_info_json) then
            local host_info = json.decode(host_info_json)
            local mac_manufacturer = ntop.getMacManufacturer(host_info.mac) or {}
            local mac_manufacturer_label = ""

            if not check_filters(host_info, filters) then
                goto skip
            end

            for n, ns in pairs(networks_stats) do
                if ns.network_id == tonumber(host_info.network) then
                    network_name = getFullLocalNetworkName(ns.network_key)
                end
            end
            if mac_manufacturer and not isEmptyString(mac_manufacturer.extended) then
                mac_manufacturer_label = mac_manufacturer.extended
            end

            host_list[#host_list + 1] = {
                ip_address = host_info.ip,
                mac_address = host_info.mac,
                host = host_info.ip .. "@" .. host_info.vlan,
                vlan = getFullVlanName(host_info.vlan),
                vlan_id = host_info.vlan,
                name = host_info.name,
                last_seen = host_info.last_seen,
                first_seen = host_info.first_seen,
                epoch_end = host_info.last_seen,
                epoch_begin = host_info.first_seen,
                device_id = host_info.device_type,
                device_type = discover.devtype2string(host_info.device_type),
                network_id = host_info.network,
                network = network_name,
                serial_key = redis_key,
                manufacturer = mac_manufacturer_label
            }
        end

        ::skip::
    end

    return host_list
end

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

-- This function return the info of a specific inactive host, given the serialization (redis) key
function inactive_hosts_utils.getInactiveHostInfo(ifid, serial_key)
    local redis_hash = string.format(OFFLINE_LOCAL_HOSTS_KEY, ifid)
    local host_info_json = ntop.getHashCache(redis_hash, serial_key)

    if not isEmptyString(host_info_json) then
        local host_info = json.decode(host_info_json)

        return host_info
    end

    return nil
end

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

-- Return the inactive host number
function inactive_hosts_utils.getInactiveHostsNumber(ifid, filters)
    local redis_hash = string.format(OFFLINE_LOCAL_HOSTS_KEY, ifid)
    local available_keys = ntop.getHashKeysCache(redis_hash) or {}
    local count = 0

    if filters then
        local redis_hash = string.format(OFFLINE_LOCAL_HOSTS_KEY, ifid)
        local available_keys = ntop.getHashKeysCache(redis_hash) or {}

        for redis_key, _ in pairs(available_keys) do
            local host_info_json = ntop.getHashCache(redis_hash, redis_key)

            if not isEmptyString(host_info_json) then
                local host_info = json.decode(host_info_json)
                local last_seen = host_info.last_seen

                -- Exclude those that do not follow the filters
                if not check_filters(host_info, filters) then
                    goto skip
                end

                count = count + 1
                ::skip::
            end
        end
    else
        count = table.len(available_keys)
    end

    return count
end

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

-- This function return a list of available VLAN filters
function inactive_hosts_utils.getVLANFilters(ifid, filters)
    local redis_hash = string.format(OFFLINE_LOCAL_HOSTS_KEY, ifid)
    local available_keys = ntop.getHashKeysCache(redis_hash) or {}
    local vlan_list = {}
    local rsp = {}

    for redis_key, _ in pairs(available_keys) do
        local host_info_json = ntop.getHashCache(redis_hash, redis_key)

        if not isEmptyString(host_info_json) then
            local host_info = json.decode(host_info_json)

            -- Exclude those that do not follow the filters
            if not check_filters(host_info, filters) then
                goto skip
            end

            if not (vlan_list[host_info.vlan]) then
                vlan_list[host_info.vlan] = 1
            else
                vlan_list[host_info.vlan] = vlan_list[host_info.vlan] + 1
            end

            ::skip::
        end
    end

    -- Add the vlan names
    for vlan, count in pairsByKeys(vlan_list) do
        local vlan_name = ''
        if vlan == 0 then
            if table.len(vlan_list) == 1 then
                break
            end
            vlan_name = i18n('no_vlan')
        else
            vlan_name = getFullVlanName(vlan)
        end
        rsp[#rsp + 1] = {
            count = count,
            key = "vlan_id",
            value = vlan,
            label = tostring(vlan_name)
        }
    end

    -- Add the "all" entry
    table.insert(rsp, 1, {
        key = "vlan_id",
        value = "",
        label = i18n('all')
    })

    return rsp
end

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

-- This function return a list of available network filters
function inactive_hosts_utils.getNetworkFilters(ifid, filters)
    local redis_hash = string.format(OFFLINE_LOCAL_HOSTS_KEY, ifid)
    local available_keys = ntop.getHashKeysCache(redis_hash) or {}
    local networks_stats = interface.getNetworksStats()
    local network_list = {}
    local rsp = {}

    for redis_key, _ in pairs(available_keys) do
        local host_info_json = ntop.getHashCache(redis_hash, redis_key)
        local network_name = ""

        if not isEmptyString(host_info_json) then
            local host_info = json.decode(host_info_json)

            -- Exclude those that do not follow the filters
            if not check_filters(host_info, filters) then
                goto skip
            end

            if not (network_list[host_info.network]) then
                network_list[host_info.network] = 1
            else
                network_list[host_info.network] = network_list[host_info.network] + 1
            end

            ::skip::
        end
    end

    -- Format the networks name
    for network, count in pairsByKeys(network_list) do
        local network_name
        for n, ns in pairs(networks_stats) do
            if ns.network_id == tonumber(network) then
                network_name = getFullLocalNetworkName(ns.network_key)
            end
        end

        rsp[#rsp + 1] = {
            count = count,
            key = "network",
            value = network,
            label = tostring(network_name or network)
        }
    end
    -- Add the "all" entry
    table.insert(rsp, 1, {
        key = "network",
        value = "",
        label = i18n('all')
    })

    return rsp
end

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

-- This function return a list of available device filters
function inactive_hosts_utils.getDeviceFilters(ifid, filters)
    local discover_utils = require "discover_utils"
    local redis_hash = string.format(OFFLINE_LOCAL_HOSTS_KEY, ifid)
    local available_keys = ntop.getHashKeysCache(redis_hash) or {}
    local device_list = {}
    local rsp = {}

    for redis_key, _ in pairs(available_keys) do
        local host_info_json = ntop.getHashCache(redis_hash, redis_key)

        if not isEmptyString(host_info_json) then
            local host_info = json.decode(host_info_json)
            local dev_name = discover_utils.devtype2string(host_info.device_type)

            -- Exclude those that do not follow the filters
            if not check_filters(host_info, filters) then
                goto skip
            end

            if not (device_list[dev_name]) then
                device_list[dev_name] = {
                    count = 1,
                    type = host_info.device_type
                }
            else
                device_list[dev_name].count = device_list[dev_name].count + 1
            end
            
            ::skip::
        end
    end

    for device, info in pairsByKeys(device_list) do
        rsp[#rsp + 1] = {
            count = info.count,
            key = "device_type",
            value = info.type,
            label = device
        }
    end
    -- Add "all" entry
    table.insert(rsp, 1, {
        key = "device_type",
        value = "",
        label = i18n('all')
    })

    return rsp
end

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

-- This function return a list of available manufacturer filters
function inactive_hosts_utils.getManufacturerFilters(ifid, filters)
    local redis_hash = string.format(OFFLINE_LOCAL_HOSTS_KEY, ifid)
    local available_keys = ntop.getHashKeysCache(redis_hash) or {}
    local manufacturer_list = {}
    local rsp = {}

    for redis_key, _ in pairs(available_keys) do
        local host_info_json = ntop.getHashCache(redis_hash, redis_key)

        if not isEmptyString(host_info_json) then
            local host_info = json.decode(host_info_json)
            local mac_manufacturer = ntop.getMacManufacturer(host_info.mac) or {}
            local tmp = mac_manufacturer.extended

            if isEmptyString(tmp) then
                tmp = mac_manufacturer.short
            end

            -- Exclude those that do not follow the filters
            if not check_filters(host_info, filters) then
                goto skip
            end

            if tmp then
                if not (manufacturer_list[tmp]) then
                    manufacturer_list[tmp] = 1
                else
                    manufacturer_list[tmp] = manufacturer_list[tmp] + 1
                end
            end

            ::skip::
        end
    end

    for manufacturer, count in pairsByKeys(manufacturer_list) do
        rsp[#rsp + 1] = {
            count = count,
            key = "manufacturer",
            value = manufacturer,
            label = manufacturer
        }
    end

    table.insert(rsp, 1, {
        key = "manufacturer",
        value = "",
        label = i18n('all')
    })

    return rsp
end

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

function inactive_hosts_utils.deleteAllEntries(ifid)
    local redis_hash = string.format(OFFLINE_LOCAL_HOSTS_KEY, ifid)
    local available_keys = ntop.getHashKeysCache(redis_hash) or {}
    local num_hosts_deleted = table.len(available_keys)

    for redis_key, _ in pairs(available_keys) do
        ntop.delHashCache(redis_hash, redis_key)
    end

    return num_hosts_deleted
end

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

function inactive_hosts_utils.deleteAllEntriesSince(ifid, epoch)
    local redis_hash = string.format(OFFLINE_LOCAL_HOSTS_KEY, ifid)
    local available_keys = ntop.getHashKeysCache(redis_hash) or {}
    local num_hosts_deleted = 0

    for redis_key, _ in pairs(available_keys) do
        local host_info_json = ntop.getHashCache(redis_hash, redis_key)

        if isEmptyString(host_info_json) then
            ntop.delHashCache(redis_hash, redis_key)
            num_hosts_deleted = num_hosts_deleted + 1
        end

        local host_info = json.decode(host_info_json)
        if host_info.last_seen < epoch then
            num_hosts_deleted = num_hosts_deleted + 1
            ntop.delHashCache(redis_hash, redis_key)
        end
    end
    
    return num_hosts_deleted
end

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

function inactive_hosts_utils.deleteSingleEntry(ifid, redis_key)
    local redis_hash = string.format(OFFLINE_LOCAL_HOSTS_KEY, ifid)
    ntop.delHashCache(redis_hash, redis_key)
    return 1 -- Number of hosts deleted
end

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

function inactive_hosts_utils.formatInactiveHosts(hosts, no_html)
    local format_utils = require("format_utils")
    local discover_utils = require "discover_utils"

    -- Format the values to be used by the front end application
    for key, value in pairs(hosts) do
        local url = nil
        hosts[key]["last_seen"] = format_utils.formatPastEpochShort(value["last_seen"])
        hosts[key]["first_seen"] = format_utils.formatPastEpochShort(value["first_seen"])
        hosts[key]["manufacturer"] = value["manufacturer"]

        -- If available, add url and extra info
        local mac_info = interface.getMacInfo(value["mac_address"])
        if mac_info then
            url = mac2url(value["mac_address"])

            if no_html then
                url = nil
            end

            hosts[key]["mac_address"] = {
                name = mac2label(value["mac_address"]),
                value = value["mac_address"],
                url = url
            }
        end

        if interface.getNetworkStats(hosts[key]["network_id"]) then
            url = '/lua/hosts_stats.lua?network=' .. hosts[key]["network_id"]

            if no_html then
                url = nil
            end
            
            hosts[key]["network"] = {
                name = hosts[key]["network"],
                value = hosts[key]["network_id"],
                url = url
            }
        end

        url = '/lua/inactive_host_details.lua?serial_key=' .. hosts[key]["serial_key"]
        local device_type = discover_utils.devtype2icon(value["device_id"])

        if no_html then
            url = nil
            device_type = nil
        end

        hosts[key]["host"] = {
            ip_address = {
                name = hosts[key]["ip_address"],
                value = hosts[key]["ip_address"],
                url = url
            },
            device_type = device_type,
            device_name = discover_utils.devtype2string(value["device_id"])
        }

        if hosts[key]["vlan_id"] then
            hosts[key]["host"]["vlan"] = {
                name = hosts[key]["vlan"],
                value = hosts[key]["vlan_id"]
            }
            if interface.getVLANInfo(hosts[key]["vlan_id"]) and not no_html then
                hosts[key]["host"]["vlan"]["url"] = '/lua/hosts_stats.lua?vlan=' .. hosts[key]["vlan_id"]
            end
        end

        hosts[key]["device_id"] = nil
        hosts[key]["network_id"] = nil
        hosts[key]["vlan_id"] = nil

        if no_html then
            hosts[key].epoch_end = nil
            hosts[key].epoch_begin = nil
            hosts[key].serial_key = nil
            hosts[key].vlan = nil
            hosts[key].name = nil
        end
    end

    return hosts
end

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

function inactive_hosts_utils.formatInactiveHostsCSV(hosts)
    local formatted_hosts = inactive_hosts_utils.formatInactiveHosts(hosts, true --[[ No HTML ]])
    local column_names = "|"
    local csv_formatted = ""

    for key, value in pairs(formatted_hosts) do
        local tmp_value

        if value.mac_address then
            tmp_value = value.mac_address.value
            formatted_hosts[key].mac_address = tmp_value
        end

        tmp_value = value.network.name
        formatted_hosts[key].network = tmp_value
        
        tmp_value = value.host.ip_address.value
        formatted_hosts[key].ip_address = tmp_value
        
        tmp_value = value.host.ip_address.name
        formatted_hosts[key].hostname = tmp_value
        
        tmp_value = value.host.device_name
        formatted_hosts[key].device_type = tmp_value
        
        tmp_value = ""
        if value.host.vlan then
            tmp_value = value.host.vlan.name or value.host.vlan.value
        end
        formatted_hosts[key].vlan = tmp_value

        value.host = nil
        column_names = "|"
        local concat = "|"
        for column, data in pairsByKeys(value) do
            column_names = column_names .. string.upper(column) .. "|"
            concat = concat .. data .. "|"
        end 
        csv_formatted = csv_formatted .. concat .. "\n"
    end
    
    return column_names .. "\n" .. csv_formatted
end

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

function inactive_hosts_utils.formatInactiveHostsJSON(hosts)
    local json = require("dkjson")
    local formatted_hosts = inactive_hosts_utils.formatInactiveHosts(hosts, true --[[ No HTML ]])

    return json.encode(formatted_hosts or {})
end

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

-- Return the distribution of inactive hosts on epoch basis:
--  - hour
--  - day
--  - week
--  - older
function inactive_hosts_utils.getInactiveHostsEpochDistribution(ifid, filters)
    local redis_hash = string.format(OFFLINE_LOCAL_HOSTS_KEY, ifid)
    local available_keys = ntop.getHashKeysCache(redis_hash) or {}
    local epoch_list = {
        last_hour = 0,
        last_day = 0,
        last_week = 0,
        older = 0,
    }
    local now = os.time() -- current epoch
    local one_hour_epoch = now - 3600
    local one_day_epoch = now - 86400
    local one_week_epoch = now - 604800

    for redis_key, _ in pairs(available_keys) do
        local host_info_json = ntop.getHashCache(redis_hash, redis_key)
        local network_name = ""

        if not isEmptyString(host_info_json) then
            local host_info = json.decode(host_info_json)
            local last_seen = host_info.last_seen

            -- Exclude those that do not follow the filters
            if not check_filters(host_info, filters) then
                goto skip
            end

            if last_seen >= one_hour_epoch then
                -- newer then one hour
                epoch_list.last_hour = epoch_list.last_hour + 1 
            elseif last_seen >= one_day_epoch then
                -- newer then one day but older then one hour
                epoch_list.last_day = epoch_list.last_day + 1
            elseif last_seen >= one_week_epoch then
                -- newer then one week but older then one day
                epoch_list.last_week = epoch_list.last_week + 1
            else
                -- Older then one week
                epoch_list.older = epoch_list.older + 1
            end

            ::skip::
        end
    end

    return epoch_list
end

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

function inactive_hosts_utils.getFilters()
    local filters = {
        vlan = _GET["vlan_id"],
        network = _GET["network"],
        device_type = _GET["device_type"],
        manufacturer = _GET["manufacturer"],
    }

    -- Return the data
    for filter, value in pairs(filters) do
        if isEmptyString(value) then
            filters[filter] = nil
        end
    end

    return filters
end

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

return inactive_hosts_utils
