local lrucache   = require "resty.lrucache"
local url        = require "socket.url"
local kong_meta = require "kong.meta"


local kong     = kong
local re_find  = ngx.re.find
local find     = string.find
local concat   = table.concat
local tostring = tostring
local ipairs   = ipairs


local HTTP_OK = 200


local CorsHandler = {}


CorsHandler.PRIORITY = 2000
CorsHandler.VERSION = kong_meta.version


-- per-plugin cache of normalized origins for runtime comparison
local mt_cache = { __mode = "k" }
local config_cache = setmetatable({}, mt_cache)


-- per-worker cache of parsed requests origins with 1000 slots
local normalized_req_domains = lrucache.new(10e3)


local function is_origin_provided(origin)
  return origin and origin ~= ""
end


local function normalize_origin(domain)
  local parsed_obj = assert(url.parse(domain))
  if not parsed_obj.host then
    return {
      domain = domain,
      host = domain,
    }
  end

  local port = parsed_obj.port
  if (parsed_obj.scheme == "http" and port == "80")
     or (parsed_obj.scheme == "https" and port == "443") then
    port = nil
  end

  return {
    domain = (parsed_obj.scheme and parsed_obj.scheme .. "://" or "") ..
              parsed_obj.host ..
              (port and ":" .. port or ""),
    host = parsed_obj.host,
  }
end

local function add_vary_header(header_filter)
  if header_filter then
    kong.response.add_header("vary", "Origin")
  else
    kong.response.set_header("vary", "Origin")
  end
end

local function configure_origin(conf, header_filter, req_origin)
  local n_origins = conf.origins ~= nil and #conf.origins or 0
  local set_header = kong.response.set_header

  if n_origins == 0 or (n_origins == 1 and conf.origins[1] == "*") then
    set_header("Access-Control-Allow-Origin", "*")
    return true
  end

  -- always set Vary header (it can be used for calculating cache key)
  -- https://github.com/rs/cors/issues/10
  add_vary_header(header_filter)

  if n_origins == 1 then
    -- if this doesnt look like a regex, set the ACAO header directly
    -- otherwise, we'll fall through to an iterative search and
    -- set the ACAO header based on the client Origin
    local from, _, err = re_find(conf.origins[1], "^[A-Za-z0-9.:/-]+$", "jo")
    if err then
      kong.log.err("could not inspect origin for type: ", err)
    end

    if from then
      set_header("Access-Control-Allow-Origin", conf.origins[1])
      return false
    end
  end

  if is_origin_provided(req_origin) then
    local cached_domains = config_cache[conf]
    if not cached_domains then
      cached_domains = {}

      for _, entry in ipairs(conf.origins) do
        if entry == "*" then
          set_header("Access-Control-Allow-Origin", "*")
          return true
        end

        local domain
        local maybe_regex, _, err = re_find(entry, "[^A-Za-z0-9.:/-]", "jo")
        if err then
          kong.log.err("could not inspect origin for type: ", err)
        end

        if maybe_regex then
          -- Kong 0.x did not anchor regexes:
          -- Perform adjustments to support regexes
          -- explicitly anchored by the user.
          if entry:sub(-1) ~= "$" then
            entry = entry .. "$"
          end

          if entry:sub(1, 1) == "^" then
            entry = entry:sub(2)
          end

          domain = { regex = entry }

        else
          domain = normalize_origin(entry)
        end

        domain.by_host = not find(entry, ":", 1, true)
        table.insert(cached_domains, domain)
      end

      config_cache[conf] = cached_domains
    end

    local normalized_req_origin = normalized_req_domains:get(req_origin)
    if not normalized_req_origin then
      normalized_req_origin = normalize_origin(req_origin)
      normalized_req_domains:set(req_origin, normalized_req_origin)
    end

    for _, cached_domain in ipairs(cached_domains) do
      local found, _, err

      if cached_domain.regex then
        local subject = cached_domain.by_host
                      and normalized_req_origin.host
                      or  normalized_req_origin.domain

        found, _, err = re_find(subject, cached_domain.regex, "ajo")
        if err then
          kong.log.err("could not search for domain: ", err)
        end

      else
        found = (normalized_req_origin.domain == cached_domain.domain)
      end

      if found then
        set_header("Access-Control-Allow-Origin", normalized_req_origin.domain)
        return false
      end
    end
  end

  kong.response.clear_header("Access-Control-Allow-Origin")
  return false
end


local function configure_credentials(conf, allow_all, header_filter, req_origin)
  local set_header = kong.response.set_header

  if not conf.credentials then
    return
  end

  if not allow_all then
    set_header("Access-Control-Allow-Credentials", true)
    return
  end

  -- Access-Control-Allow-Origin is '*', must change it because ACAC cannot
  -- be 'true' if ACAO is '*'.
  if is_origin_provided(req_origin) then
    add_vary_header(header_filter)
    set_header("Access-Control-Allow-Origin", req_origin)
    set_header("Access-Control-Allow-Credentials", true)
  end
end


function CorsHandler:access(conf)
  local req_origin = kong.request.get_header("Origin")
  if kong.request.get_method() ~= "OPTIONS"
     or not is_origin_provided(req_origin)
     or not kong.request.get_header("Access-Control-Request-Method")
  then
    return
  end

  -- don't add any response header because we are delegating the preflight to
  -- the upstream API (conf.preflight_continue=true), or because we already
  -- added them all
  kong.ctx.plugin.skip_response_headers = true

  if conf.preflight_continue then
    return
  end

  local allow_all = configure_origin(conf, false, req_origin)
  configure_credentials(conf, allow_all, false, req_origin)

  local set_header = kong.response.set_header

  if conf.headers and #conf.headers > 0 then
    set_header("Access-Control-Allow-Headers", concat(conf.headers, ","))

  else
    local acrh = kong.request.get_header("Access-Control-Request-Headers")
    if acrh then
      set_header("Access-Control-Allow-Headers", acrh)
    else
      kong.response.clear_header("Access-Control-Allow-Headers")
    end
  end

  local methods = conf.methods and concat(conf.methods, ",")
                  or "GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS,TRACE,CONNECT"

  set_header("Access-Control-Allow-Methods", methods)

  if conf.max_age then
    set_header("Access-Control-Max-Age", tostring(conf.max_age))
  end

  if conf.private_network and
    kong.request.get_header("Access-Control-Request-Private-Network") == 'true' then
      set_header("Access-Control-Allow-Private-Network", 'true')
  end

  return kong.response.exit(HTTP_OK)
end


function CorsHandler:header_filter(conf)
  if kong.ctx.plugin.skip_response_headers then
    return
  end

  local req_origin = kong.request.get_header("origin")
  if not is_origin_provided(req_origin) and not conf.allow_origin_absent then
    return
  end

  local allow_all = configure_origin(conf, true, req_origin)
  configure_credentials(conf, allow_all, true, req_origin)

  if conf.exposed_headers and #conf.exposed_headers > 0 then
    kong.response.set_header("Access-Control-Expose-Headers",
                             concat(conf.exposed_headers, ","))
  end
end


return CorsHandler
