--
-- (C) 2021-25 - ntop.org
--
-- This file contains a small set of utility functions

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

if (pragma_once_ntop_utils == true) then
   -- io.write(debug.traceback().."\n")
   -- avoid multiple inclusions
   return
end

pragma_once_ntop_utils = true

-- This is required because it simply adds the tracing functions, like traceError
require "lua_trace"

local clock_start = os.clock()

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

-- split
function split(s, delimiter)
   result = {};
   if (s ~= nil) then
      if delimiter == nil then
	 -- No delimiter, split all characters
	 for match in s:gmatch "." do table.insert(result, match); end
      else
	 -- Split by delimiter
	 for match in (s .. delimiter):gmatch("(.-)" .. delimiter) do
	    table.insert(result, match);
	 end
      end
   end
   return result;
end

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

function ends(String, End)
   return End == '' or string.sub(String, -string.len(End)) == End
end

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

function string.starts(String, Start)
   if type(String) ~= 'string' or type(Start) ~= 'string' then return false end
   return string.sub(String, 1, string.len(Start)) == Start
end

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

function string.ends(String, End)
   if type(String) ~= 'string' or type(End) ~= 'string' then return false end
   return End == '' or string.sub(String, -string.len(End)) == End
end

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

-- @Brief Checks if a dotted-decimal SNMP OID starts with another dotted-decimal SNMP OID
-- @param oid_string The string-encoded dotted-decimal SNMP OID to check
-- @param oid_string_start A string-encoded dotted-decimal SNMP OID prefix
-- @return True if `oid_string` starts with `oid_string_start` or false otherwise
function string.oid_starts(oid_string, oid_string_start)
   if type(oid_string) ~= 'string' or type(oid_string_start) ~= 'string' then
      return false
   end

   -- Make sure both OIDs end with a dot, to avoid
   -- considering 1.3.6.1.4.1.99 starting with 1.3.6.1.4.1.9
   if not string.ends(oid_string, ".") then oid_string = oid_string .. "." end

   if not string.ends(oid_string_start, ".") then
      oid_string_start = oid_string_start .. "."
   end

   return string.sub(oid_string, 1, string.len(oid_string_start)) ==
      oid_string_start
end

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

-- Print contents of `tbl`, with indentation.
-- You can call it as tprint(mytable)
-- The other two parameters should not be set
function tprint(s, l, i)
   -- io.write(debug.traceback().."\n")

   l = (l) or 1000;
   i = i or ""; -- default item limit, indent string
   if (l < 1) then
      io.write("ERROR: Item limit reached.\n");
      return l - 1
   end
   local ts = type(s);
   if (ts ~= "table") then
      io.write(i .. ' ' .. ts .. ' ' .. tostring(s) .. '\n');
      return l - 1
   end
   io.write(i .. ' ' .. ts .. '\n');
   for k, v in pairs(s) do
      local indent = ""

      if (i ~= "") then indent = i .. "." end
      indent = indent .. tostring(k)

      l = tprint(v, l, indent);
      if (l < 0) then break end
   end

   return l
end

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

function splitNetworkPrefix(net)
   if not net then tprint(debug.traceback()) end
   local prefix = tonumber(net:match("/(.+)"))
   local address = net:gsub("/.+", "")
   return address, prefix
end

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

