--
-- (C) 2020-24 - ntop.org
--
if (pragma_once_tag_utils == true) then
    -- io.write(debug.traceback().."\n")
    -- avoid multiple inclusions
    return
end

pragma_once_tag_utils = true

local dirs = ntop.getDirs()
package.path = dirs.installdir .. "/scripts/lua/modules/?.lua;" .. package.path

local alert_consts = require "alert_consts"
local host_pools = require "host_pools"
local consts = require "consts"
local qoe_utils

if ntop.isEnterpriseL() then
    package.path = dirs.installdir .. "/scripts/lua/pro/modules/?.lua;" .. package.path
    qoe_utils = require "qoe_utils"
end

local snmp_filter_options_cache

local tag_utils = {}

-- Operator Separator in query strings
tag_utils.SEPARATOR = consts.SEPARATOR

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

-- Supported operators
tag_utils.tag_operators = {
    ["eq"] = "=",
    ["neq"] = "!=",
    ["lt"] = "<",
    ["gt"] = ">",
    ["gte"] = ">=",
    ["lte"] = "<=",
    ["in"] = i18n("has"),
    ["nin"] = i18n("does_not_have")
}

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

-- Supported input types
tag_utils.input_types = {
    input = 'input',
    select = 'select',
    select_with_input = 'select-with-input'
}

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

