local ipmatcher     = require "resty.ipmatcher"
local lrucache      = require "resty.lrucache"
local isempty       = require "table.isempty"
local clone         = require "table.clone"
local clear         = require "table.clear"
local bit           = require "bit"
local utils         = require "kong.router.utils"


local setmetatable  = setmetatable
local is_http       = ngx.config.subsystem == "http"
local get_method    = ngx.req.get_method
local get_headers   = ngx.req.get_headers
local re_match      = ngx.re.match
local re_find       = ngx.re.find
local header        = ngx.header
local var           = ngx.var
local ngx_log       = ngx.log
local ngx_ERR       = ngx.ERR
local worker_id     = ngx.worker.id
local concat        = table.concat
local sort          = table.sort
local byte          = string.byte
local upper         = string.upper
local lower         = string.lower
local find          = string.find
local format        = string.format
local sub           = string.sub
local tonumber      = tonumber
local pairs         = pairs
local ipairs        = ipairs
local error         = error
local type          = type
local max           = math.max
local band          = bit.band
local bor           = bit.bor
local yield         = require("kong.tools.yield").yield
local server_name   = require("ngx.ssl").server_name


local sanitize_uri_postfix = utils.sanitize_uri_postfix
local check_select_params  = utils.check_select_params
local strip_uri_args       = utils.strip_uri_args
local get_service_info     = utils.get_service_info
local add_debug_headers    = utils.add_debug_headers
local get_upstream_uri_v0  = utils.get_upstream_uri_v0
local route_match_stat     = utils.route_match_stat


-- limits regex degenerate times to the low miliseconds
local REGEX_PREFIX  = "(*LIMIT_MATCH=10000)"
local SLASH         = byte("/")
local DOT           = byte(".")

local ERR           = ngx.ERR
local WARN          = ngx.WARN


local APPENDED = {}


local function append(destination, value)
  local n = destination[0] + 1
  destination[0] = n
  destination[n] = value
end


local log
do
  log = function(lvl, ...)
    ngx_log(lvl, "[router] ", ...)
  end
end


local get_header
if is_http then
  get_header = require("kong.tools.http").get_header
end


local split_port
do
  local ZERO, NINE, LEFTBRACKET, RIGHTBRACKET = ("09[]"):byte(1, -1)


  local function safe_add_port(host, port)
    if not port then
      return host
    end

    return host .. ":" .. port
  end


  local function onlydigits(s, begin)
    for i = begin or 1, #s do
      local c = byte(s, i)
      if c < ZERO or c > NINE then
        return false
      end
    end
    return true
  end


  --- Splits an optional ':port' section from a hostname
  -- the port section must be decimal digits only.
  -- brackets ('[]') are peeled off the hostname if present.
  -- if there's more than one colon and no brackets, no split is possible.
  -- on non-parseable input, returns name unchanged,
  -- every string input produces at least one string output.
  -- @tparam string name the string to split.
  -- @tparam number default_port default port number
  -- @treturn string hostname without port
  -- @treturn string hostname with port
  -- @treturn boolean true if input had a port number
  local function l_split_port(name, default_port)
    if byte(name, 1) == LEFTBRACKET then
      if byte(name, -1) == RIGHTBRACKET then
        return sub(name, 2, -2), safe_add_port(name, default_port), false
      end

      local splitpos = find(name, "]:", 2, true)
      if splitpos then
        if splitpos == #name - 1 then
          return sub(name, 2, splitpos - 1), name .. (default_port or ""), false
        end

        if onlydigits(name, splitpos + 2) then
          return sub(name, 2, splitpos - 1), name, true
        end
      end

      return name, safe_add_port(name, default_port), false
    end

    local firstcolon = find(name, ":", 1, true)
    if not firstcolon then
      return name, safe_add_port(name, default_port), false
    end

    if firstcolon == #name then
      local host = sub(name, 1, firstcolon - 1)
      return host, safe_add_port(host, default_port), false
    end

    if not onlydigits(name, firstcolon + 1) then
      if default_port then
        return name, format("[%s]:%s", name, default_port), false
      end

      return name, name, false
    end

    return sub(name, 1, firstcolon - 1), name, true
  end


  -- split_port is a pure function, so we can memoize it.
  local memo_h = setmetatable({}, { __mode = "k" })
  local memo_hp = setmetatable({}, { __mode = "k" })
  local memo_p = setmetatable({}, { __mode = "k" })


  split_port = function(name, default_port)
    local k = name .. "#" .. (default_port or "")
    local h, hp, p = memo_h[k], memo_hp[k], memo_p[k]
    if not h then
      h, hp, p = l_split_port(name, default_port)
      memo_h[k], memo_hp[k], memo_p[k] = h, hp, p
    end

    return h, hp, p
  end
end


local DEFAULT_MATCH_LRUCACHE_SIZE = utils.DEFAULT_MATCH_LRUCACHE_SIZE


local MATCH_RULES = {
  HOST            = 0x00000040,
  HEADER          = 0x00000020,
  URI             = 0x00000010,
  METHOD          = 0x00000008,
  SNI             = 0x00000004,
  SRC             = 0x00000002,
  DST             = 0x00000001,
}


local SORTED_MATCH_RULES = is_http and {
  MATCH_RULES.HOST,
  MATCH_RULES.HEADER,
  MATCH_RULES.URI,
  MATCH_RULES.METHOD,
  MATCH_RULES.SNI,
  [0] = 5,
} or {
  MATCH_RULES.SNI,
  MATCH_RULES.SRC,
  MATCH_RULES.DST,
  [0] = 3,
}


local MATCH_SUBRULES = {
  HAS_REGEX_URI          = 0x01,
  PLAIN_HOSTS_ONLY       = 0x02,
  HAS_WILDCARD_HOST_PORT = 0x04,
}


local EMPTY_T = require("kong.tools.table").EMPTY


local match_route
local reduce
local lua_regex_cache_max_entries


