--
-- Licensed to the Apache Software Foundation (ASF) under one or more
-- contributor license agreements.  See the NOTICE file distributed with
-- this work for additional information regarding copyright ownership.
-- The ASF licenses this file to You under the Apache License, Version 2.0
-- (the "License"); you may not use this file except in compliance with
-- the License.  You may obtain a copy of the License at
--
--     http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--

--- @module Routing
-- Used to dynamically handle nginx routing based on an object containing implementation details

local cjson = require "cjson"
if (cjson.decode_array_with_array_mt ~= nil) then
  cjson.decode_array_with_array_mt(true)
end
local url = require "url"
local utils = require "lib/utils"
local request = require "lib/request"
-- load policies
local security = require "policies/security"
local mapping = require "policies/mapping"
local rateLimit = require "policies/rateLimit"
local backendRouting = require "policies/backendRouting"
local cors = require "cors"
local OPTIMIZE = os.getenv("OPTIMIZE")

if OPTIMIZE ~= nil then
  OPTIMIZE = tonumber(OPTIMIZE)
else
  OPTIMIZE = 0
end

local SNAPSHOTTING = os.getenv('SNAPSHOTTING')

local _M = {}

local function setRequestLogs()
  local requestHeaders = ngx.req.get_headers()
  for k in pairs(requestHeaders) do
    if k == 'authorization' or k == ngx.ctx.clientSecretName then
      requestHeaders[k] = '[redacted]'
    end
  end
  ngx.var.requestHeaders = cjson.encode(requestHeaders)
  ngx.req.read_body()
  ngx.var.requestBody = ngx.req.get_body_data()
end

--- Function to read the list of policies and send implementation to the correct backend
-- @param red redis client instance
-- @param obj List of policies containing a type and value field. This function reads the type field and routes it appropriately.
-- @param apiKey optional subscription api key
local function parsePolicies(dataStore, obj, apiKey)
  for _, v in pairs (obj) do
    if v.type == 'reqMapping' then
      mapping.processMap(v.value)
    elseif v.type == 'rateLimit' then
      rateLimit.limit(dataStore, v.value, apiKey)
    elseif v.type == 'backendRouting' then
      backendRouting.setDynamicRoute(v.value)
    end
  end
end

--- Given a verb, transforms the backend request to use that method
-- @param v Verb to set on the backend request
local function setVerb(v)
  local allowedVerbs = {'GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'}
  local verb = string.upper(v)
  if utils.tableContains(allowedVerbs, verb) then
    ngx.req.set_method(ngx[utils.concatStrings({"HTTP_", verb})])
  else
    ngx.req.set_method(ngx.HTTP_GET)
  end
end

--- Main function that handles parsing of invocation details and carries out implementation
function _M.processCall(dataStore)
  -- Get request headers
  local requestHeaders = ngx.req.get_headers()
  -- Get resource object from redis
  local tenantId = ngx.var.tenant

  if SNAPSHOTTING == 'true' then
    dataStore:setSnapshotId(tenantId)
  end
  local gatewayPath = ngx.var.gatewayPath
  local _ , j = ngx.var.request_uri:find("/api/([^/]+)")
  ngx.var.analyticsUri = ngx.var.request_uri:sub(j+1)
  if ngx.req.get_headers()["x-debug-mode"] == "true" then
    setRequestLogs()
  end
  local redisKey = _M.findResource(dataStore, tenantId, gatewayPath)
  if redisKey == nil then
    request.err(404, 'Not found.')
  end
  local resource = dataStore:getResource(redisKey, "resources")
  if resource == nil then
    request.err(404, 'Snapshot not found.')
  end
  local obj = cjson.decode(resource)

  cors.processCall(obj)
  ngx.var.tenantNamespace = obj.tenantNamespace
  ngx.var.tenantInstance = obj.tenantInstance
  ngx.var.apiId = obj.apiId
  for verb, opFields in pairs(obj.operations) do
    if string.upper(verb) == ngx.req.get_method() then
      -- Check if auth is required
      local key
      if (opFields.security) then
        for _, sec in ipairs(opFields.security) do
          local result = security.process(dataStore, sec)
          if key == nil and sec.type ~= "oauth2" then
            key = result -- use key from either apiKey or clientSecret security policy
          end
        end
      end
      -- Set backend method
      if opFields.backendMethod ~= nil then
        setVerb(opFields.backendMethod)
      end
      -- Set backend upstream and uri
      backendRouting.setRoute(opFields.backendUrl, gatewayPath)
      -- Set gateway url as request header
      local requestScheme = requestHeaders["X-Forwarded-Proto"]
      if requestScheme == nil or requestScheme == "" then
        requestScheme = ngx.var.scheme
      end
      local requestHost = requestHeaders["X-Forwarded-Host"]
      if requestHost == nil or requestHost == "" then
        requestHost = ngx.var.host
      end
      local requestUrl = utils.concatStrings({requestScheme, "://", requestHost})
      local prefix = requestHeaders["X-Forwarded-Prefix"]
      if prefix ~= nil and prefix ~= "" then
        requestUrl = utils.concatStrings({requestUrl, prefix})
      end
      local requestUri = requestHeaders["X-Forwarded-Uri"]
      if requestUri == nil or requestUri == "" then
        requestUri = ngx.var.request_uri
      end
      ngx.req.set_header("X-Forwarded-Url", utils.concatStrings({requestUrl, requestUri}))
      -- Parse policies
      if opFields.policies ~= nil then
        parsePolicies(dataStore, opFields.policies, key)
      end
      -- Log updated request headers/body info to access logs
      if requestHeaders["x-debug-mode"] == "true" then
        setRequestLogs()
      end
      dataStore:close()
      return nil
    end
  end
  request.err(404, 'Whoops. Verb not supported.')