tag_utils.defined_tags = {
    alert_id = {
        type = tag_utils.input_types.select,
        value_type = 'alert_id',
        i18n_label = i18n('db_search.tags.alert_id'),
        operators = { 'eq', 'neq' },
        hourly_available = false,
    },
    alert_category = {
        type = tag_utils.input_types.select,
        value_type = 'alert_category',
        i18n_label = i18n('db_search.tags.alert_category'),
        operators = { 'eq', 'neq' },
        hourly_available = false,
    },
    l7proto = {
        type = tag_utils.input_types.select,
        value_type = 'l7_proto',
        i18n_label = i18n('db_search.tags.l7_proto'),
        operators = { 'eq', 'neq' },
        hourly_available = true,
    },
    l7proto_master = {
        type = tag_utils.input_types.select,
        value_type = 'l7_proto',
        i18n_label = i18n('db_search.tags.l7_proto'),
        operators = { 'eq', 'neq' },
        hide = true,
        hourly_available = true,
    },
    l7cat = {
        type = tag_utils.input_types.select,
        value_type = 'l7_category',
        i18n_label = i18n('db_search.tags.l7cat'),
        operators = { 'eq', 'neq' },
        hourly_available = true,
    },
    flow_risk = {
        type = tag_utils.input_types.select,
        value_type = 'flow_risk',
        i18n_label = i18n('db_search.tags.flow_risk'),
        operators = { 'eq', 'neq', 'in', 'nin' },
        hourly_available = false,
    },
    l4proto = {
        type = tag_utils.input_types.select,
        value_type = 'l4_proto',
        i18n_label = i18n('db_search.tags.l4proto'),
        operators = { 'eq', 'neq' },
        bpf_key = 'ip proto',
        hourly_available = true,
    },
    ip_version = {
        type = tag_utils.input_types.select,
        value_type = 'ip_version',
        i18n_label = i18n('db_search.tags.ip_version'),
        operators = { 'eq', 'neq' },
        hourly_available = true,
    },
    ip = {
        value_type = 'ip,cidr', -- Set to 'ip' to accept IP only
        i18n_label = i18n('db_search.tags.ip'),
        operators = { 'eq', 'neq' },
        bpf_key = 'ip host',
        hourly_available = true,
    },
    cli_ip = {
        value_type = 'ip,cidr', -- Set to 'ip' to accept IP only
        i18n_label = i18n('db_search.tags.cli_ip'),
        operators = { 'eq', 'neq' },
        bpf_key = 'ip host',
        hourly_available = true,
    },
    srv_ip = {
        value_type = 'ip,cidr', -- Set to 'ip' to accept IP only
        i18n_label = i18n('db_search.tags.srv_ip'),
        operators = { 'eq', 'neq' },
        bpf_key = 'ip host',
        hourly_available = true,
    },
    network_cidr = {
        value_type = 'cidr',
        i18n_label = i18n('db_search.tags.network_cidr'),
        operators = { 'eq', 'neq' },
        bpf_key = 'net',
        hourly_available = true,
    },
    cli_network_cidr = {
        value_type = 'cidr',
        i18n_label = i18n('db_search.tags.cli_network_cidr'),
        operators = { 'eq', 'neq' },
        bpf_key = 'net',
        hourly_available = true,
    },
    srv_network_cidr = {
        value_type = 'cidr',
        i18n_label = i18n('db_search.tags.srv_network_cidr'),
        operators = { 'eq', 'neq' },
        bpf_key = 'net',
        hourly_available = true,
    },
    traffic_direction = {
        type = tag_utils.input_types.select,
        value_type = 'traffic_direction',
        i18n_label = i18n('db_search.tags.traffic_direction'),
        operators = { 'eq', 'neq' },
        hourly_available = false,
    },
    name = {
        value_type = 'hostname',
        i18n_label = i18n('db_search.tags.name'),
        operators = { 'eq', 'neq', 'in', 'nin' },
        hourly_available = true,
    },
    cli_name = {
        value_type = 'hostname',
        i18n_label = i18n('db_search.tags.cli_name'),
        operators = { 'eq', 'neq', 'in', 'nin' },
        hourly_available = true,
    },
    srv_name = {
        value_type = 'hostname',
        i18n_label = i18n('db_search.tags.srv_name'),
        operators = { 'eq', 'neq', 'in', 'nin' },
        hourly_available = true,
    },
    network_name = {
        value_type = 'text',
        i18n_label = i18n('db_search.tags.network_name'),
        operators = { 'eq', 'neq' },
        hourly_available = true,
    },
    src2dst_dscp = {
        type = tag_utils.input_types.select,
        value_type = 'dscp_type',
        i18n_label = i18n('db_search.tags.src2dst_dscp'),
        operators = { 'eq', 'neq' },
        hourly_available = false,
    },
    dst2src_dscp = {
        type = tag_utils.input_types.select,
        value_type = 'dscp_type',
        i18n_label = i18n('db_search.tags.dst2src_dscp'),
        operators = { 'eq', 'neq' },
        hourly_available = false,
    },
    duration = {
        value_type = 'number',
        i18n_label = i18n('db_search.duration'),
        operators = { 'eq', 'neq', 'lt', 'gt', 'gte', 'lte' },
        hourly_available = true,
    },
    cli_port = {
        value_type = 'port',
        i18n_label = i18n('db_search.tags.cli_port'),
        operators = { 'eq', 'neq', 'lt', 'gt', 'gte', 'lte' },
        bpf_key = 'port',
        hourly_available = false,
    },
    srv_port = {
        value_type = 'port',
        i18n_label = i18n('db_search.tags.srv_port'),
        operators = { 'eq', 'neq', 'lt', 'gt', 'gte', 'lte' },
        bpf_key = 'port',
        hourly_available = true,
    },
    country = {
        type = tag_utils.input_types.select,
        value_type = 'country',
        i18n_label = i18n('db_search.tags.country'),
        operators = { 'eq', 'neq' },
        hourly_available = true,
    },
    cli_country = {
        type = tag_utils.input_types.select,
        value_type = 'country',
        i18n_label = i18n('db_search.tags.cli_country'),
        operators = { 'eq', 'neq' },
        hourly_available = true,
    },
    srv_country = {
        type = tag_utils.input_types.select,
        value_type = 'country',
        i18n_label = i18n('db_search.tags.srv_country'),
        operators = { 'eq', 'neq' },
        hourly_available = true,
    },
    asn = {
        value_type = 'asn',
        i18n_label = i18n('db_search.tags.asn'),
        operators = { 'eq', 'neq' },
        hourly_available = true,
    },
    cli_asn = {
        value_type = 'asn',
        i18n_label = i18n('db_search.tags.cli_asn'),
        operators = { 'eq', 'neq' },
        hourly_available = true,
    },
    srv_asn = {
        value_type = 'asn',
        i18n_label = i18n('db_search.tags.srv_asn'),
        operators = { 'eq', 'neq' },
        hourly_available = true,
    },
    cli_nw_latency = {
        value_type = 'nw_latency_type',
        i18n_label = i18n('db_search.tags.cli_nw_latency'),
        operators = { 'eq', 'lt', 'gt', 'lte', 'gte' },
        hourly_available = false,
    },
    srv_nw_latency = {
        value_type = 'nw_latency_type',
        i18n_label = i18n('db_search.tags.srv_nw_latency'),
        operators = { 'eq', 'lt', 'gt', 'lte', 'gte' },
        hourly_available = false,
    },
    observation_point_id = {
        type = tag_utils.input_types.select,
        value_type = 'observation_point_id',
        i18n_label = i18n('db_search.tags.observation_point_id'),
        operators = { 'eq', 'neq' },
        hourly_available = false,
    },
    probe_ip = {
        type = tag_utils.input_types.select_with_input,
        value_type = 'probe_ip',
        i18n_label = i18n('db_search.tags.probe_ip'),
        operators = { 'eq', 'neq' },
        hourly_available = false,
    },
    vlan_id = {
        value_type = 'vlan_id',
        i18n_label = i18n('db_search.tags.vlan_id'),
        operators = { 'eq', 'neq', 'lt', 'gt', 'gte', 'lte' },
        hourly_available = true,
    },
    snmp_interface = {
        type = tag_utils.input_types.select,
        value_type = 'snmp_interface',
        i18n_label = i18n('db_search.tags.snmp_interface'),
        operators = { 'eq', 'neq' },
        hourly_available = true,
    },
    input_snmp = {
        type = tag_utils.input_types.select,
        value_type = 'snmp_interface',
        i18n_label = i18n('db_search.tags.input_snmp'),
        operators = { 'eq', 'neq' },
        hourly_available = true,
    },
    output_snmp = {
        type = tag_utils.input_types.select,
        value_type = 'snmp_interface',
        i18n_label = i18n('db_search.tags.output_snmp'),
        operators = { 'eq', 'neq' },
        hourly_available = true,
    },
    src2dst_tcp_flags = {
        value_type = 'flags',
        i18n_label = i18n('db_search.src2dst_tcp_flags'),
        operators = { 'eq', 'neq', 'in', 'nin' },
        hourly_available = false,
    },
    dst2src_tcp_flags = {
        value_type = 'flags',
        i18n_label = i18n('db_search.dst2src_tcp_flags'),
        operators = { 'eq', 'neq', 'in', 'nin' },
        hourly_available = false,
    },
    alert_status = {
        type = tag_utils.input_types.select,
        value_type = 'alert_status',
        i18n_label = i18n('db_search.tags.alert_status'),
        operators = { 'eq', 'neq' },
        hourly_available = false,
    },
    require_attention = {
        value_type = 'boolean',
        i18n_label = i18n('db_search.tags.require_attention'),
        operators = { 'eq', 'neq' },
        hourly_available = false,
    },
    severity = {
        type = tag_utils.input_types.select,
        value_type = 'severity',
        i18n_label = i18n('db_search.tags.severity'),
        operators = { 'eq', 'lte', 'gte', 'neq' },
        hourly_available = false,
    },
    score = {
        value_type = 'score',
        i18n_label = i18n('db_search.tags.score'),
        operators = { 'eq', 'neq', 'lt', 'gt', 'gte', 'lte' },
        hourly_available = true,
    },
    qoe_score = {
        type = tag_utils.input_types.select,
        value_type = 'qoe_score',
        i18n_label = i18n('db_search.tags.qoe'),
        operators = { 'eq', 'neq', 'lt', 'gt', 'gte', 'lte' },
        hourly_available = false,
    },
    mac = {
        value_type = 'mac',
        i18n_label = i18n('db_search.tags.mac'),
        operators = { 'eq', 'neq' },
        bpf_key = 'ether host',
        hourly_available = true,
    },
    cli_mac = {
        value_type = 'mac',
        i18n_label = i18n('db_search.tags.cli_mac'),
        operators = { 'eq', 'neq' },
        bpf_key = 'ether host',
        hourly_available = true,
    },
    srv_mac = {
        value_type = 'mac',
        i18n_label = i18n('db_search.tags.srv_mac'),
        operators = { 'eq', 'neq' },
        bpf_key = 'ether host',
        hourly_available = true,
    },
    network = {
        type = tag_utils.input_types.select,
        value_type = 'network_id',
        i18n_label = i18n('db_search.tags.network'),
        operators = { 'eq', 'neq' },
        hourly_available = true,
    },
    cli_network = {
        type = tag_utils.input_types.select,
        value_type = 'network_id',
        i18n_label = i18n('db_search.tags.cli_network'),
        operators = { 'eq', 'neq' },
        hourly_available = true,
    },
    srv_network = {
        type = tag_utils.input_types.select,
        value_type = 'network_id',
        i18n_label = i18n('db_search.tags.srv_network'),
        operators = { 'eq', 'neq' },
        hourly_available = true,
    },
    info = {
        value_type = 'text',
        i18n_label = i18n('db_search.tags.info'),
        operators = { 'eq', 'neq', 'in', 'nin' },
        hourly_available = false,
    },
    bytes = {
        value_type = 'bytes',
        i18n_label = i18n('db_search.tags.bytes'),
        operators = { 'eq', 'neq', 'lt', 'gt', 'gte', 'lte' },
        hourly_available = true,
    },
    cli2srv_bytes = {
        type = tag_utils.input_types.input,
        value_type = 'number',
        i18n_label = i18n('traffic_labels.cli2srv_bytes'),
        operators = { 'eq', 'neq', 'lt', 'gt', 'gte', 'lte' },
        hourly_available = true,
    },
    srv2cli_bytes = {
        type = tag_utils.input_types.input,
        value_type = 'number',
        i18n_label = i18n('traffic_labels.srv2cli_bytes'),
        operators = { 'eq', 'neq', 'lt', 'gt', 'gte', 'lte' },
        hourly_available = true,
    },
    cli2srv_packets = {
        value_type = 'packets',
        i18n_label = i18n('traffic_labels.cli2srv_packets'),
        operators = { 'eq', 'neq', 'lt', 'gt', 'gte', 'lte' },
        hourly_available = true,
    },
    srv2cli_packets = {
        value_type = 'packets',
        i18n_label = i18n('traffic_labels.srv2cli_packets'),
        operators = { 'eq', 'neq', 'lt', 'gt', 'gte', 'lte' },
        hourly_available = true,
    },
    number = {
        value_type = 'number',
        i18n_label = i18n('db_search.tags.number'),
        operators = { 'eq', 'neq', 'lt', 'gt', 'gte', 'lte' },
        hourly_available = false,
    },
    out_of_order = {
        value_type = 'number',
        i18n_label = i18n('db_search.tags.out_of_order'),
        operators = { 'eq', 'neq', 'lt', 'gt', 'gte', 'lte' },
        hourly_available = false,
    },
    lost = {
        value_type = 'number',
        i18n_label = i18n('db_search.tags.lost'),
        operators = { 'eq', 'neq', 'lt', 'gt', 'gte', 'lte' },
        hourly_available = false,
    },
    retransmissions = {
        value_type = 'number',
        i18n_label = i18n('db_search.tags.retransmissions'),
        operators = { 'eq', 'neq', 'lt', 'gt', 'gte', 'lte' },
        hourly_available = false,
    },
    flows_number = {
        value_type = 'number',
        i18n_label = i18n('db_search.tags.flows_number'),
        operators = { 'eq', 'neq', 'lt', 'gt', 'gte', 'lte' },
        hourly_available = true,
    },
    packets = {
        value_type = 'packets',
        i18n_label = i18n('db_search.tags.packets'),
        operators = { 'eq', 'neq', 'lt', 'gt', 'gte', 'lte' },
        hourly_available = true,
    },
    host_pool = {
        type = tag_utils.input_types.select,
        value_type = 'host_pool',
        i18n_label = i18n('db_search.tags.host_pool_id'),
        operators = { 'eq', 'neq', 'lt', 'gt', 'gte', 'lte' },
        hourly_available = true,
    },
    cli_host_pool_id = {
        type = tag_utils.input_types.select,
        value_type = 'host_pool_id',
        i18n_label = i18n('db_search.tags.cli_host_pool_id'),
        operators = { 'eq', 'neq', 'lt', 'gt', 'gte', 'lte' },
        hourly_available = true,
    },
    srv_host_pool_id = {
        type = tag_utils.input_types.select,
        value_type = 'host_pool_id',
        i18n_label = i18n('db_search.tags.srv_host_pool_id'),
        operators = { 'eq', 'neq', 'lt', 'gt', 'gte', 'lte' },
        hourly_available = true,
    },
    subtype = {
        value_type = 'text',
        i18n_label = i18n('db_search.tags.subtype'),
        operators = { 'eq', 'neq' },
        hourly_available = false,
    },
    role = {
        type = tag_utils.input_types.select,
        value_type = 'role',
        i18n_label = i18n('db_search.tags.role'),
        operators = { 'eq' },
        hourly_available = false,
    },
    role_cli_srv = {
        type = tag_utils.input_types.select,
        value_type = 'role_cli_srv',
        i18n_label = i18n('db_search.tags.role_cli_srv'),
        operators = { 'eq' },
        hourly_available = false,
    },
    l7_error_id = {
        value_type = 'l7_error_id',
        i18n_label = i18n('db_search.tags.error_code'),
        operators = { 'eq', 'neq' },
        hourly_available = false,
    },
    confidence = {
        type = tag_utils.input_types.select,
        value_type = 'confidence',
        i18n_label = i18n('db_search.tags.confidence'),
        operators = { 'eq', 'neq' },
        hourly_available = false,
    },
    community_id = {
        value_type = 'text',
        i18n_label = i18n('db_search.tags.community_id'),
        operators = { 'eq', 'neq', 'in', 'nin' },
        hourly_available = false,
    },
    cli_fingerprint = {
        value_type = 'text',
        i18n_label = i18n('db_search.tags.cli_fingerprint'),
        operators = { 'eq', 'neq' },
        hourly_available = false,
    },
    --ja4_client = {
    --    value_type = 'text',
    --    i18n_label = i18n('ja4_client_hash'),
    --    operators = { 'eq', 'neq' }
    --},
    http_method = {
        type = tag_utils.input_types.select,
        value_type = 'http_method',
        i18n_label = i18n('db_search.tags.http_method'),
        operators = { 'eq', 'neq' },
        hourly_available = false,
    },
    http_return = {
        type = tag_utils.input_types.select,
        value_type = 'http_return',
        i18n_label = i18n('db_search.tags.http_return_code'),
        operators = { 'eq', 'neq' },
        hourly_available = false,
    },
    http_url = {
        value_type = 'http_url',
        i18n_label = i18n('db_search.tags.http_url'),
        operators = { 'eq', 'neq', 'in', 'nin' },
        hourly_available = false,
    },
    issuer_dn = {
        value_type = 'text',
        i18n_label = i18n('db_search.tags.issuer_dn'),
        operators = { 'eq', 'neq', 'in', 'nin' },
        hourly_available = false,
    },
    user_agent = {
        value_type = 'user_agent',
        i18n_label = i18n('db_search.tags.user_agent'),
        operators = { 'eq', 'neq', 'in', 'nin' },
        hourly_available = false,
    },
    last_server = {
        value_type = 'text',
        i18n_label = i18n('db_search.tags.last_server'),
        operators = { 'eq', 'neq', 'in', 'nin' },
        hourly_available = false,
    },
    netbios_name = {
        value_type = 'netbios_name',
        i18n_label = i18n('db_search.tags.netbios_name'),
        operators = { 'eq', 'neq', 'in', 'nin' },
        hourly_available = false,
    },
    dns_query = {
        value_type = 'text',
        i18n_label = i18n('db_search.tags.dns_query'),
        operators = { 'eq', 'neq', 'in', 'nin' },
        hourly_available = false,
    },
    dns_answer = {
        value_type = 'dns_answer',
        i18n_label = i18n('db_search.tags.dns_answer'),
        operators = { 'eq', 'neq', 'in', 'nin' },
        hourly_available = false,
    },
    mdns_answer = {
        value_type = 'text',
        i18n_label = i18n('db_search.tags.mdns_answer'),
        operators = { 'eq', 'neq' },
        hourly_available = false,
    },
    mdns_name = {
        value_type = 'text',
        i18n_label = i18n('db_search.tags.mdns_name'),
        operators = { 'eq', 'neq', 'in', 'nin' },
        hourly_available = false,
    },
    mdns_name_txt = {
        value_type = 'text',
        i18n_label = i18n('db_search.tags.mdns_name_txt'),
        operators = { 'eq', 'neq', 'in', 'nin' },
        hourly_available = false,
    },
    mdns_ssid = {
        value_type = 'text',
        i18n_label = i18n('db_search.tags.mdns_ssid'),
        operators = { 'eq', 'neq', 'in', 'nin' },
        hourly_available = false,
    },
    domain_name = {
        value_type = 'text',
        i18n_label = i18n('db_search.tags.domain_name'),
        operators = { 'eq', 'neq', 'in', 'nin' },
        hourly_available = false,
    },
    alert_domain = {
        value_type = 'text',
        i18n_label = i18n('db_search.tags.dga_domain_name'),
        operators = { 'eq', 'neq', 'in', 'nin' },
        hourly_available = false,
    },
    cli_location = {
        type = tag_utils.input_types.select,
        value_type = 'location',
        i18n_label = i18n('db_search.tags.cli_location'),
        operators = { 'eq', 'neq' },
        hourly_available = true,
    },
    srv_location = {
        type = tag_utils.input_types.select,
        value_type = 'location',
        i18n_label = i18n('db_search.tags.srv_location'),
        operators = { 'eq', 'neq' },
        hourly_available = true,
    },
    host_location = {
        type = tag_utils.input_types.select,
        value_type = 'location',
        i18n_label = i18n('db_search.tags.host_location'),
        operators = { 'eq', 'neq' },
        hourly_available = true,
    },
    cli_proc_name = {
        value_type = 'text',
        i18n_label = i18n('db_search.tags.cli_proc_name'),
        operators = { 'eq', 'neq' },
        hourly_available = false,
    },
    srv_proc_name = {
        value_type = 'text',
        i18n_label = i18n('db_search.tags.srv_proc_name'),
        operators = { 'eq', 'neq' },
        hourly_available = false,
    },
    cli_user_name = {
        value_type = 'text',
        i18n_label = i18n('db_search.tags.cli_user_name'),
        operators = { 'eq', 'neq' },
        hourly_available = false,
    },
    srv_user_name = {
        value_type = 'text',
        i18n_label = i18n('db_search.tags.srv_user_name'),
        operators = { 'eq', 'neq' },
        hourly_available = false,
    },
    major_connection_state = {
        type = tag_utils.input_types.select,
        value_type = 'major_connection_state',
        i18n_label = i18n("db_search.tags.major_connection_state"),
        operators = { 'eq', 'neq' },
        hourly_available = false,
    },
    minor_connection_state = {
        type = tag_utils.input_types.select,
        value_type = 'minor_connection_state',
        i18n_label = i18n("db_search.tags.minor_connection_state"),
        operators = { 'eq', 'neq' },
        hourly_available = false,
    },
    pre_nat_ipv4_src_addr = {
        value_type = 'ip',
        i18n_label = i18n("db_search.tags.pre_nat_ipv4_src_addr"),
        operators = { 'eq', 'neq' },
        hourly_available = false,
    },
    pre_nat_ipv4_dst_addr = {
        value_type = 'ip',
        i18n_label = i18n("db_search.tags.pre_nat_ipv4_dst_addr"),
        operators = { 'eq', 'neq' },
        hourly_available = false,
    },
    post_nat_ipv4_src_addr = {
        value_type = 'ip',
        i18n_label = i18n("db_search.tags.post_nat_ipv4_src_addr"),
        operators = { 'eq', 'neq' },
        hourly_available = false,
    },
    post_nat_ipv4_dst_addr = {
        value_type = 'ip',
        i18n_label = i18n("db_search.tags.post_nat_ipv4_dst_addr"),
        operators = { 'eq', 'neq' },
        hourly_available = false,
    },
    pre_nat_src_port = {
        value_type = 'port',
        i18n_label = i18n("db_search.tags.pre_nat_src_port"),
        operators = { 'eq', 'neq' },
        hourly_available = false,
    },
    pre_nat_dst_port = {
        value_type = 'port',
        i18n_label = i18n("db_search.tags.pre_nat_dst_port"),
        operators = { 'eq', 'neq' },
        hourly_available = false,
    },
    post_nat_src_port = {
        value_type = 'port',
        i18n_label = i18n("db_search.tags.post_nat_src_port"),
        operators = { 'eq', 'neq' },
        hourly_available = false,
    },
    post_nat_dst_port = {
        value_type = 'port',
        i18n_label = i18n("db_search.tags.post_nat_dst_port"),
        operators = { 'eq', 'neq' },
        hourly_available = false,
    },
    description = {
        value_type = 'text',
        i18n_label = i18n('db_search.tags.alert_description'),
        operators = { 'in' },
        hourly_available = false,
    },
    mitre_tactic = {
        type = tag_utils.input_types.select,
        value_type = 'mitre_tactic',
        i18n_label = i18n('db_search.tags.mitre_tactic'),
        operators = { 'eq', 'neq' },
        hourly_available = false,
    },
    mitre_technique = {
        type = tag_utils.input_types.select,
        value_type = 'mitre_technique',
        i18n_label = i18n('db_search.tags.mitre_technique'),
        operators = { 'eq', 'neq' },
        hourly_available = false,
    },
    mitre_subtechnique = {
        type = tag_utils.input_types.select,
        value_type = 'mitre_subtechnique',
        i18n_label = i18n('db_search.tags.mitre_subtechnique'),
        operators = { 'eq', 'neq' },
        hourly_available = false,
    },
    mitre_id = {
        type = tag_utils.input_types.select,
        value_type = 'mitre_id',
        i18n_label = i18n('db_search.tags.mitre_id'),
        operators = { 'eq', 'neq' },
        hourly_available = false,
    },
    wlan_ssid = {
        type = tag_utils.input_types.select,
        value_type = 'wlan_ssid',
        i18n_label = i18n('db_search.tags.wlan_ssid'),
        operators = { 'eq', 'neq', 'in', 'nin' },
        hourly_available = false,
    },
    apn_mac = {
        value_type = 'mac',
        i18n_label = i18n('db_search.tags.apn_mac'),
        operators = { 'eq', 'neq' },
        hourly_available = false,
    },
}
-- #####################################