local function _set_ngx(mock_ngx)
  if type(mock_ngx) ~= "table" then
    return
  end

  if mock_ngx.header then
    header = mock_ngx.header
  end

  if mock_ngx.var then
    var = mock_ngx.var
  end

  if mock_ngx.log then
    ngx_log = mock_ngx.log
  end

  if mock_ngx.ERR then
    ERR = mock_ngx.ERR
  end

  if type(mock_ngx.req) == "table" then
    if mock_ngx.req.get_method then
      get_method = mock_ngx.req.get_method
    end

    if mock_ngx.req.get_headers then
      get_headers = mock_ngx.req.get_headers
    end
  end

  if type(mock_ngx.config) == "table" then
    if mock_ngx.config.subsystem then
      is_http = mock_ngx.config.subsystem == "http"
    end
  end

  if type(mock_ngx.re) == "table" then
    if mock_ngx.re.match then
      re_match = mock_ngx.re.match
    end

    if mock_ngx.re.find then
      re_find = mock_ngx.re.find
    end
  end

  get_header = function(key)
    local mock_headers = mock_ngx.headers or {}
    local mock_var = mock_ngx.var or {}
    return mock_headers[key] or mock_var["http_" .. key]
  end
end


local function create_range_f(ip)
  if ip and find(ip, "/", nil, true) then
    local matcher = ipmatcher.new({ ip })
    return function(ip) return matcher:match(ip) end
  end
end