--
-- Concatenates table keys to values with separators
--
-- Parameters
--    keys_values: the table which contains the items
--    kv_sep: a string to be put between a key and a value
--    group_sep: a string to be put between key-value groups
--    last_sep: a string to be put after last value, if table is not empty
--    value_quote: a string to be used to quote values
--
function table.tconcat(keys_values, kv_sep, group_sep, last_sep, value_quote)
   local groups = {}
   kv_sep = kv_sep or ""
   group_sep = group_sep or ""
   last_sep = last_sep or ""
   value_quote = value_quote or ""

   for k, v in pairs(keys_values) do
      local parts = {k, kv_sep, value_quote, v, value_quote}
      groups[#groups + 1] = table.concat(parts, "")
   end

   if #groups > 0 then
      return table.concat(groups, group_sep) .. last_sep
   else
      return ""
   end
end

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

-- NOTE: on index based tables using #table is much more performant
function table.len(tbl)
   local count = 0

   if tbl == nil then
      -- io.write("ERROR: table expected, got nil\n")
      -- io.write(debug.traceback().."\n")
      return 0
   end

   if type(tbl) ~= "table" then
      io.write("ERROR: table expected, got " .. type(tbl) .. "\n")
      io.write(debug.traceback() .. "\n")
      return 0
   end

   for k, v in pairs(tbl) do count = count + 1 end

   return count
end

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

function table.slice(tbl, first, last, step)
   local sliced = {}

   for i = first or 1, last or #tbl, step or 1 do
      sliced[#sliced + 1] = tbl[i]
   end

   return sliced
end

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

function table.empty(tbl)
   if (tbl == nil) then return true end

   if next(tbl) == nil then return true end

   return false
end

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

function isIPv4(address)
   if (address == nil) then return false end

   -- Reuse the for loop to check the address validity
   local checkAddress = (function(chunks)
	 for _, v in pairs(chunks) do
            if (tonumber(v) < 0) or (tonumber(v) > 255) then
	       return false
            end
	 end
	 return true
   end)

   local chunks = {address:match("^(%d+)%.(%d+)%.(%d+)%.(%d+)$")}
   local chunksWithPort = {
      address:match("^(%d+)%.(%d+)%.(%d+)%.(%d+)%:(%d+)$")
   }

   if #chunks == 4 then
      return checkAddress(chunks)
   elseif #chunksWithPort == 5 then
      table.remove(chunksWithPort, 5)
      return checkAddress(chunksWithPort)
   end

   return false
end

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

function isIPv6(ip) return ((not isEmptyString(ip)) and ntop.isIPv6(ip)) end

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

-- Check if address is a CIDR
-- strict (optional) do not accept subnets without the '/<mask>'
function isIPv4Network(address, strict)
   -- Check for @ VLAN
   local parts = split(address, "@")
   if #parts == 2 then address = parts[1] end

   -- Parse CIDR
   parts = split(address, "/")
   if #parts == 2 then
      local prefix = tonumber(parts[2])

      if (prefix == nil) or (math.floor(prefix) ~= prefix) or (prefix < 0) or
	 (prefix > 32) then return false end

   elseif #parts == 1 and strict then
      return false

      -- Check empty
   elseif #parts ~= 1 then
      return false
   end

   -- Check IP
   return isIPv4(parts[1])
end

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

-- Check if address is a CIDR
-- strict (optional) do not accept subnets without the '/<mask>'
function isIPv6Network(address, strict)
   -- Check for @ VLAN
   local parts = split(address, "@")
   if #parts == 2 then address = parts[1] end

   -- Parse CIDR
   parts = split(address, "/")
   if #parts == 2 then
      local prefix = tonumber(parts[2])

      if (prefix == nil) or (math.floor(prefix) ~= prefix) or (prefix < 0) or
	 (prefix > 128) then return false end

   elseif #parts == 1 and strict then
      return false

      -- Check empty
   elseif #parts ~= 1 then
      return false
   end

   -- Check IPv6
   return isIPv6(parts[1])
end

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

function firstToUpper(str)
   str = tostring(str)
   return (str:gsub("^%l", string.upper))
end

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

function pairsByKeys(t, f)
   local a = {}
   if t == nil then io.write(debug.traceback() .. "\n") end
   for n in pairs(t) do table.insert(a, n) end
   table.sort(a, f)
   local i = 0 -- iterator variable
   local iter = function() -- iterator function
      i = i + 1
      if a[i] == nil then
	 return nil
      else
	 return a[i], t[a[i]]
      end
   end
   return iter
end

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

function pairsByValues(t, f)
   local a = {}
   if t == nil then io.write(debug.traceback() .. "\n") end
   for n in pairs(t) do table.insert(a, n) end
   table.sort(a, function(x, y) return f(t[x], t[y]) end)
   local i = 0 -- iterator variable
   local iter = function() -- iterator function
      i = i + 1
      if a[i] == nil then
	 return nil
      else
	 return a[i], t[a[i]]
      end
   end
   return iter
end

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

-- @brief Sorted iteration of a table whose keys are strings in dotted decimal format
--        Can be used to sort dotted-decimal IPs, SNMP oids, etc.
-- @param t The table to be iterated
-- @param f The sort function, either `asc` or `rev`
-- @return An iterator
function pairsByDottedDecimalKeys(t, f)
   local sorter = {}

   -- Build a support array for the actual sorting
   for key, value in pairs(t) do
      local key_sorter = key:split("%.") or {key} -- An array that will be used to sort
      local splitted = key_sorter[#key_sorter]:split("@") or {}
      -- This example handles the VLAN, if no VLAN is present, add 0, in case
      -- a comparison between an host with VLAN and one without is performed
      key_sorter[#key_sorter] = splitted[1]
      key_sorter[#key_sorter + 1] = splitted[2] or 0

      sorter[#sorter + 1] = {
	 sorter = key_sorter,
	 key = key, -- Original key
	 value = value -- Original value
      }
   end

   table.sort(sorter, function(left, right)
		 -- The minimum of the two lengths, used to to the comparisons
		 local len = math.min(#left.sorter, #right.sorter)

		 for i = 1, len do
		    -- Convert elements to numbers
		    local left_number, right_number = tonumber(left.sorter[i]),
		       tonumber(right.sorter[i])

		    if left_number ~= right_number then
		       -- If numbers are different, compare them using the sort function
		       return f(left_number, right_number)
		    elseif i == len then
		       -- This is the lat time we do the comparison:
		       -- When lengths are not equal, legths are used at tie breaker
		       return f(#left.sorter, #right.sorter)
		    end
		 end
   end)

   local i = 0
   local iter = function()
      i = i + 1

      if sorter[i] == nil then return end

      return sorter[i].key, sorter[i].value
   end

   return iter
end

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

function pairsByField(t, field, f)
   local a = {}
   for n in pairs(t) do table.insert(a, n) end

   table.sort(a, function(x, y) return f(t[x][field], t[y][field]) end)
   local i = 0 -- iterator variable
   local iter = function() -- iterator function
      i = i + 1
      if a[i] == nil then
	 return nil
      else
	 return a[i], t[a[i]]
      end
   end
   return iter
end

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

function asc(a, b)
   if a == nil or b == nil then
      return false
   elseif type(a) ~= type(b) then
      traceError(TRACE_WARNING, TRACE_CONSOLE,
		 "Bad types in asc(): " .. a .. " (" .. type(a) .. ") vs " ..
		 b .. " (" .. type(b) .. ")")
      return false
   end

   return (a < b)
end

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

function rev(a, b)
   if a == nil or b == nil then
      return false
   elseif type(a) ~= type(b) then
      traceError(TRACE_WARNING, TRACE_CONSOLE,
		 "Bad types in rev(): " .. a .. " (" .. type(a) .. ") vs " ..
		 b .. " (" .. type(b) .. ")")
      tprint(debug.traceback())
      return false
   end

   return (a > b)
end

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

function asc_insensitive(a, b)
   if type(a) ~= "string" then return asc(a, b) end
   return (string.lower(a) < string.lower(b))
end

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

function rev_insensitive(a, b)
   if type(a) ~= "string" then return rev(a, b) end
   return (string.lower(a) > string.lower(b))
end

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

function string.split(s, p)
   local temp = {}
   local index = 0

   if s == nil then io.write(debug.traceback() .. "\n") end

   local last_index = string.len(s)

   while true do
      local i, e = string.find(s, p, index)

      if i and e then
	 local next_index = e + 1
	 local word_bound = i - 1
	 table.insert(temp, string.sub(s, index, word_bound))
	 index = next_index
      else
	 if index > 0 and index <= last_index then
	    table.insert(temp, string.sub(s, index, last_index))
	 elseif index == 0 then
	    temp = nil
	 end
	 break
      end
   end

   return temp
end

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

function isMacAddress(address)
   local v
   local addr

   if (address == nil) then return false end

   v = string.split(address, "@")

   if (v ~= nil) then
      addr = v[1]
   else
      addr = address
   end

   if (string.ends(addr, "_v4") or string.ends(addr, "_v6") or
       (string.match(addr, "^%x%x:%x%x:%x%x:%x%x:%x%x:%x%x$") ~= nil) or
       (string.match(addr, "^%x%x:%x%x:%x%x:%x%x:%x%x:%x%x%@%d+$") ~= nil)) then
      return true
   end
   return false
end

function isCommunityId(address)
   local c
   if (address == nil) then return false end

   c = string.split(address, ":")
   if (c ~= nil and #c == 2) then return true end

   return false
end

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

function isEmptyString(str)
   if ((str == nil) or (str == "") or (str == " ")) then
      return true
   else
      return false
   end
end

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

function ternary(cond, T, F)
   if cond then
      return T
   else
      return F
   end
end

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

function isAdministrator() return ntop.isAdministrator() end

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

function isNoLoginUser() return _SESSION["user"] == ntop.getNologinUser() end

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

function getSystemInterfaceId()
   -- NOTE: keep in sync with SYSTEM_INTERFACE_ID in ntop_defines.h
   -- This must be a string as it is passed in interface.select
   return "-1"
end

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

function getSystemInterfaceName()
   -- NOTE: keep in sync with SYSTEM_INTERFACE_NAME in ntop_defines.h
   return "__system__"
end

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

function hasHighResolutionTs()
   local active_driver = ntop.getPref("ntopng.prefs.timeseries_driver")

   -- High resolution timeseries means dumping the host timeseries
   -- every 60 seconds instead of 300 seconds.
   return ((active_driver == "influxdb") and
      (ntop.getPref("ntopng.prefs.ts_resolution") ~= "300"))
end

if (trace_script_duration ~= nil) then
   io.write(debug.getinfo(1, 'S').source .. " executed in " ..
	    (os.clock() - clock_start) * 1000 .. " ms\n")
end

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

function getInterfaceId(interface_name)
   if (interface_name == getSystemInterfaceName()) then
      return (getSystemInterfaceId())
   end

   local ifnames = interface.getIfNames()

   for if_id, if_name in pairs(ifnames) do
      if if_name == interface_name then return tonumber(if_id) end
   end

   return (-1)
end

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

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

      if ver == "v4" then
	 visual_addr = address
      else
	 visual_addr = address .. " (" .. ver .. ")"
      end

      return visual_addr
   end

   return tskey
end

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

-- @brief Performs a difference between the current metric value and
-- the previous saved value and saves the current value for next call.
-- @param reg lua C context pointer to the alertable entity storage
-- @param metric_name name of the metric to retrieve
-- @param granularity the granularity number
-- @param curr_val the current metric value
-- @param skip_first if true, 0 will be returned when no cached value is present
-- @return the difference between current and previous value
function delta_val(reg, metric_name, granularity, curr_val, skip_first)
   -- This require is okay, alert_granularities is just a struct
   local alert_granularities = require "alert_granularities"

   local granularity_num = alert_granularities[granularity] or 0
   if granularity_num ~= 0 and not isEmptyString(granularity_num) then
      granularity_num = granularity_num.granularity_id
   end

   local key = string.format("%s:%d", metric_name, granularity_num)

   -- Read cached value and purify it
   local prev_val
   local prev = reg.getCachedAlertValue(key, granularity_num)
   if prev == nil or type(prev) == "table" then -- Safety check and debug
      -- traceError(TRACE_ERROR, TRACE_CONSOLE, "Bad prev val")
      -- tprint(prev)
      -- tprint(debug.traceback())
   else
      prev_val = tonumber(prev)
   end

   -- Save the value for the next round
   reg.setCachedAlertValue(key, tostring(curr_val), granularity_num)

   if ((skip_first == true) and (prev_val == nil)) then
      return (0)
   else
      if (prev_val == nil) then prev_val = 0 end

      if (curr_val < prev_val) then
	 return (0) -- counter wrap check
      else
	 return (curr_val - (prev_val or 0))
      end
   end
end

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

--
-- Members supported format
-- 192.168.1.10/32@10
-- 00:11:22:33:44:55
--

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

-- Check if this is a valid pool member (MAC or CIDR@VLAN)
-- @param fix_relaxed Fix relaxed notation (IP without net mask or vlan)
function checkPoolMember(member, fix_relaxed)
   if isEmptyString(member) then return nil end

   if isMacAddress(member) then return member end

   -- VLAN
   local vlan_id
   local vlan_idx = string.find(member, "@")
   if vlan_idx == nil then
      if fix_relaxed then
	 vlan_id = 0 -- default vlan: 0
      else
	 return nil -- no vlan
      end
   elseif vlan_idx == 1 then
      return nil -- bad format
   else
      local other = string.sub(member, 1, vlan_idx - 1)
      vlan_id = tonumber(string.sub(member, vlan_idx + 1))
      if vlan_id == nil or vlan_id < 0 then return nil end
      member = other
   end

   -- prefix is mandatory here
   local address, prefix = splitNetworkPrefix(member)
   if address == nil then
      return nil -- bad format
   elseif prefix == nil then
      if fix_relaxed then
	 if isIPv4(address) then
	    prefix = '32' -- default mask: 32
	 elseif isIPv6(address) then
	    prefix = '128' -- default mask: 128
	 else
	    return nil -- bad format
	 end
      else
	 return nil -- no mask
      end
   end

   if isIPv4(address) and (tonumber(prefix) >= 0) and (tonumber(prefix) <= 32) then
      -- ok
   elseif isIPv6(address) and (tonumber(prefix) >= 0) and
      (tonumber(prefix) <= 128) then
      -- ok
   else
      return nil -- bad format
   end

   return address .. '/' .. prefix .. '@' .. vlan_id
end

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

-- Check if this is a valid pool member (MAC or CIDR@VLAN)
-- @param relaxed Allow relaxed notation (IP without net mask or vlan)
function isValidPoolMember(member, relaxed)
   if checkPoolMember(member, relaxed) then
      return true
   else
      return false
   end
end

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

function fixPoolMemberFormat(member) return checkPoolMember(member, true) end

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

function bit(p)
   return 2 ^ (p - 1) -- 1-based indexing
end

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

-- Typical call:  if hasbit(x, bit(3)) then ...
function hasbit(x, p) return x % (p + p) >= p end

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

function setbit(x, p) return hasbit(x, p) and x or x + p end

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

function clearbit(x, p) return hasbit(x, p) and x - p or x end

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

function getHttpHost()
   local ntopng_info = ntop.getInfo()
   local ntopng_host_info = ntop.getHostInformation() or {}

   -- Read configured ntopng host name or IP
   local ntopng_host_ip = ntop.getPref("ntopng.prefs.ntopng_host_address")
   if isEmptyString(ntopng_host_ip) then
      -- fallback: managegemt IP
      ntopng_host_ip = ntopng_host_info.ip
   end
   if isEmptyString(ntopng_host_ip) then
      -- last resort: dummy IP
      ntopng_host_ip = '127.0.0.1'
   end

   local http_host
   if starts(ntopng_host_ip, 'http') then
      http_host = ntopng_host_ip
   else
      -- Computing URL adding protocol and port
      local ntopng_protocol = "http://"
      local ntopng_port = ntopng_info.http_port

      if not ntop.isnEdge() and ntopng_info.https_port and
	 tonumber(ntopng_info.https_port) ~= 0 then
	 ntopng_protocol = "https://"
	 ntopng_port = ntopng_info.https_port
      end

      http_host = ntopng_protocol .. ntopng_host_ip .. ":" .. ntopng_port
   end

   return http_host
end

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

function starts(String, Start)
   if ((String == nil) or (Start == nil)) then return (false) end

   return string.sub(String, 1, string.len(Start)) == Start
end

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

function getExportersUnifiedStats()
   local flowdevs = interface.getFlowDevices()
   local unified_exporters = {}
   for interface_id, device_list in pairs(flowdevs or {}) do
      for device_id, exporter_info in pairs(device_list, asc) do
	 if type(exporter_info) == "number" then
	    tprint(flowdevs)
	    tprint(debug.traceback())
	    goto continue
	 end
	 local exporter_ip = exporter_info.exporter_ip
	 if not unified_exporters[exporter_ip] then
	    unified_exporters[exporter_ip] = {}
	 end
	 local ports_table = interface.getFlowDeviceInfoByIP(exporter_ip,
							     true)
	 for _, ports in pairs(ports_table) do
	    for port_idx, port_info in pairs(ports) do
	       if not unified_exporters[exporter_ip][port_idx] then
		  unified_exporters[exporter_ip][port_idx] = port_info
	       else
		  local tmp = unified_exporters[exporter_ip][port_idx]
		  tmp["ifid"] = interface_id
		  tmp["bytes.in_bytes"] =
		     port_info["bytes.in_bytes"] +
		     (tmp["bytes.in_bytes"] or 0)
		  tmp["bytes.out_bytes"] =
		     port_info["bytes.out_bytes"] +
		     (tmp["bytes.out_bytes"] or 0)
		  if not tmp.ndpi then
		     tmp["ndpi"] = {}
		  end
		  for proto, proto_info in pairs(port_info.ndpi or {}) do
		     if not tmp["ndpi"][proto] then
			tmp["ndpi"][proto] = {}
		     end
		     for field, value in pairs(proto_info or {}) do
			if (type(value) == number) then
			   -- skip non numeric fields such as "breed"
			   tmp["ndpi"][proto][field] = value +
			      (tmp["ndpi"][proto][field] or
			       0)
			end
		     end
		  end
		  unified_exporters[exporter_ip][port_idx] = tmp
	       end
	    end
	 end
	 ::continue::
      end
   end

   return unified_exporters
end

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

function getExporterInfo(unique_source_id) -- Exporter unique_source_id
   local ifstats = interface.getStats()
   local exporter = {}
   local found = false
   if ifstats.probes then
      for interface_id, probe_list in pairs(ifstats.probes or {}) do
	 for probe_id, probe_info in pairs(probe_list or {}) do
	    if probe_info.exporters and table.len(probe_info.exporters) > 0 then -- Sflow or NetFlow/IPFIX
	       for exporter_ip, exporter_info in pairs(
		  probe_info.exporters or
		  {}) do
		  if exporter_info.unique_source_id == unique_source_id then
		     return table.merge({
			   exporter_ip = exporter_ip,
			   probe_ip = probe_info["probe.ip"],
			   ifid = interface_id
					}, exporter_info)
		  end
	       end
	    elseif probe_info["probe.source_id"] == unique_source_id then
	       return {
		  exporter_ip = probe_info["remote.if_addr"],
		  probe_ip = probe_info["probe.ip"],
		  ifid = interface_id
	       }
	    end
	 end
      end
   end

   return exporter
end

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

function getProbeFromUUID(nprobe_uuid)
   nprobe_uuid = tonumber(nprobe_uuid)
   local ifstats = interface.getStats()
   local exporter = {}
   local found = false
   if ifstats.probes then
      for interface_id, probe_list in pairs(ifstats.probes or {}) do
	 for probe_id, probe_info in pairs(probe_list or {}) do
	    if nprobe_uuid == probe_info["probe.source_id"] then
	       return probe_info
	    end
	 end
      end
   end

   return nil
end

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

function getProbeInfoFromExporterIP(ip)
   local ifstats = interface.getStats()
   for interface_id, probe_list in pairs(ifstats.probes or {}) do
      for probe_ip, probe_info in pairsByKeys(probe_list or {}) do
	 for exporter_ip, exporter_info in pairsByKeys(
	    probe_info.exporters or {}) do
	    if exporter_ip == ip then
	       return {
		  probe_uuid = tostring(probe_info["probe.source_id"]),
		  exporter_uuid = tostring(
		     exporter_info["unique_source_id"])
	       }
	    end
	 end
      end
   end

   return nil
end

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

function generateASNLink(asn)
   if (tostring(asn) == '0') then return nil end
   return string.format("%s/lua/as_overview.lua?asn=%s", ntop.getHttpPrefix(),
			asn)
end

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

function generateExporterLink(ip)
   if ntop.isPro() then
      -- Fallback SNMP not available, use exporter link
      local exporters_utils = require "exporters_utils"
      local exporter_uuid = nil
      local probe_uuid = nil
      local exporter_ifid = nil

      if tonumber(ip) then ip = ntop.inet_ntoa(ip) end
      exporter_uuid, exporter_ifid = exporters_utils.getExporterUUID(ip)
      probe_uuid = exporters_utils.getProbeUUID(ip)
      return string.format(
	 "%s/lua/pro/enterprise/exporters.lua?ip=%s&exporter_uuid=%s&probe_uuid=%s",
	 ntop.getHttpPrefix(), ip, exporter_uuid, probe_uuid)
   end

   return nil
end

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

function generateExporterInterfaceLink(ip, interface)
   if ntop.isPro() then
      -- Fallback SNMP not available, use exporter link
      local exporters_utils = require "exporters_utils"
      local exporter_uuid = nil
      local exporter_ifid = nil
      if tonumber(ip) then ip = ntop.inet_ntoa(ip) end

      exporter_uuid, exporter_ifid = exporters_utils.getExporterUUID(ip)

      return string.format(
	 "%s/lua/pro/enterprise/flowdevice_interface_details.lua?ip=%s&snmp_port_idx=%s&ifid=%s",
	 ntop.getHttpPrefix(), ip, interface, exporter_ifid)
   end

   return nil
end

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

function isSystemInterface(ifid)
   return (getSystemInterfaceId() == tostring(ifid))
end