tag_utils.traffic_direction = { {
    label = i18n("flows_page.remote_only"),
    id = 0
}, {
    label = i18n("flows_page.local_only"),
    id = 1
}, {
    label = i18n("flows_page.local_srv_remote_cli"),
    id = 2
}, {
    label = i18n("flows_page.local_cli_remote_srv"),
    id = 3
} }

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

tag_utils.confidence = { {
    label = i18n("confidence_unknown"),
    id = -1
}, {
    label = i18n("confidence_guessed"),
    id = 0
}, {
    label = i18n("confidence_dpi"),
    id = 1
} }

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

tag_utils.http_return = { {
    label = i18n("http_info.return_codes.200"),
    id = 200
}, {
    label = i18n("http_info.return_codes.400"),
    id = 400
}, {
    label = i18n("http_info.return_codes.401"),
    id = 401
}, {
    label = i18n("http_info.return_codes.403"),
    id = 403
}, {
    label = i18n("http_info.return_codes.404"),
    id = 404
}, {
    label = i18n("http_info.return_codes.405"),
    id = 405
}, {
    label = i18n("http_info.return_codes.406"),
    id = 406
}, {
    label = i18n("http_info.return_codes.408"),
    id = 408
}, {
    label = i18n("http_info.return_codes.409"),
    id = 409
}, {
    label = i18n("http_info.return_codes.410"),
    id = 410
}, {
    label = i18n("http_info.return_codes.412"),
    id = 412
}, {
    label = i18n("http_info.return_codes.415"),
    id = 415
}, {
    label = i18n("http_info.return_codes.423"),
    id = 423
}, {
    label = i18n("http_info.return_codes.428"),
    id = 428
}, {
    label = i18n("http_info.return_codes.429"),
    id = 429
}, {
    label = i18n("http_info.return_codes.500"),
    id = 500
}, {
    label = i18n("http_info.return_codes.501"),
    id = 501
}, {
    label = i18n("http_info.return_codes.503"),
    id = 503
} }

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