end

--- Find the correct redis key based on the path that's passed in
-- @param dataStore the datastore object
-- @param tenant tenantId
-- @param path path to look for
function _M.findResource(dataStore, tenant, path)
  -- Check for exact match
  local redisKey = utils.concatStrings({"resources:", tenant, ":", path})
  local cfRedisKey
  local cfUrl = ngx.req.get_headers()["x-cf-forwarded-url"]
  if cfUrl ~= nil and cfUrl ~= "" then
    local u = url.parse(cfUrl)
    cfRedisKey = utils.concatStrings({"resources:", tenant, ":", path, u.path})
    ngx.var.analyticsUri = (u.path == "") and "/" or u.path
    if next(u.query) ~= nil then
      ngx.var.analyticsUri = utils.concatStrings({ngx.var.analyticsUri, '?', u.query})
    end
  end

  local result
  if OPTIMIZE > 0 then
    result = dataStore:optimizedLookup(tenant, path)
  end
  if result ~= nil then
    ngx.var.gatewayPath = result:gsub(utils.concatStrings({'resources:', tenant, ':'}), '')
    return result
  end

  local resourceKeys = dataStore:getAllResources(tenant)
  result = _M.slowLookup(resourceKeys, tenant, path, redisKey, cfRedisKey)

  if OPTIMIZE > 0 and result ~= nil then
    dataStore:optimizeLookup(tenant, result, path)
  end

  return result
end

--- Perform a linear lookup of the api based on the apis in a tenant
-- @param resourceKeys all of the resources under a given tenant
-- @param tenant the tenant we are looking up for
-- @param path the path used to call the api gateway
-- @param redisKey a guess for a redis key based on the tenant id and path
-- @param cfRedisKey a redis key that will exist if cloud foundry routing logic is used
function _M.slowLookup(resourceKeys, tenant, path, redisKey, cfRedisKey)
  for _, key in pairs(resourceKeys) do
    if key == redisKey or key == cfRedisKey then
      local res = {string.match(key, "([^:]+):([^:]+):([^:]+)")}
      ngx.var.gatewayPath = res[3]
      return key
    end
  end
  local cfUrl = ngx.req.get_headers()["x-cf-forwarded-url"]
  if cfUrl ~= nil and cfUrl ~= "" then
    return nil
  end
  -- Construct a table of redisKeys based on number of slashes in the path
  local keyTable = {}
  for _, key in pairs(resourceKeys) do
    local _, count = string.gsub(key, "/", "")
    -- handle cases where resource path is "/"
    if count == 1 and string.sub(key, -1) == "/" then
      count = count - 1
    end
    count = tostring(count)
    if keyTable[count] == nil then
      keyTable[count] = {}
    end
    table.insert(keyTable[count], key)
  end
  -- Check for proxy or path parameter matching
  local _, count = string.gsub(redisKey, "/", "")
  for i = count, 0, -1 do
    local countString = tostring(i)
    if keyTable[countString] ~= nil then
      for _, key in pairs(keyTable[countString]) do
        -- Check for exact match or path parameter match
        if key == redisKey or key == utils.concatStrings({redisKey, "/"}) or _M.pathParamMatch(key, redisKey) == true then
          local res = {string.match(key, "([^:]+):([^:]+):([^:]+)")}
          ngx.var.gatewayPath = res[3]
          return key
        end
      end
    end
    -- substring redisKey upto last "/"
    local index = redisKey:match("^.*()/")
    if index == nil then
      return nil
    end
    redisKey = string.sub(redisKey, 1, index - 1)
  end
  return nil

end

--- Check redis if resourceKey matches path parameters
-- @param key key that may have path parameter variables
-- @param resourceKey redis resourceKey to check if it matches path parameter
function _M.pathParamMatch(key, resourceKey)
  local pathParamVars = {}
  for w in string.gmatch(key, "({%w+})") do
    w = string.sub(w, 2, string.len(w) - 1)
    pathParamVars[#pathParamVars + 1] = w
  end
  if next(pathParamVars) ~= nil then
    local pathPattern, count = string.gsub(key, "%{(%w*)%}", "([^:]+)")
    pathPattern = string.gsub(pathPattern, "%-", "%%-")
    local obj = {string.match(resourceKey, pathPattern)}
    if (#obj == count) then
      for i, v in pairs(obj) do
        ngx.ctx[pathParamVars[i]] = v
      end
      return true
    end
  end
  return false
end

function _M.setResponseLogs()
  ngx.var.responseHeaders = cjson.encode(ngx.resp.get_headers())
  local resp_body = ngx.arg[1]
  ngx.ctx.buffered = (ngx.ctx.buffered or '') .. resp_body
  if ngx.arg[2] then
    ngx.var.responseBody = ngx.ctx.buffered
  end
end

return _M
