local kong_certificate = require "kong.runloop.certificate"
local client = require "kong.plugins.acme.client"
local ngx_ssl = require "ngx.ssl"
local kong_meta = require "kong.meta"

local ipairs = ipairs
local setmetatable = setmetatable

local string_sub   = string.sub
local string_gsub  = string.gsub
local string_find  = string.find
local string_lower = string.lower
local table_insert = table.insert
local table_concat = table.concat

local ngx_timer_at = ngx.timer.at
local ngx_re_match = ngx.re.match
local server_name  = ngx_ssl.server_name

local acme_challenge_path = [[^/\.well-known/acme-challenge/(.+)]]

-- cache for dummy cert kong generated (it's a table)
local default_cert_key

local ACMEHandler = {}

-- this has to be higher than auth plugins,
-- otherwise acme-challenges endpoints may be blocked by auth plugins
-- causing validation failures
ACMEHandler.VERSION = kong_meta.version
ACMEHandler.PRIORITY = 1705

local function build_domain_matcher(domains)
  local domains_plain = {}
  local domains_wildcard = {}
  local domains_wildcard_count = 0

  if domains == nil or domains == ngx.null then
    return false
  end

  for _, d in ipairs(domains) do
    if string_sub(d, 1, 1) == "*" then
      d = string_gsub(string_sub(d, 2), "%.", "\\.")
      table_insert(domains_wildcard, d)
      domains_wildcard_count = domains_wildcard_count + 1
    else
      domains_plain[d] = true
    end
  end

  local domains_pattern
  if domains_wildcard_count > 0 then
    domains_pattern = "(" .. table_concat(domains_wildcard, "|") .. ")$"
  end

  return setmetatable(domains_plain, {
    __index = function(_, k)
      if not domains_pattern then
        return false
      end
      return ngx_re_match(k, domains_pattern, "jo")
    end
  })
end

-- cache the domains_matcher. ACME is a global plugin.
local domains_matcher

-- expose it for use in api.lua
ACMEHandler.build_domain_matcher = build_domain_matcher


local CONFIG


local function renew(premature)
  if premature or not CONFIG then
    return
  end
  client.renew_certificate(CONFIG)
end


ACMEHandler.renew = renew


function ACMEHandler:init_worker()
  local worker_id = ngx.worker.id() or -1
  kong.log.info("acme renew timer started on worker ", worker_id)
  ngx.timer.every(86400, renew)
end


function ACMEHandler:configure(configs)
  CONFIG = configs and configs[1] or nil
  if CONFIG then
    domains_matcher = build_domain_matcher(CONFIG.domains)
  end
end


local function check_domains(conf, host)
  if not conf.enable_ipv4_common_name and string_find(host, "^(%d+)%.(%d+)%.(%d+)%.(%d+)$") then
    return false
  end

  if conf.allow_any_domain then
    return true
  end

  -- create the cache at first usage
  if domains_matcher == nil then
    domains_matcher = build_domain_matcher(conf.domains)
  end

  return domains_matcher and domains_matcher[host]
end

function ACMEHandler:certificate(conf)
  -- we can't check for Host header in this phase
  local host, err = server_name()
  if err then
    kong.log.warn("failed to read SNI server name: ", err)
    return
  elseif not host then
    kong.log.debug("ignoring because no SNI provided by client")
    return
  end

  host = string_lower(host)

  if not check_domains(conf, host) then
    kong.log.debug("ignoring because domain is not in allowed-list: ", host)
    return
  end

  local cert_and_key, err = kong_certificate.find_certificate(host)
  if err then
    kong.log.err("error find certificate for current request:", err)
    return
  end

  if not default_cert_key then
    -- hack: find_certificate() returns default cert and key if no sni defined
    default_cert_key = kong_certificate.find_certificate()
  end

  -- note we compare the table address, this relies on the fact that Kong doesn't
  -- copy the default cert table around
  if cert_and_key ~= default_cert_key then
    kong.log.debug("ignoring because non-default cert is served")
    return
  end

  local certkey, err = client.load_certkey_cached(conf, host)
  if err then
    kong.log.warn("can't load cert and key from storage: ", err)
    return
  end

  -- cert not found, get a new one and serve default cert for now
  if not certkey then
    if kong.configuration.role == "data_plane" and conf.storage == "kong" then
      kong.log.err("creating new certificate through proxy side with ",
                    "\"kong\" storage in Hybrid mode is not supported; ",
                    "consider create certificate using Admin API or ",
                    "use other external storages")
      return
    end

    ngx_timer_at(0, function()
      local ok, err = client.update_certificate(conf, host, nil)
      if err then
        kong.log.err("failed to update certificate for host: ", host, " err:", err)
        return
      end
      -- if not ok and err is nil, meaning the update is running by another worker
      if ok then
        err = client.store_renew_config(conf, host)
        if err then
          kong.log.err("failed to store renew config for host: ", host, " err:", err)
          return
        end
      end
    end)
    return
  end

  -- this will only be run in dbless
  kong.log.debug("set certificate for host: ", host)
  local _, err
  _, err = ngx_ssl.clear_certs()
  if err then
    kong.log.warn("failed to clear certs: ", err)
  end
  _, err = ngx_ssl.set_der_cert(certkey.cert)
  if err then
    kong.log.warn("failed to set cert: ", err)
  end
  _, err = ngx_ssl.set_der_priv_key(certkey.key)
  if err then
    kong.log.warn("failed to set key: ", err)
  end
end

-- access phase is to terminate the http-01 challenge request if necessary
function ACMEHandler:access(conf)

  local protocol = kong.client.get_protocol()

  -- http-01 challenge only sends to http port
  if protocol == 'http' then
    local host = kong.request.get_host()
    if not host then
      return
    end

    if not check_domains(conf, host) then
      -- We do not log here because it would flood the log
      return
    end

    local captures, err =
      ngx_re_match(kong.request.get_path(), acme_challenge_path, "jo")
    if err then
      kong.log(kong.WARN, "error matching acme-challenge uri: ", err)
      return
    end

    if captures then
      -- if this is just a sanity test, we always return 404 status
      if captures[1] == "x" then
        return kong.response.exit(404, "Not found\n")
      end

      -- TODO: race condition creating account?
      local err = client.create_account(conf)
      if err then
        kong.log.err("failed to create account:", err)
        return
      end

      local acme_client, err = client.new(conf)
      if err then
        kong.log.err("failed to create acme client:", err)
        return
      end

      acme_client:serve_http_challenge()
    end
    return
  end
end


return ACMEHandler