tag_utils.http_method = { {
    label = i18n("http_info.methods.get"),
    id = 'GET'
}, {
    label = i18n("http_info.methods.head"),
    id = 'HEAD'
}, {
    label = i18n("http_info.methods.post"),
    id = 'POST'
}, {
    label = i18n("http_info.methods.put"),
    id = 'PUT'
}, {
    label = i18n("http_info.methods.delete"),
    id = 'DELETE'
}, {
    label = i18n("http_info.methods.connect"),
    id = 'CONNECT'
}, {
    label = i18n("http_info.methods.options"),
    id = 'OPTIONS'
}, {
    label = i18n("http_info.methods.trace"),
    id = 'TRACE'
}, {
    label = i18n("http_info.methods.patch"),
    id = 'PATCH'
} }

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

tag_utils.location = { {
    label = i18n("details.label_remote"),
    id = 0
}, {
    label = i18n("details.label_local_host"),
    id = 1
}, {
    label = i18n("multicast"),
    id = 2
} }

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

function tag_utils.build_request_filter(key, op, value)
    return key .. '=' .. value .. tag_utils.SEPARATOR .. op
end

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

function tag_utils.get_tag_filters_from_request()
    local filters = {}
    for key, value in pairs(tag_utils.defined_tags) do
        if _GET[key] ~= nil then
            filters[key] = _GET[key]
        end
    end

    if _GET["host"] then -- from the host page
        filters["ip"] = _GET["host"] -- convert to tag key
    end

    for key, value in pairs(_GET or {}) do
        if key:starts('custom_fields') then
            filters[key] = value
        end
    end

    if not isEmptyString(filters['l7proto']) then
        local l7proto = ""
        -- Splitting per multiple l7protos
        for _, v in pairs(split(filters['l7proto'], ",")) do
            local l7string = ""
            -- Splitting per ; (e.g. 217.16;eq)
            local tmp_proto = split(v, ";")

            if tmp_proto[1] then
                -- Splitting per . , to get both master proto and app proto
                local app_protos = split(tmp_proto[1], "%%.")

                if not tonumber(app_protos[1]) then
                    app_protos[1] = interface.getnDPIProtoId(app_protos[1])
                end

                l7string = app_protos[1]

                if app_protos[2] then
                    if not tonumber(app_protos[2]) then
                        app_protos[2] = interface.getnDPIProtoId(app_protos[2])
                    end

                    l7string = l7string .. '.' .. app_protos[2]
                end
            end

            l7proto = l7proto .. l7string .. ";" .. tmp_proto[2] .. ","
        end

        filters['l7proto'] = l7proto:sub(1, -2)
    end

    return filters