local function marshall_route(r)
  local route        = r.route
  local hosts        = route.hosts
  local headers      = route.headers
  local paths        = route.paths
  local methods      = route.methods
  local snis         = route.snis
  local sources      = route.sources
  local destinations = route.destinations

  if not (hosts or headers or methods or paths or snis or sources or destinations)
  then
    return nil, "could not categorize route"
  end

  local match_rules     = 0x00
  local match_weight    = 0
  local submatch_weight = 0
  local max_uri_length  = 0
  local hosts_t         = { [0] = 0 }
  local headers_t       = { [0] = 0 }
  local uris_t          = { [0] = 0 }
  local methods_t       = {}
  local sources_t       = { [0] = 0 }
  local destinations_t  = { [0] = 0 }
  local snis_t          = {}


  -- hosts


  if hosts then
    if type(hosts) ~= "table" then
      return nil, "hosts field must be a table"
    end

    local has_host_wildcard
    local has_host_plain
    local has_wildcard_host_port

    for i = 1, #hosts do
      local host = hosts[i]
      if type(host) ~= "string" then
        return nil, "hosts values must be strings"
      end

      if find(host, "*", nil, true) then
        -- wildcard host matching
        has_host_wildcard = true

        local wildcard_host_regex = host:gsub("%.", "\\.")
                                        :gsub("%*", ".+") .. "$"

        local _, _, has_port = split_port(host)
        if not has_port then
          wildcard_host_regex = wildcard_host_regex:gsub("%$$", [[(?::\d+)?$]])
        end

        if has_wildcard_host_port == nil and has_port then
          has_wildcard_host_port = true
        end

        append(hosts_t, {
          wildcard = true,
          value    = host,
          regex    = wildcard_host_regex,
        })

      else
        -- plain host matching
        has_host_plain = true
        append(hosts_t, { value = host })
        hosts_t[host] = host
      end
    end

    if has_host_plain or has_host_wildcard then
      match_rules = bor(match_rules, MATCH_RULES.HOST)
      match_weight = match_weight + 1
    end

    if not has_host_wildcard then
      submatch_weight = bor(submatch_weight, MATCH_SUBRULES.PLAIN_HOSTS_ONLY)
    end

    if has_wildcard_host_port then
      submatch_weight = bor(submatch_weight, MATCH_SUBRULES.HAS_WILDCARD_HOST_PORT)
    end
  end


  -- headers


  if headers then
    if type(headers) ~= "table" then
      return nil, "headers field must be a table"
    end

    for header_name, header_values in pairs(headers) do
      if type(header_values) ~= "table" then
        return nil, "header values must be a table for header '" ..
                    header_name .. "'"
      end

      header_name = lower(header_name)

      if header_name ~= "host" then
        local header_values_map = {}
        local header_values_count = #header_values
        for i = 1, header_values_count do
          header_values_map[lower(header_values[i])] = true
        end
        local header_pattern
        if header_values_count == 1 then
          local first_header = header_values[1]
          if sub(first_header, 1, 2) == "~*" then
            header_pattern = sub(first_header, 3)
          end
        end

        append(headers_t, {
          name = header_name,
          values_map = header_values_map,
          header_pattern = header_pattern,
        })
      end
    end

    if headers_t[0] > 0 then
      match_rules = bor(match_rules, MATCH_RULES.HEADER)
      match_weight = match_weight + 1
    end
  end


  -- paths


  if paths then
    if type(paths) ~= "table" then
      return nil, "paths field must be a table"
    end

    local count = #paths
    if count > 0 then
      match_rules = bor(match_rules, MATCH_RULES.URI)
      match_weight = match_weight + 1
      for i = 1, count do
        local path = paths[i]
        local is_regex = sub(path, 1, 1) == "~"

        if not is_regex then
          -- plain URI or URI prefix

          local uri_t = {
            is_prefix = true,
            value     = path,
          }

          append(uris_t, uri_t)
          uris_t[path] = uri_t
          max_uri_length = max(max_uri_length, #path)

        else

          path = sub(path, 2)
          -- regex URI
          local strip_regex  = REGEX_PREFIX .. path .. [[(?<uri_postfix>.*)]]

          local uri_t    = {
            is_regex     = true,
            value        = path,
            regex        = path,
            strip_regex  = strip_regex,
          }

          append(uris_t, uri_t)
          uris_t[path] = uri_t
          submatch_weight = bor(submatch_weight, MATCH_SUBRULES.HAS_REGEX_URI)
        end
      end
    end
  end


  -- methods


  if methods then
    if type(methods) ~= "table" then
      return nil, "methods field must be a table"
    end

    local count = #methods
    if count > 0 then
      match_rules = bor(match_rules, MATCH_RULES.METHOD)
      match_weight = match_weight + 1

      for i = 1, count do
        methods_t[upper(methods[i])] = true
      end
    end
  end


  -- snis

  if snis then
    if type(snis) ~= "table" then
      return nil, "snis field must be a table"
    end

    local count = #snis
    if count > 0 then
      match_rules = bor(match_rules, MATCH_RULES.SNI)
      match_weight = match_weight + 1

      for i = 1, count do
        local sni = snis[i]
        if type(sni) ~= "string" then
          return nil, "sni elements must be strings"
        end

        if #sni > 1 and byte(sni, -1) == DOT then
          -- last dot in FQDNs must not be used for routing
          sni = sub(sni, 1, -2)
        end

        snis_t[sni] = sni
      end
    end
  end


  -- sources


  if sources then
    if type(sources) ~= "table" then
      return nil, "sources field must be a table"
    end

    local count = #sources
    if count > 0 then
      match_rules = bor(match_rules, MATCH_RULES.SRC)
      match_weight = match_weight + 1

      for i = 1, count do
        local source = sources[i]
        if type(source) ~= "table" then
          return nil, "sources elements must be tables"
        end

        append(sources_t, {
          ip = source.ip,
          port = source.port,
          range_f = create_range_f(source.ip),
        })
      end
    end
  end


  -- destinations


  if destinations then
    if type(destinations) ~= "table" then
      return nil, "destinations field must be a table"
    end

    local count = #destinations
    if count > 0 then
      match_rules = bor(match_rules, MATCH_RULES.DST)
      match_weight = match_weight + 1

      for i = 1, count do
        local destination = destinations[i]
        if type(destination) ~= "table" then
          return nil, "destinations elements must be tables"
        end

        append(destinations_t, {
          ip = destination.ip,
          port = destination.port,
          range_f = create_range_f(destination.ip),
        })
      end
    end
  end


  -- upstream_url parsing


  local service = r.service

  local service_protocol, service_type,
        service_host, service_port,
        service_hostname_type, service_path = get_service_info(service)


  return {
    type            = service_type,
    route           = route,
    service         = service,
    strip_uri       = route.strip_path    == true,
    preserve_host   = route.preserve_host == true,
    match_rules     = match_rules,
    match_weight    = match_weight,
    submatch_weight = submatch_weight,
    max_uri_length  = max_uri_length,
    hosts           = hosts_t,
    headers         = headers_t,
    uris            = uris_t,
    methods         = methods_t,
    sources         = sources_t,
    destinations    = destinations_t,
    snis            = snis_t,
    upstream_url_t  = {
      scheme = service_protocol,
      type = service_hostname_type,
      host = service_host,
      port = service_port,
      path = service_path,
    },
  }
end


local function index_src_dst(source, indexes, funcs)
  for i = 1, source[0] do
    local src_dst_t = source[i]
    if src_dst_t.ip then
      indexes[src_dst_t.ip] = true

      if src_dst_t.range_f then
        append(funcs, src_dst_t.range_f)
      end
    end

    if src_dst_t.port then
      indexes[src_dst_t.port] = true
    end
  end
end


local function index_route_t(route_t, plain_indexes, prefix_uris, regex_uris,
                             wildcard_hosts, src_trust_funcs, dst_trust_funcs)
  for i = 1, route_t.hosts[0] do
    local host_t = route_t.hosts[i]
    if host_t.wildcard then
      append(wildcard_hosts, host_t)

    else
      plain_indexes.hosts[host_t.value] = true
    end
  end

  local headers = plain_indexes.headers
  for i = 1, route_t.headers[0] do
    local header_t = route_t.headers[i]
    if not headers[header_t.name] then
      headers[header_t.name] = true
      append(headers, header_t.name)
    end
  end

  for i = 1, route_t.uris[0] do
    local uri_t = route_t.uris[i]
    if uri_t.is_prefix then
      plain_indexes.uris[uri_t.value] = true
      append(prefix_uris, uri_t)

    else
      append(regex_uris, uri_t)
    end
  end

  for method in pairs(route_t.methods) do
    plain_indexes.methods[method] = true
  end

  for sni in pairs(route_t.snis) do
    plain_indexes.snis[sni] = true
  end

  index_src_dst(route_t.sources, plain_indexes.sources, src_trust_funcs)
  index_src_dst(route_t.destinations, plain_indexes.destinations, dst_trust_funcs)
end


local function sort_routes(r1, r2)
  if r1.submatch_weight ~= r2.submatch_weight then
    return r1.submatch_weight > r2.submatch_weight
  end

  if r1.headers[0] ~= r2.headers[0] then
    return r1.headers[0] > r2.headers[0]
  end

  -- only regex path use regex_priority
  if band(r1.submatch_weight, MATCH_SUBRULES.HAS_REGEX_URI) ~= 0 then
    do
      local rp1 = r1.route.regex_priority or 0
      local rp2 = r2.route.regex_priority or 0

      if rp1 ~= rp2 then
        return rp1 > rp2
      end
    end
  end

  if r1.max_uri_length ~= r2.max_uri_length then
    return r1.max_uri_length > r2.max_uri_length
  end

  if r1.route.created_at ~= nil and r2.route.created_at ~= nil then
    return r1.route.created_at < r2.route.created_at
  end
end


local function sort_categories(c1, c2)
  if c1.match_weight ~= c2.match_weight then
    return c1.match_weight > c2.match_weight
  end

  return c1.category_bit > c2.category_bit
end


local function sort_uris(p1, p2)
  return #p1.value > #p2.value
end


local function sort_sources(r1, r2)
  local sources_r1 = r1.sources
  local sources_r2 = r2.sources

  if sources_r1 == sources_r2 then
    return false
  end

  local ip_port_r1 = 0
  for i = 1, sources_r1[0] do
    if sources_r1[i].ip and sources_r1[i].port then
      ip_port_r1 = 1
      break
    end
  end

  local ip_port_r2 = 0
  for i = 1, sources_r2[0] do
    if sources_r2[i].ip and sources_r2[i].port then
      ip_port_r2 = 1
      break
    end
  end

  return ip_port_r1 > ip_port_r2
end


local function sort_destinations(r1, r2)
  local destinations_r1 = r1.destinations
  local destinations_r2 = r2.destinations

  if destinations_r1 == destinations_r2 then
    return false
  end

  local ip_port_r1 = 0
  for i = 1, destinations_r1[0] do
    if destinations_r1[i].ip and destinations_r1[i].port then
      ip_port_r1 = 1
      break
    end
  end

  local ip_port_r2 = 0
  for i = 1, destinations_r2[0] do
    if destinations_r2[i].ip and destinations_r2[i].port then
      ip_port_r2 = 1
      break
    end
  end

  return ip_port_r1 > ip_port_r2
end


local function sort_src_dst(source, func)
  if not isempty(source) then
    for _, routes in pairs(source) do
      sort(routes, func)
    end
  end
end


local function categorize_hosts_headers_uris(route_t, source, category, key)
  for i = 1, source[0] do
    local value = source[i][key or "value"]
    if category[value] then
      append(category[value], route_t)

    else
      category[value] = { [0] = 1, route_t }
    end
  end
end


local function categorize_methods_snis(route_t, source, category)
  for key in pairs(source) do
    if category[key] then
      append(category[key], route_t)
    else
      category[key] = { [0] = 1, route_t }
    end
  end
end


local function categorize_src_dst(route_t, source, category)
  if source[0] == 0 then
    return
  end

  for i = 1, source[0] do
    local src_dst_t = source[i]
    local ip = src_dst_t.ip
    if ip then
      if not category[ip] then
        category[ip] = { [0] = 0 }
      end

      if not APPENDED[ip] then
        append(category[ip], route_t)
        APPENDED[ip] = true
      end
    end

    local port = src_dst_t.port
    if port then
      if not category[port] then
        category[port] = { [0] = 0 }
      end

      if not APPENDED[port] then
        append(category[port], route_t)
        APPENDED[port] = true
      end
    end
  end

  clear(APPENDED)
end


local function categorize_route_t(route_t, bit_category, categories)
  local category = categories[bit_category]
  if not category then
    category                 = {
      match_weight           = route_t.match_weight,
      routes_by_hosts        = {},
      routes_by_headers      = {},
      routes_by_uris         = {},
      routes_by_methods      = {},
      routes_by_sources      = {},
      routes_by_destinations = {},
      routes_by_sni          = {},
      all                    = { [0] = 0 },
    }

    categories[bit_category] = category
  end

  append(category.all, route_t)
  categorize_hosts_headers_uris(route_t, route_t.hosts, category.routes_by_hosts)
  categorize_hosts_headers_uris(route_t, route_t.headers, category.routes_by_headers, "name")
  categorize_hosts_headers_uris(route_t, route_t.uris, category.routes_by_uris)
  categorize_methods_snis(route_t, route_t.methods, category.routes_by_methods)
  categorize_methods_snis(route_t, route_t.snis, category.routes_by_sni)
  categorize_src_dst(route_t, route_t.sources, category.routes_by_sources)
  categorize_src_dst(route_t, route_t.destinations, category.routes_by_destinations)
end


local function matcher_src_dst(source, ctx, ip_name, port_name)
  for i = 1, source[0] do
    local src_dst_t = source[i]
    local ip_ok
    if not src_dst_t.ip then
      ip_ok = true
    elseif src_dst_t.range_f then
      ip_ok = src_dst_t.range_f(ctx[ip_name])
    else
      ip_ok = src_dst_t.ip == ctx[ip_name]
    end

    if ip_ok then
      if not src_dst_t.port or (src_dst_t.port == ctx[port_name]) then
        ctx.matches[ip_name] = src_dst_t.ip
        ctx.matches[port_name] = src_dst_t.port
        return true
      end
    end
  end
end


local function match_regex_uri(uri_t, req_uri, matches)
  local m, err = re_match(req_uri, uri_t.strip_regex, "ajo")
  if err then
    return nil, err
  end

  if not m then
    return
  end

  local uri_postfix = m.uri_postfix
  if uri_postfix then
    matches.uri_prefix = sub(req_uri, 1, -(#uri_postfix + 1))

    -- remove the uri_postfix group
    m[#m] = nil
    m.uri_postfix = nil

    uri_postfix = sanitize_uri_postfix(uri_postfix)
  end

  matches.uri = uri_t.value
  matches.uri_postfix = uri_postfix

  if m[1] ~= nil then
    matches.uri_captures = m
  end

  return true
end


do
  local matchers = {
    [MATCH_RULES.HOST] = function(route_t, ctx)
      local hosts = route_t.hosts
      local req_host = ctx.hits.host or ctx.req_host
      local host = hosts[req_host] or hosts[ctx.host_no_port]
      if host then
        ctx.matches.host = host
        return true
      end

      for i = 1, hosts[0] do
        local host_t = hosts[i]
        if host_t.wildcard then
          local from, _, err = re_find(ctx.host_with_port, host_t.regex, "ajo")
          if err then
            log(ERR, "could not evaluate wildcard host regex: ", err)
            return
          end

          if from then
            ctx.matches.host = host_t.value
            return true
          end
        end
      end
    end,

    [MATCH_RULES.HEADER] = function(route_t, ctx)
      local headers = route_t.headers
      local matches_headers = {}
      ctx.matches.headers = matches_headers
      for i = 1, headers[0] do
        local found_in_req
        local header_t = headers[i]
        local req_header = ctx.req_headers[header_t.name]
        if type(req_header) == "table" then
          for j = 1, #req_header do
            local req_header_val = lower(req_header[j])
            if header_t.values_map[req_header_val] then
              found_in_req = true
              matches_headers[header_t.name] = req_header_val
              break
            end
            -- fallback to regex check if exact match failed
            if header_t.header_pattern and re_find(req_header_val, header_t.header_pattern, "jo") then
              found_in_req = true
              ctx.matches.headers[header_t.name] = req_header_val
              break
            end
          end

        elseif req_header then -- string
          req_header = lower(req_header)
          if header_t.values_map[req_header] then
            found_in_req = true
            matches_headers[header_t.name] = req_header
          end
          -- fallback to regex check if exact match failed
          if header_t.header_pattern and re_find(req_header, header_t.header_pattern, "jo") then
            found_in_req = true
            ctx.matches.headers[header_t.name] = req_header
          end
        end

        if not found_in_req then
          return
        end
      end

      return true
    end,

    [MATCH_RULES.URI] = function(route_t, ctx)
      local req_uri = ctx.req_uri
      if req_uri == "" then
        return
      end

      local matches = ctx.matches
      do
        local uri_t = route_t.uris[ctx.hits.uri or req_uri]
        if uri_t then
          if uri_t.is_regex then
            local is_match, err = match_regex_uri(uri_t, req_uri, matches)
            if is_match then
              return true
            end

            if err then
              log(ERR, "could not evaluate URI prefix/regex: ", err)
              return
            end
          end

          -- plain or prefix match from the index
          matches.uri_prefix = sub(req_uri, 1, #uri_t.value)
          matches.uri_postfix = sanitize_uri_postfix(sub(req_uri, #uri_t.value + 1))
          matches.uri = uri_t.value
          return true
        end
      end

      local uris = route_t.uris
      for i = 1, uris[0] do
        local uri_t = uris[i]
        if uri_t.is_regex then
          local is_match, err = match_regex_uri(uri_t, req_uri, matches)
          if is_match then
            return true
          end

          if err then
            log(ERR, "could not evaluate URI prefix/regex: ", err)
            return
          end

        else
          -- plain or prefix match (not from the index)
          local from, to = find(req_uri, uri_t.value, nil, true)
          if from == 1 then
            matches.uri_prefix = sub(req_uri, 1, to)
            matches.uri_postfix = sanitize_uri_postfix(sub(req_uri, to + 1))
            matches.uri = uri_t.value
            return true
          end
        end
      end
    end,

    [MATCH_RULES.METHOD] = function(route_t, ctx)
      if route_t.methods[ctx.req_method] then
        ctx.matches.method = ctx.req_method
        return true
      end
    end,

    [MATCH_RULES.SNI] = function(route_t, ctx)
      if ctx.req_scheme == "http" or route_t.snis[ctx.sni] then
        ctx.matches.sni = ctx.sni
        return true
      end
    end,

    [MATCH_RULES.SRC] = function(route_t, ctx)
      return matcher_src_dst(route_t.sources, ctx, "src_ip", "src_port")
    end,

    [MATCH_RULES.DST] = function(route_t, ctx)
      return matcher_src_dst(route_t.destinations, ctx, "dst_ip", "dst_port")
    end,
  }


  match_route = function(route_t, ctx)
    -- run cached matcher
    local match_rules = route_t.match_rules
    if type(matchers[match_rules]) == "function" then
      clear(ctx.matches)
      return matchers[match_rules](route_t, ctx)
    end

    -- build and cache matcher

    local matchers_set = { [0] = 0 }

    for _, bit_match_rule in pairs(MATCH_RULES) do
      if band(match_rules, bit_match_rule) ~= 0 then
        append(matchers_set, matchers[bit_match_rule])
      end
    end

    matchers[route_t.match_rules] = function(route_t, ctx)
      -- clear matches context for this try on this route
      clear(ctx.matches)

      for i = 1, matchers_set[0] do
        if not matchers_set[i](route_t, ctx) then
          return
        end
      end

      return true
    end

    return matchers[route_t.match_rules](route_t, ctx)
  end
end


do
  local reducers = {
    [MATCH_RULES.HOST] = function(category, ctx)
      return category.routes_by_hosts[ctx.hits.host or ctx.req_host]
    end,

    [MATCH_RULES.HEADER] = function(category, ctx)
      return category.routes_by_headers[ctx.hits.header_name]
    end,

    [MATCH_RULES.URI] = function(category, ctx)
      -- no ctx.req_uri indexing since regex URIs have a higher priority than
      -- plain URIs
      return category.routes_by_uris[ctx.hits.uri]
    end,

    [MATCH_RULES.METHOD] = function(category, ctx)
      return category.routes_by_methods[ctx.req_method]
    end,

    [MATCH_RULES.SNI] = function(category, ctx)
      return category.routes_by_sni[ctx.sni]
    end,

    [MATCH_RULES.SRC] = function(category, ctx)
      return category.routes_by_sources[ctx.src_ip]
          or category.routes_by_sources[ctx.src_port]
    end,

    [MATCH_RULES.DST] = function(category, ctx)
      return category.routes_by_destinations[ctx.dst_ip]
          or category.routes_by_destinations[ctx.dst_port]
    end,
  }

  local build_cached_reducer = function(bit_category)
    local reducers_count = 0
    local reducers_set = {}
    local header_rule = 0

    for i = 1, SORTED_MATCH_RULES[0] do
      local bit_match_rule = SORTED_MATCH_RULES[i]
      if band(bit_category, bit_match_rule) ~= 0 then
        reducers_count = reducers_count + 1
        reducers_set[reducers_count] = reducers[bit_match_rule]
        if bit_match_rule == MATCH_RULES.HEADER then
          header_rule = reducers_count
        end
      end
    end

    return function(category, ctx)
      local min_len = 0
      local smallest_set

      for i = 1, reducers_count do
        local candidates = reducers_set[i](category, ctx)
        if candidates ~= nil then
          if i == header_rule then
            return candidates
          end
          local candidates_len = #candidates
          if not smallest_set or candidates_len < min_len then
            min_len = candidates_len
            smallest_set = candidates
          end
        end
      end

      return smallest_set
    end
  end

  reduce = function(category, bit_category, ctx)
    if type(reducers[bit_category]) ~= "function" then
      -- build and cache reducer
      reducers[bit_category] = build_cached_reducer(bit_category)
    end

    -- run cached reducer
    return reducers[bit_category](category, ctx), category.all
  end
end


local function match_src_dst(source, ip, port, funcs)
  if source[ip] or source[port] then
    return true

  elseif funcs[0] > 0 then
    for i = 1, funcs[0] do
      if funcs[i](ip) then
        return true
      end
    end
  end
end


local function match_candidates(candidates, ctx)
  for i = 1, #candidates do
    if match_route(candidates[i], ctx) then
      return candidates[i]
    end
  end
end


local function find_match(ctx)
  -- iterate from the highest matching to the lowest category to
  -- find our route
  local category_idx = ctx.categories_lookup[ctx.req_category] or 1
  while category_idx <= ctx.categories_weight_sorted[0] do
    local matched_route

    local bit_category = ctx.categories_weight_sorted[category_idx].category_bit
    local category     = ctx.categories[bit_category]

    if category then
      local reduced_candidates, category_candidates = reduce(category,
                                                             bit_category,
                                                             ctx)
      if reduced_candidates then
        -- check against a reduced set of routes that is a strong candidate
        -- for this request, instead of iterating over all the routes of
        -- this category
        matched_route = match_candidates(reduced_candidates, ctx)
      end

      if not matched_route then
        -- no result from the reduced set, must check for results from the
        -- full list of routes from that category before checking a lower
        -- category
        matched_route = match_candidates(category_candidates, ctx)
      end

      if matched_route then
        local upstream_host
        local upstream_uri
        local upstream_url_t = matched_route.upstream_url_t

        if matched_route.route.id and ctx.routes_by_id[matched_route.route.id].route then
          matched_route.route = ctx.routes_by_id[matched_route.route.id].route
        end

        local matches = ctx.matches

        -- Path construction

        local request_prefix

        if matched_route.type == "http" then
          request_prefix = matched_route.strip_uri and matches.uri_prefix or nil

          -- if we do not have a path-match, then the postfix is simply the
          -- incoming path, without the initial slash
          local req_uri = ctx.req_uri
          local request_postfix = matches.uri_postfix or sub(req_uri, 2, -1)
          local upstream_base = upstream_url_t.path or "/"

          if matched_route.route.path_handling == "v1" then
            if matched_route.strip_uri then
              -- we drop the matched part, replacing it with the upstream path
              if byte(upstream_base, -1) == SLASH and
                 byte(request_postfix, 1) == SLASH then
                -- double "/", so drop the first
                upstream_uri = sub(upstream_base, 1, -2) .. request_postfix

              else
                upstream_uri = upstream_base .. request_postfix
              end

            else
              -- we retain the incoming path, just prefix it with the upstream
              -- path, but skip the initial slash
              upstream_uri = upstream_base .. sub(req_uri, 2, -1)
            end

          else -- matched_route.route.path_handling == "v0"
            upstream_uri = get_upstream_uri_v0(matched_route, request_postfix, req_uri,
                                               upstream_base)
          end

          -- preserve_host header logic

          if matched_route.preserve_host then
            upstream_host = ctx.raw_req_host
          end
        end

        if matched_route.preserve_host and upstream_host == nil then
          upstream_host = ctx.sni
        end

        return {
          route           = matched_route.route,
          service         = matched_route.service,
          headers         = matched_route.headers,
          upstream_url_t  = upstream_url_t,
          upstream_scheme = upstream_url_t.scheme,
          upstream_uri    = upstream_uri,
          upstream_host   = upstream_host,
          prefix          = request_prefix,
          matches         = {
            uri_captures  = matches.uri_captures,
            uri           = matches.uri,
            host          = matches.host,
            headers       = matches.headers,
            method        = matches.method,
            src_ip        = matches.src_ip,
            src_port      = matches.src_port,
            dst_ip        = matches.dst_ip,
            dst_port      = matches.dst_port,
            sni           = matches.sni,
          }
        }
      end
    end

    -- check lower category
    category_idx = category_idx + 1
  end
end


local _M = { DEFAULT_MATCH_LRUCACHE_SIZE = DEFAULT_MATCH_LRUCACHE_SIZE }


-- for unit-testing purposes only
_M._set_ngx = _set_ngx
_M.split_port = split_port


function _M.new(routes, cache, cache_neg)
  if type(routes) ~= "table" then
    return error("expected arg #1 routes to be a table")
  end


  -- hash table for fast lookup of plain properties
  -- incoming requests/connections
  local plain_indexes = {
    hosts             = {},
    headers           = { [0] = 0 },
    uris              = {},
    methods           = {},
    sources           = {},
    destinations      = {},
    snis              = {},
  }


  -- when hash lookup in plain_indexes fails, those are arrays
  -- of regexes for `uris` as prefixes and `hosts` as wildcards
  -- or IP ranges comparison functions
  local prefix_uris     = { [0] = 0 } -- will be sorted by length
  local regex_uris      = { [0] = 0 }
  local wildcard_hosts  = { [0] = 0 }
  local src_trust_funcs = { [0] = 0 }
  local dst_trust_funcs = { [0] = 0 }


  -- all routes grouped by the category they belong to, to reduce
  -- iterations over sets of routes per request
  local categories = {}

  -- all routes indexed by id
  local routes_by_id = {}

  if not cache then
    cache = lrucache.new(DEFAULT_MATCH_LRUCACHE_SIZE)
  end

  if not cache_neg then
    cache_neg = lrucache.new(DEFAULT_MATCH_LRUCACHE_SIZE)
  end

  -- index routes

  do
    local marshalled_routes = { [0] = 0 }

    for i = 1, #routes do
      yield(true)

      local route = routes[i]
      local r = routes[i].route
      if r.expression then
        ngx_log(ngx_ERR, "expecting a traditional route while expression is given. ",
                    "Likely it's a misconfiguration. Please check router_flavor")
      end

      if r.id ~= nil then
        routes_by_id[r.id] = route
      end

      local paths = r.paths
      local count = paths and #paths or 0
      if count > 1 then
        -- split routes by paths to sort properly
        for j = 1, count do
          r.paths = { paths[j] }
          local route_t, err = marshall_route(route)
          if not route_t then
            return nil, err
          end

          append(marshalled_routes, route_t)
        end

        r.paths = paths

      else
        local route_t, err = marshall_route(route)
        if not route_t then
          return nil, err
        end

        append(marshalled_routes, route_t)
      end
    end

    -- sort wildcard hosts and uri regexes since those rules
    -- don't have their own matching category
    --
    -- * plain hosts > wildcard hosts
    -- * more plain headers > less plain headers
    -- * regex uris > plain uris
    -- * longer plain URIs > shorter plain URIs

    sort(marshalled_routes, sort_routes)

    for i = 1, marshalled_routes[0] do
      yield(true)

      local route_t = marshalled_routes[i]
      categorize_route_t(route_t, route_t.match_rules, categories)
      index_route_t(route_t, plain_indexes, prefix_uris, regex_uris,
                    wildcard_hosts, src_trust_funcs, dst_trust_funcs)
    end
  end


  -- a sorted array of all categories bits (from the most significant
  -- matching-wise, to the least significant)
  local categories_weight_sorted = { [0] = 0 }


  -- a lookup array to get the category_idx from a category_bit. The
  -- idx will be a categories_weight_sorted index
  local categories_lookup = {}


  for category_bit, category in pairs(categories) do
    append(categories_weight_sorted, {
      category_bit = category_bit,
      match_weight = category.match_weight,
    })
  end

  sort(categories_weight_sorted, sort_categories)

  for i = 1, categories_weight_sorted[0] do
    categories_lookup[categories_weight_sorted[i].category_bit] = i
  end

  yield()

  sort(prefix_uris, sort_uris)

  if not isempty(categories) then
    for _, category in pairs(categories) do
      yield()

      sort_src_dst(category.routes_by_sources, sort_sources)
      sort_src_dst(category.routes_by_destinations, sort_destinations)
    end
  end


  local hits = {}
  local matches = {}
  local ctx = {
    hits = hits,
    matches = matches,
    categories = categories,
    categories_lookup = categories_lookup,
    categories_weight_sorted = categories_weight_sorted,
    routes_by_id = routes_by_id,
  }

  local match_headers        = plain_indexes.headers[0] > 0
  local match_prefix_uris    = prefix_uris[0] > 0
  local match_regex_uris     = regex_uris[0] > 0
  local match_hosts          = not isempty(plain_indexes.hosts)
  local match_wildcard_hosts = not isempty(wildcard_hosts)
  local match_uris           = not isempty(plain_indexes.uris)
  local match_methods        = not isempty(plain_indexes.methods)
  local match_snis           = not isempty(plain_indexes.snis)
  local match_sources        = not isempty(plain_indexes.sources)
  local match_destinations   = not isempty(plain_indexes.destinations)

  -- warning about the regex cache size being too small
  if not lua_regex_cache_max_entries then
    lua_regex_cache_max_entries = tonumber(kong.configuration.nginx_http_lua_regex_cache_max_entries) or 1024
  end

  if worker_id() == 0 and regex_uris[0] * 2 > lua_regex_cache_max_entries then
    ngx_log(WARN, "the 'nginx_http_lua_regex_cache_max_entries' setting is set to ",
                  lua_regex_cache_max_entries,
                  " but there are ", regex_uris[0], " regex paths configured. ",
                  "This may lead to performance issue due to regex cache trashing. ",
                  "Consider increasing the 'nginx_http_lua_regex_cache_max_entries' ",
                  "to at least ", regex_uris[0] * 2)
  end

  local function find_route(req_method, req_uri, req_host, req_scheme,
                            src_ip, src_port,
                            dst_ip, dst_port,
                            sni, req_headers)

    check_select_params(req_method, req_uri, req_host, req_scheme,
                        src_ip, src_port,
                        dst_ip, dst_port,
                        sni, req_headers)

    -- input sanitization for matchers

    local raw_req_host = req_host

    req_method = req_method or ""
    req_uri = req_uri or ""
    req_host = req_host or ""
    req_headers = req_headers or EMPTY_T
    src_ip = src_ip or ""
    src_port = src_port or ""
    dst_ip = dst_ip or ""
    dst_port = dst_port or ""
    sni = sni or ""

    local req_category = 0x00

    clear(hits)

    -- router, router, which of these routes is the fairest?
    --
    -- determine which category this request *might* be targeting

    -- header match

    local headers_key do
      local headers_count = 0
      if match_headers then
        for i = 1, plain_indexes.headers[0] do
          local name = plain_indexes.headers[i]
          local value = req_headers[name]
          if value then
            if type(value) == "table" then
              value = clone(value)
              for i, v in ipairs(value) do
                value[i] = v:lower()
              end
              sort(value)
              value = concat(value, ", ")

            else
              value = lower(value)
            end

            if headers_count == 0 then
              headers_key = { "|", name, "=", value }

            else
              headers_key[headers_count + 1] = "|"
              headers_key[headers_count + 2] = name
              headers_key[headers_count + 3] = "="
              headers_key[headers_count + 4] = value
            end

            headers_count = headers_count + 4

            if not hits.header_name then
              hits.header_name = name
              req_category = bor(req_category, MATCH_RULES.HEADER)
            end
          end
        end
      end
      headers_key = headers_key and concat(headers_key, nil, 1, headers_count) or ""
    end

    -- cache lookup

    local cache_key = req_method .. "|" .. req_uri .. "|" .. req_host
                                 .. "|" .. src_ip  .. "|" .. src_port
                                 .. "|" .. dst_ip  .. "|" .. dst_port
                                 .. "|" .. sni .. headers_key
    local match_t = cache:get(cache_key)
    if match_t then
      route_match_stat(ctx, "pos")

      return match_t
    end

    if cache_neg:get(cache_key) then
      route_match_stat(ctx, "neg")

      return nil
    end

    -- host match

    -- req_host might have port or maybe not, host_no_port definitely doesn't
    -- if there wasn't a port, req_port is assumed to be the default port
    -- according the protocol scheme
    local host_no_port, host_with_port
    if raw_req_host then
      host_no_port, host_with_port = split_port(req_host, req_scheme == "https" and 443 or 80)
      if match_hosts and (plain_indexes.hosts[host_with_port] or
                          plain_indexes.hosts[host_no_port])
      then
        req_category = bor(req_category, MATCH_RULES.HOST)

      elseif match_wildcard_hosts then
        for i = 1, wildcard_hosts[0] do
          local host = wildcard_hosts[i]
          local from, _, err = re_find(host_with_port, host.regex, "ajo")
          if err then
            log(ERR, "could not match wildcard host: ", err)
            return
          end

          if from then
            hits.host    = host.value
            req_category = bor(req_category, MATCH_RULES.HOST)
            break
          end
        end
      end
    end

    -- uri match

    if match_regex_uris then
      for i = 1, regex_uris[0] do
        local from, _, err = re_find(req_uri, regex_uris[i].regex, "ajo")
        if err then
          log(ERR, "could not evaluate URI regex: ", err)
          return
        end

        if from then
          hits.uri     = regex_uris[i].value
          req_category = bor(req_category, MATCH_RULES.URI)
          break
        end
      end
    end

    if match_uris and not hits.uri then
      if plain_indexes.uris[req_uri] then
        hits.uri     = req_uri
        req_category = bor(req_category, MATCH_RULES.URI)

      elseif match_prefix_uris then
        for i = 1, prefix_uris[0] do
          if find(req_uri, prefix_uris[i].value, nil, true) == 1 then
            hits.uri     = prefix_uris[i].value
            req_category = bor(req_category, MATCH_RULES.URI)
            break
          end
        end
      end
    end

    -- method match

    if match_methods and plain_indexes.methods[req_method] then
      req_category = bor(req_category, MATCH_RULES.METHOD)
    end

    -- sni match

    if match_snis and plain_indexes.snis[sni] then
      req_category = bor(req_category, MATCH_RULES.SNI)
    end

    -- src match

    if match_sources and match_src_dst(plain_indexes.sources, src_ip, src_port, src_trust_funcs) then
      req_category = bor(req_category, MATCH_RULES.SRC)
    end

    -- dst match

    if match_destinations and match_src_dst(plain_indexes.destinations, dst_ip, dst_port, dst_trust_funcs) then
      req_category = bor(req_category, MATCH_RULES.DST)
    end

    --print("highest potential category: ", req_category)

    if req_category ~= 0x00 then
      ctx.req_category             = req_category
      ctx.raw_req_host             = raw_req_host
      ctx.req_method               = req_method
      ctx.req_uri                  = req_uri
      ctx.req_host                 = req_host
      ctx.req_scheme               = req_scheme
      ctx.req_headers              = req_headers
      ctx.src_ip                   = src_ip
      ctx.src_port                 = src_port
      ctx.dst_ip                   = dst_ip
      ctx.dst_port                 = dst_port
      ctx.sni                      = sni
      ctx.host_with_port           = host_with_port
      ctx.host_no_port             = host_no_port

      local match_t = find_match(ctx)
      if match_t then
        cache:set(cache_key, match_t)
        return match_t
      end
    end

    -- no match :'(
    cache_neg:set(cache_key, true)
  end

  local exec
  if is_http then
    exec = function(ctx)
      local req_method = get_method()
      local req_uri = ctx and ctx.request_uri or var.request_uri
      local req_host = get_header("host", ctx)
      local req_scheme = ctx and ctx.scheme or var.scheme
      local sni = server_name()

      local headers
      if match_headers then
        local err
        headers, err = get_headers()
        if err == "truncated" then
          local lua_max_req_headers = kong and kong.configuration and kong.configuration.lua_max_req_headers or 100
          log(ERR, "router: not all request headers were read in order to determine the route as ",
                    "the request contains more than ", lua_max_req_headers, " headers, route selection ",
                    "may be inaccurate, consider increasing the 'lua_max_req_headers' configuration value ",
                    "(currently at ", lua_max_req_headers, ")")
        end

        headers.host = nil
      end

      req_uri = strip_uri_args(req_uri)

      local match_t = find_route(req_method, req_uri, req_host, req_scheme,
                                 nil, nil, -- src_ip, src_port
                                 nil, nil, -- dst_ip, dst_port
                                 sni, headers)
      if match_t then
        -- debug HTTP request header logic
        add_debug_headers(ctx, header, match_t)
      end

      return match_t
    end

  else -- stream
    exec = function(ctx)
      local src_ip = var.remote_addr
      local dst_ip = var.server_addr
      local src_port = tonumber(var.remote_port, 10)
      local dst_port = (ctx or ngx.ctx).host_port or tonumber(var.server_port, 10)
      -- error value for non-TLS connections ignored intentionally
      local sni = server_name()
      -- fallback to preread SNI if current connection doesn't terminate TLS
      if not sni then
        sni = var.ssl_preread_server_name
      end

      local scheme
      if var.protocol == "UDP" then
        scheme = "udp"
      else
        scheme = sni and "tls" or "tcp"
      end

      -- when proxying TLS request in second layer or doing TLS passthrough
      -- rewrite the dst_ip, port back to what specified in proxy_protocol
      if var.kong_tls_passthrough_block == "1" or var.ssl_protocol then
        dst_ip = var.proxy_protocol_server_addr
        dst_port = tonumber(var.proxy_protocol_server_port, 10)
      end

      return find_route(nil, nil, nil, scheme,
                        src_ip, src_port,
                        dst_ip, dst_port,
                        sni)
    end
  end

  return {
    _set_ngx = _set_ngx,
    select = find_route,
    exec = exec
  }
end


return _M