end

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

-- @brief Evaluate operator
function tag_utils.eval_op(v1, op, v2)
    local default_verdict = true

    -- Convert boolean for compatibility
    if type(v1) == 'boolean' then
        if v1 then
            v1 = 1
        else
            v1 = 0
        end
    end

    if not v1 or not v2 then
        return default_verdict
    end

    if op == 'eq' then
        return v1 == v2
    elseif op == 'neq' then
        return v1 ~= v2
    elseif op == 'lt' then
        return v1 < v2
    elseif op == 'gt' then
        return v1 > v2
    elseif op == 'gte' then
        return v1 >= v2
    elseif op == 'lte' then
        return v1 <= v2
    elseif op == 'in' then
        v_and = v1 & v2
        return v1 == v_and
    elseif op == 'nin' then
        v_and = v1 & v2
        return v1 ~= v_and
    end

    return default_verdict
end

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

tag_utils.formatters = {
    l4proto = function(proto)
        return l4_proto_to_string(proto)
    end,
    l4_proto = function(proto)
        return l4_proto_to_string(proto)
    end,
    l7_proto = function(proto)
        return interface.getnDPIProtoName(tonumber(proto))
    end,
    l7proto = function(proto)
        return interface.getnDPIProtoName(tonumber(proto))
    end,
    l7cat = function(cat)
        return getCategoryLabel(interface.getnDPICategoryName(tonumber(cat)), tonumber(cat))
    end,
    severity = function(severity)
        return (i18n(alert_consts.alertSeverityById(tonumber(severity)).i18n_title))
    end,
    alert_status = function(status)
        return (i18n(alert_consts.alertSeverityById(tonumber(status)).i18n_title))
    end,
    alert_id = function(status)
        local alert_entities = require "alert_entities"
        return alert_consts.alertTypeLabel(status, true, alert_entities.flow.entity_id)
    end,
    alert_category = function(category_id)
        local alert_category_utils = require "alert_category_utils"
        return alert_category_utils.getCategoryById(category_id)
    end,
    role = function(role)
        return (i18n(role))
    end,
    role_cli_srv = function(role)
        return (i18n(role))
    end,
    flow_risk = function(risk)
        local flow_risk_list = ntop.getRiskList() or {}
        flow_risk_list[1] = i18n("flow_risk.ndpi_no_risk")
        return flow_risk_list[tonumber(risk) + 1] or risk
    end
}

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

function tag_utils.get_tag_info(id, entity, hide_exporters_name, restrict_filters, ifid, is_aggregated)
    local alert_utils = require "alert_utils"
    local tag = tag_utils.defined_tags[id]
    local changed_ifid = false
    local current_ifid = interface.getId()
    if tag == nil then
        -- traceError(TRACE_WARNING, TRACE_CONSOLE, "Tag " .. id .. " not found")
        return nil
    end

    if not ifid then
        ifid = interface.getId()
    else
        if tonumber(current_ifid) ~= tonumber(ifid) then
            changed_ifid = true
            interface.select(ifid)
        end
    end

    if is_aggregated and tag.hourly_available 
        and (is_aggregated ~= tag.hourly_available)
        or is_aggregated and not tag.hourly_available then
        return nil
    end

    local filter = {
        id = id,
        label = tag.i18n_label,
        value_type = tag.value_type,
        value_label = tag.value_i18n_label or tag.i18n_label,
        operators = {},
        type = tag.type
    }

    for _, op in ipairs(tag.operators) do
        filter.operators[#filter.operators + 1] = {
            id = op,
            label = tag_utils.tag_operators[op]
        }
    end

    -- select (array of values)
    -- tprint(tag.value_type)
    if (tag.value_type == "alert_id" or tag.value_type == "alert_type" --[[ alert_id should be used --]]) and entity ~=
        nil then
        filter.value_type = 'array'
        filter.options = {}
        local alert_types = alert_consts.getAlertTypesInfo(entity.entity_id)
        for id, info in pairsByField(alert_types, 'label', asc) do
            filter.options[#filter.options + 1] = {
                value = id,
                label = info.label
            }
        end
    elseif tag.value_type == "qoe_score" then
        if not qoe_utils then
            -- Exclude the filter if it's not L, this info is found only in the l version
            filter = nil
            return nil
        end
        filter.value_type = 'array'
        filter.options = {}
        for _, info in pairsByKeys(qoe_utils.getPossibleQoE(), asc) do
            filter.options[#filter.options + 1] = {
                value = info.value,
                label = i18n(info.i18n_label),
            }
        end
    elseif tag.value_type == "mitre_id" then
        filter.value_type = 'array'
        filter.options = {}
        for name, info in pairsByField(alert_consts.getAllAlertMitreInfoIDs(), 'mitre_id', asc) do
            filter.options[#filter.options + 1] = {
                value = info.mitre_id,
                label = info.mitre_id,
            }
        end
    elseif tag.value_type == "wlan_ssid" then
        local flows_stats = interface.getActiveFlowsStats()
        filter.value_type = 'array'
        filter.options = {}
        local tmp_list = {}

        if table.len(flows_stats["wlan_ssid"]) > 0 then
            local tmp_list = {}
            for key, value in pairs(flows_stats["wlan_ssid"] or {}, asc) do
                tmp_list[key] = {
                    key = "wlan_ssid",
                    value = key,
                    label = key
                }
            end

            for key, _ in pairsByKeys(tmp_list, asc) do
                filter.options[#filter.options + 1] = {
                    value = key,
                    label = key,
                }
            end
        end
    elseif tag.value_type == "mitre_tactic" then
        filter.value_type = 'array'
        filter.options = {}
        local mitre_utils = require "mitre_utils"
        for name, info in pairsByField(mitre_utils.tactic, 'i18n_label', asc) do
            filter.options[#filter.options + 1] = {
                value = info.id,
                label = i18n(info.i18n_label)
            }
        end
    elseif tag.value_type == "mitre_technique" then
        filter.value_type = 'array'
        filter.options = {}
        local mitre_utils = require "mitre_utils"
        for name, info in pairsByField(mitre_utils.technique, 'i18n_label', asc) do
            filter.options[#filter.options + 1] = {
                value = info.id,
                label = i18n(info.i18n_label)
            }
        end
    elseif tag.value_type == "mitre_subtechnique" then
        filter.value_type = 'array'
        filter.options = {}
        local mitre_utils = require "mitre_utils"
        for name, info in pairsByField(mitre_utils.sub_technique, 'i18n_label', asc) do
            filter.options[#filter.options + 1] = {
                value = info.id,
                label = i18n(info.i18n_label)
            }
        end
    elseif tag.value_type == "alert_category" then
        filter.value_type = 'array'
        filter.options = {}
        local alert_categories = require "alert_categories"
        for name, info in pairsByField(alert_categories, 'i18n_title', asc) do
            filter.options[#filter.options + 1] = {
                value = info.id,
                label = i18n(info.i18n_title)
            }
        end
    elseif tag.value_type == "dscp_type" then
        local dscp_consts = require "dscp_consts"
        filter.value_type = 'array'
        filter.options = {}
        local dscp_types = dscp_consts.dscp_class_list()
        for id, info in pairsByField(dscp_types, 'label', asc) do
            filter.options[#filter.options + 1] = {
                value = id,
                label = info.label
            }
        end
    elseif tag.value_type == "flow_risk" then
        filter.value_type = 'array'
        filter.options = {}
        local flow_risk_list = ntop.getRiskList() or {}
        if table.len(flow_risk_list) > 0 then
            flow_risk_list[1] = i18n("flow_risk.ndpi_no_risk")
        end
        for id, info in pairsByValues(flow_risk_list, asc) do
            filter.options[#filter.options + 1] = {
                value = id - 1,
                label = info
            }
        end
    elseif tag.value_type == "host_pool_id" or tag.value_type == "host_pool" then
        filter.value_type = 'array'
        filter.options = {}
        local host_pools_instance = host_pools:create()
        local host_pools_stats = interface.getHostPoolsStats()
        local host_pool_list = {}
        for pool_id, _ in pairs(host_pools_stats) do
            local label = host_pools_instance:get_pool_name(pool_id)
            filter.options[#filter.options + 1] = {
                value = pool_id,
                label = label
            }
        end
    elseif tag.value_type == "minor_connection_state" then
        local flow_consts = require "flow_consts"
        filter.value_type = 'array'
        filter.options = {}
        for state, id in pairs(flow_consts.minor_connection_states) do
            -- EXCLUDE NO_STATE
            if (id ~= 0) then
                filter.options[#filter.options + 1] = {
                    value = id,
                    label = i18n(string.format("db_search.tags.minor_connection_states.%u", id))
                }
            end
        end
    elseif tag.value_type == "major_connection_state" then
        local flow_consts = require "flow_consts"
        filter.value_type = 'array'
        filter.options = {}
        for state, id in pairs(flow_consts.major_connection_states) do
            -- EXCLUDE NO_STATE
            if (id ~= 0) then
                filter.options[#filter.options + 1] = {
                    value = id,
                    label = i18n(string.format("flow_fields_description.major_connection_states.%u", id))
                }
            end
        end
    elseif tag.value_type == "traffic_direction" then
        filter.value_type = 'array'
        filter.options = {}
        for _, v in pairsByField(tag_utils.traffic_direction, 'label', asc) do
            filter.options[#filter.options + 1] = {
                value = v.id,
                label = v.label
            }
        end
    elseif tag.value_type == "confidence" then
        filter.value_type = 'array'
        filter.options = {}
        for _, v in pairsByField(tag_utils.confidence, 'label', asc) do
            filter.options[#filter.options + 1] = {
                value = v.id,
                label = v.label
            }
        end
    elseif tag.value_type == "http_return" then
        filter.value_type = 'array'
        filter.options = {}
        for _, v in pairsByField(tag_utils.http_return, 'label', asc) do
            filter.options[#filter.options + 1] = {
                value = v.id,
                label = v.label
            }
        end
    elseif tag.value_type == "http_method" then
        filter.value_type = 'array'
        filter.options = {}
        for _, v in pairsByField(tag_utils.http_method, 'label', asc) do
            filter.options[#filter.options + 1] = {
                value = v.id,
                label = v.label
            }
        end
    elseif tag.value_type == "location" then
        filter.value_type = 'array'
        filter.options = {}
        for _, v in pairsByField(tag_utils.location, 'label', asc) do
            filter.options[#filter.options + 1] = {
                value = v.id,
                label = v.label
            }
        end
    elseif tag.value_type == "l4_proto" then
        filter.value_type = 'array'
        filter.options = {}
        local l4_protocol_list = require "l4_protocol_list"

        local l4_protocols = l4_protocol_list.l4_keys

        local list = {}

        for _, proto in pairs(l4_protocols) do
            -- add L4 proto only
            if proto[2] ~= 'ip' and proto[2] ~= 'ipv6' then
                list[proto[1]] = proto[3]
            end
        end

        for name, id in pairsByKeys(list, asc) do
            filter.options[#filter.options + 1] = {
                value = id,
                label = name
            }
        end
    elseif tag.value_type == "l7_proto" then
        filter.value_type = 'array'
        filter.options = {}
        local l7_protocols = interface.getnDPIProtocols()
        for name, id in pairsByKeys(l7_protocols, asc) do
            filter.options[#filter.options + 1] = {
                value = id,
                label = name
            }
        end
    elseif tag.value_type == "l7_category" then
        filter.value_type = 'array'
        filter.options = {}
        local l7_categories = interface.getnDPICategories()
        for name, id in pairsByKeys(l7_categories, asc) do
            filter.options[#filter.options + 1] = {
                value = id,
                label = getCategoryLabel(name, id)
            }
        end
    elseif tag.value_type == "network_id" then
        filter.options = {}
        local networks_stats = interface.getNetworksStats()
        for n, ns in pairs(networks_stats) do
            filter.options[#filter.options + 1] = {
                value = ns.network_id,
                label = getFullLocalNetworkName(ns.network_key)
            }
        end
    elseif tag.value_type == "observation_point_id" then
        filter.value_type = 'array'
        filter.options = {}
        local obs_points = interface.getObsPointsInfo()
        local obs_points_list = {}
        for _, stats in pairs(obs_points["ObsPoints"] or {}) do
            obs_points_list[#obs_points_list + 1] = {
                alias = getFullObsPointName(stats["obs_point"]),
                id = stats["obs_point"]
            }
        end
        for _, v in pairsByField(obs_points_list, 'alias', asc) do
            filter.options[#filter.options + 1] = {
                value = v.id,
                label = v.alias
            }
        end
    elseif tag.value_type == "country" then
        local country_codes = require "country_codes"
        filter.value_type = 'array'
        filter.options = {}
        for code, label in pairsByValues(country_codes, asc) do
            local id = code
            -- if entity == nil then -- historical flows
            --   id = interface.convertCountryCode2U16(code)
            -- end
            filter.options[#filter.options + 1] = {
                value = id,
                label = label
            }
        end
    elseif tag.value_type == "vlan_id" then
        filter.options = {}
        local vlans = interface.getVLANsList()

        if vlans == nil then
            vlans = {
                VLANs = {}
            }
        end
        vlans = vlans["VLANs"]
        for _, vlan in pairs(vlans) do
            local vlan_name = getFullVlanName(vlan["vlan_id"])
            if isEmptyString(vlan_name) then
                vlan_name = i18n('no_vlan')
            end
            filter.options[#filter.options + 1] = {
                value = vlan["vlan_id"],
                label = vlan_name
            }
        end
    elseif tag.value_type == "probe_ip" then
        filter.options = {}
        local full_dev_list = {}

        -- Add both Flow devices
        if interface.getFlowDevices then -- Pro Only
            for exporter_ip, _ in pairs(getExporterList()) do
                local probe_name = getProbeName(exporter_ip)
                full_dev_list[exporter_ip] = {
                    value = exporter_ip,
                    label = probe_name,
                    display_more_filters = true
                }
            end
        end
        -- And sFlow devices
        if interface.getSFlowDevices then -- Pro Only
            for interface, device_list in pairs(interface.getSFlowDevices() or {}) do
                for probe, _ in pairsByValues(device_list or {}, asc) do
                    local probe_name = getProbeName(probe)
                    -- local label = format_name_value(probe_name, probe)
                    full_dev_list[probe] = {
                        value = probe,
                        label = probe_name,
                        display_more_filters = true
                    }
                end
            end
        end

        for _, device_info in pairsByKeys(full_dev_list, asc) do
            filter.options[#filter.options + 1] = device_info
        end
    elseif tag.value_type == "ip_version" then
        filter.value_type = 'array'
        filter.options = {}
        filter.options[#filter.options + 1] = {
            value = "4",
            label = i18n("ipv4")
        }
        filter.options[#filter.options + 1] = {
            value = "6",
            label = i18n("ipv6")
        }
    elseif tag.value_type == "role" then
        filter.value_type = 'array'
        filter.options = {}
        filter.options[#filter.options + 1] = {
            value = "attacker",
            label = i18n("attacker")
        }
        filter.options[#filter.options + 1] = {
            value = "victim",
            label = i18n("victim")
        }
        filter.options[#filter.options + 1] = {
            value = "no_attacker_no_victim",
            label = i18n("no_attacker_no_victim")
        }
    elseif tag.value_type == "role_cli_srv" then
        filter.value_type = 'array'
        filter.options = {}
        filter.options[#filter.options + 1] = {
            value = "client",
            label = i18n("client")
        }
        filter.options[#filter.options + 1] = {
            value = "server",
            label = i18n("server")
        }
    elseif tag.value_type == "alert_status" then
        filter.value_type = 'array'
        filter.options = {}
        for key, info in pairs(alert_consts.alert_status) do
            if info.on_db then
                filter.options[#filter.options + 1] = {
                    value = info.alert_status_id,
                    label = i18n(info.i18n_title)
                }
            end
        end
    elseif tag.value_type == "severity" then
        filter.value_type = 'array'
        filter.options = {}
        for _, severity in pairsByField(alert_consts.get_printable_severities(), "severity_id", asc) do
            filter.options[#filter.options + 1] = {
                value = severity.severity_id,
                label = i18n(severity.i18n_title)
            }
        end
    elseif tag.value_type == "snmp_interface" then
        if ntop.isPro() then
            filter.value_type = 'array'

            if snmp_filter_options_cache then
                filter.options = snmp_filter_options_cache
            else
                filter.options = {}
                local flow_devices = {}
                local interfaces_list = {}
                local probe_ip_requested = nil
                -- Active flow devices
                if (restrict_filters and not isEmptyString(_GET["probe_ip"])) then
                    local tmp = split(_GET["probe_ip"], ";")
                    probe_ip_requested = tmp[1]
                    flow_devices = {
                        [tmp[1]] = 1
                    }
                else
                    local tmp = interface.getFlowDevices()
                    for _, dev_list in pairs(tmp) do
                        for _, exporter_info in pairs(dev_list) do
                            flow_devices[exporter_info.exporter_ip] = 1
                        end
                    end
                end

                -- SNMP devices
                local snmp_cached_dev = require "snmp_cached_dev"
                local snmp_config = require "snmp_config"
                local devices = {}
                if isEmptyString(probe_ip_requested) then
                    devices = snmp_config.get_all_configured_devices()
                else
                    devices = flow_devices
                end

                -- use pairsByKeys to impose order
                for probe_ip, _ in pairsByKeys(devices) do
                    if flow_devices[probe_ip] then
                        -- Use SNMP info, remove from flow devices list
                        flow_devices[probe_ip] = nil
                    end
                    local cached_interfaces = snmp_cached_dev:get_interfaces(probe_ip)
                    local probe_label
                    if not isEmptyString(probe_ip) then
                        probe_label = getProbeName(probe_ip)
                    end
                    if isEmptyString(probe_label) then
                        probe_label = probe_ip
                    end
                    if cached_interfaces and cached_interfaces["interfaces"] then
                        local interfaces = cached_interfaces["interfaces"]
                        for interface_id, interface_info in pairs(interfaces) do
                            local interface_name = interface_id
                            if interface_info.name then
                                interface_name = interface_info.name
                            end
                            local label = interface_name .. ' · ' .. probe_label
                            if hide_exporters_name then
                                label = interface_name
                            end

                            interfaces_list[label] = {
                                value = probe_ip .. "_" .. interface_id,
                                label = label,
                                show_only_value = probe_ip
                            }
                        end
                    end
                end

                for exporter_ip, _ in pairs(flow_devices or {}) do
                    -- Add interfaces for flow devices which are not polled by SNMP
                    local interfaces = interface.getFlowDeviceInfoByIP(exporter_ip)

                    for _, interfaces_table in pairs(interfaces or {}) do
                        for interface_id, _ in pairsByKeys(interfaces_table) do
                            local label = format_portidx_name(exporter_ip, interface_id, false, false)
                            if not hide_exporters_name then
                                label = exporter_ip .. ' · ' .. label
                            end
                            interfaces_list[tostring(label)] = {
                                value = exporter_ip .. "_" .. interface_id,
                                label = label,
                                show_only_value = exporter_ip
                            }
                        end
                    end
                end

                for _, values in pairsByKeys(interfaces_list, asc) do
                    filter.options[#filter.options + 1] = values
                end

                snmp_filter_options_cache = filter.options
            end
        end
    end
    
    if changed_ifid then
        interface.select(current_ifid)
    end

    return filter
end

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

function tag_utils.add_tag_if_valid(tags, tag_key, operators, i18n_prefix)
    if isEmptyString(_GET[tag_key]) then
        return
    end

    local get_value = _GET[tag_key]
    local list = split(get_value, ',')

    for _, item in ipairs(list) do
        local selected_operator = 'eq'

        local splitted = split(item, tag_utils.SEPARATOR)

        local realValue
        if #splitted == 2 then
            realValue = splitted[1]
            selected_operator = splitted[2]
        end

        local value = realValue
        if tag_utils.formatters[tag_key] ~= nil then
            value = tag_utils.formatters[tag_key](value)
        end

        tag = {
            realValue = realValue,
            value = value,
            label = i18n(i18n_prefix .. "." .. tag_key),
            key = tag_key,
            operators = operators,
            selectedOperator = selected_operator
        }

        table.insert(tags, tag)
    end
end

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

function tag_utils.build_bpf(filters)
    local bpf = ""

    local n = 0

    local and_tags = {}
    local or_tags = {}

    -- Build 'or' groups (same key)
    for key, _value in pairs(filters) do
        if not tag_utils.defined_tags[key] then
            goto skip_filter
        end
        local bpf_key = tag_utils.defined_tags[key].bpf_key

        if not bpf_key then
            goto skip_filter
        end

        local list = split(_value, ',')

        for _, value in ipairs(list) do
            local op = "eq" -- default
            local bpf_val = value

            -- tags has value formatted in this way: (e.g.) cli_port = 888,eq
            -- it means, search for values with port == 888
            local splitted_value = split(value, tag_utils.SEPARATOR)

            if table.len(splitted_value) == 2 then
                op = splitted_value[2]
                bpf_val = splitted_value[1]
            end

            local version = 4
            if key:ends('ip') then -- either cli_ip, srv_ip or ip (for both)
                version = isIPv6(bpf_val) and 6 or 4
            end

            if key == "l4proto" and bpf_val and not tonumber(bpf_val) then
                bpf_val = l4_proto_to_id(bpf_val)
            end

            -- Fetch the clickhouse key

            if op ~= "eq" and op ~= "neq" then
                goto continue
            end

            local cond = bpf_key .. ' ' .. bpf_val

            if op == "neq" then
                cond = 'not' .. ' ' .. cond
            end

            if op == "neq" then -- All 'neq' with the same key are in 'and'
                if and_tags[key] then
                    and_tags[key] = and_tags[key] .. " AND " .. cond
                else
                    and_tags[key] = cond
                end
            else -- All other operators with the same key are in 'or'
                if or_tags[key] then
                    or_tags[key] = or_tags[key] .. " OR " .. cond
                else
                    or_tags[key] = cond
                end
            end

            n = n + 1

            ::continue::
        end

        ::skip_filter::
    end

    if n == 0 then
        return bpf
    end

    -- Join all groups with 'and'

    -- AND groups
    for key, value in pairs(and_tags) do
        if isEmptyString(bpf) then
            bpf = "(" .. value .. ")"
        else
            bpf = bpf .. " and " .. "(" .. value .. ")"
        end
    end

    -- OR groups
    for key, value in pairs(or_tags) do
        if isEmptyString(bpf) then
            bpf = "(" .. value .. ")"
        else
            bpf = bpf .. " and " .. "(" .. value .. ")"
        end
    end

    return bpf
end

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

return tag_utils
