local cjson         = require "cjson.safe"
local upload        = require "resty.upload"
local decoder       = require "kong.api.arguments_decoder"


local setmetatable  = setmetatable
local getmetatable  = getmetatable
local rawget        = rawget
local concat        = table.concat
local insert        = table.insert
local pairs         = pairs
local lower         = string.lower
local find          = string.find
local sub           = string.sub
local type          = type
local ngx           = ngx
local req           = ngx.req
local log           = ngx.log
local req_read_body = req.read_body
local get_uri_args  = req.get_uri_args
local get_body_data = req.get_body_data
local get_post_args = req.get_post_args
local json_decode   = cjson.decode
local kong          = kong


local NOTICE        = ngx.NOTICE


local arguments_mt = {}


function arguments_mt:__index(name)
  return rawget(self, "post")[name] or
         rawget(self,  "uri")[name]
end


local defaults = {
  decode        = true,
  multipart     = true,
  timeout       = 1000,
  chunk_size    = 4096,
  max_uri_args  = 1000,
  max_post_args = 1000,
  max_line_size = nil,
  max_part_size = nil,
}


defaults.__index = defaults


local function basename(path, separator)
  if not path then
    return nil
  end

  local sep = separator or "/"

  local location = 1

  local boundary = find(path, sep, 1, true)
  while boundary do
    location = boundary + 1
    boundary = find(path, sep, location, true)
  end

  if location > 1 then
    path = sub(path, location)
  end

  if not separator then
    return basename(path, "\\")
  end

  return path
end


local function find_content_type_boundary(content_type)
  if not content_type then
    return nil
  end

  local boundary, e = find(content_type, "boundary=", 21, true)
  if boundary then
    local s = find(content_type, ";", e + 1, true)

    if s then
      boundary = sub(content_type, e + 1, s - 1)

    else
      boundary = sub(content_type, e + 1)
    end

    if (sub(boundary, 1, 1) == '"' and sub(boundary, -1)  == '"') or
       (sub(boundary, 1, 1) == "'" and sub(boundary, -1)  == "'") then
      boundary = sub(boundary, 2, -2)
    end
  end

  if boundary ~= "" then
    return boundary
  end

  return nil
end


local function combine_arg(to, arg)
  if type(arg) ~= "table" or getmetatable(arg) == decoder.multipart_mt then
    insert(to, #to + 1, arg)

  else
    for k, v in pairs(arg) do
      local t = to[k]

      if not t then
        to[k] = v

      else
        if type(t) == "table" and getmetatable(t) ~= decoder.multipart_mt then
          combine_arg(t, v)

        else
          to[k] = { t }
          combine_arg(to[k], v)
        end
      end
    end
  end
end


local function combine(args)
  local to = {}

  if type(args) ~= "table" then
    return to
  end

  for _, arg in ipairs(args) do
    combine_arg(to, arg)
  end

  return to
end


local infer


local function infer_value(value, field)
  if not value or type(field) ~= "table" then
    return value
  end

  if value == "" or value == ngx.null then
    return ngx.null
  end

  if field.type == "number" or field.type == "integer" then
    return tonumber(value) or value

  elseif field.type == "boolean" then
    if value == "true" then
      return true

    elseif value == "false" then
      return false
    end

  elseif field.type == "array" or field.type == "set" then
    if type(value) ~= "table" then
      value = { value }
    end

    for i, item in ipairs(value) do
      value[i] = infer_value(item, field.elements)
    end

  elseif field.type == "foreign" then
    if type(value) == "table" then
      return infer(value, field.schema)
    end

  elseif field.type == "map" then
    if type(value) == "table" then
      for k, v in pairs(value) do
        value[k] = infer_value(v, field.values)
      end
    end

  elseif field.type == "record" and not field.abstract then
    if type(value) == "table" then
      for k, v in pairs(value) do
        for i in ipairs(field.fields) do
          local item = field.fields[i]
          if item then
            local key = next(item)
            local fld = item[key]
            if k == key then
              value[k] = infer_value(v, fld)
            end
          end
        end

        if field.shorthand_fields then
          for _, item in ipairs(field.shorthand_fields) do
            local key = next(item)
            local fld = item[key]
            if k == key then
              value[k] = infer_value(v, fld)
            end
          end
        end
      end
    end
  end

  return value
end


infer = function(args, schema)
  if not args then
    return
  end

  if not schema then
    return args
  end

  for field_name, field in schema:each_field(args) do
    local value = args[field_name]
    if value then
      args[field_name] = infer_value(value, field)
    end
  end

  if schema.shorthand_fields then
    for _, v in ipairs(schema.shorthand_fields) do
      local field_name = next(v)
      local field = v[field_name]

      local value = args[field_name]
      if value then
        args[field_name] = infer_value(value, field)
      end
    end
  end

  if schema.ttl == true and args.ttl then
    args.ttl = tonumber(args.ttl) or args.ttl
  end

  return args
end


local function parse_multipart_header(header, results)
  local name
  local value

  local boundary = find(header, "=", 1, true)
  if boundary then
    name  = sub(header, 2, boundary - 1)
    value = sub(header, boundary + 2, -2)

    if (sub(value, 1, 1) == '"' and sub(value, -1)  == '"') or
       (sub(value, 1, 1) == "'" and sub(value, -1)  == "'") then
      value = sub(value, 2, -2)
    end

    if sub(name, -1) == "*" and lower(sub(value, 1, 7)) == "utf-8''" then
      name = sub(name, 1, -2)
      value = sub(value, 8)

      results[name] = value

    else
      if not results[name] then
        results[name] = value
      end
    end

  else
    results[#results + 1] = header
  end
end


local function parse_multipart_headers(headers)
  if not headers then
    return nil
  end

  local results  = {}
  local location = 1

  local boundary = find(headers, ";", 1, true)
  while boundary do
    local header = sub(headers, location, boundary - 1)
    parse_multipart_header(header, results)
    location = boundary + 1
    boundary = find(headers, ";", location, true)
  end

  local header = sub(headers, location)
  if header ~= "" then
    parse_multipart_header(header, results)
  end

  return results
end


local function parse_multipart_stream(options, boundary)
  local part_args = {}

  local max_part_size = options.max_part_size
  local max_post_args = options.max_post_args
  local chunk_size    = options.chunk_size

  local multipart, err = upload:new(chunk_size, options.max_line_size)
  if not multipart then
    return nil, err
  end

  multipart:set_timeout(options.timeout)

  local parts_count = 0

  local headers, headers_count, part

  while true do
    local chunk_type, chunk

    chunk_type, chunk, err = multipart:read()
    if not chunk_type then
      return nil, err
    end

    if chunk_type == "header" then
      if not headers then
        headers = {}
        headers_count = 0
      end

      if type(chunk) == "table" then
        headers_count = headers_count + 1
        headers[headers_count] = chunk[3]

        local key, value = chunk[1], parse_multipart_headers(chunk[2])
        if value then
          headers[key] = value
        end
      end

    elseif chunk_type == "body" then
      if headers then
        local content_disposition = headers["Content-Disposition"] or {}
        local content_type        = headers["Content-Type"]

        if type(content_type) == "table" then
          content_type = content_type[1]
        end

        part = setmetatable({
          boundary = boundary,
          headers  = headers,
          name     = content_disposition.name,
          type     = content_type,
          file     = basename(content_disposition.filename),
          size     = 0,
          data     = {
            n      = 0,
          }
        }, decoder.multipart_mt)

        headers = nil
      end

      if part then
        part.size = #chunk + part.size

        if max_part_size then
          if max_part_size < part.size then
            return nil, "maximum size of multipart parameter exceeded"
          end
        end

        if max_post_args and max_post_args < parts_count + 1 then
          return nil, "maximum number of multipart parameters exceeded"
        end

        local n      = part.data.n + 1
        part.data[n] = chunk
        part.data.n  = n
      end

    elseif chunk_type == "part_end" then
      if part then
        if max_post_args then
          parts_count = parts_count + 1
        end

        if part.data.n > 0 then
          part.data = concat(part.data, nil, 1, part.data.n)

        else
          part.data = nil
        end

        if part.type and sub(part.type, 1, 16) == "application/json" then
          local json
          json, err = json_decode(part.data)
          if json then
            part.json = json

          else
            log(NOTICE, err)
          end
        end

        local part_name = part.name
        if part_name then
          local enclosure = part_args[part_name]
          if enclosure then
            if type(enclosure) == "table" and getmetatable(enclosure) ~= decoder.multipart_mt then
              enclosure[#enclosure + 1] = part

            else
              enclosure = { enclosure, part }
            end

            part_args[part_name] = enclosure

          else
            part_args[part_name] = part
          end

        else
          part_args[#part_args + 1] = part
        end

        part = nil
      end

    elseif chunk_type == "eof" then
      break
    end
  end

  multipart:read()

  return part_args
end


local function parse_multipart(options, content_type)
  local boundary

  if content_type then
    boundary = find_content_type_boundary(content_type)
  end

  return parse_multipart_stream(options, boundary)
end


-- decodes and infers the arguments
-- the name "decode" is kept for backwards compatibility
local function decode(args, schema)
  local decoded = decoder.decode(args)
  return infer(combine(decoded), schema)
end


local function load(opts)
  local options = setmetatable(opts or {}, defaults)

  local args  = setmetatable({
    uri  = {},
    post = {},
  }, arguments_mt)

  local uargs, err = get_uri_args(options.max_uri_args)
  if err == "truncated" then
    return kong.response.exit(400, { message = "Too many arguments" })
  end

  if options.decode then
    args.uri = decode(uargs, options.schema)

  else
    args.uri = uargs
  end

  local content_type = ngx.var.content_type
  if not content_type then
    return args
  end

  local content_type_lower = lower(content_type)

  if find(content_type_lower, "application/x-www-form-urlencoded", 1, true) == 1 then
    req_read_body()
    local pargs, err = get_post_args(options.max_post_args)
    if err == "truncated" then
      return kong.response.exit(400, { message = "Too many arguments" })
    end
    if pargs then
      if options.decode then
        args.post = decode(pargs, options.schema)

      else
        args.post = pargs
      end

    elseif err then
      log(NOTICE, err)
    end

  elseif find(content_type_lower, "application/json", 1, true) == 1 then
    req_read_body()

    -- we don't support file i/o in case the body is
    -- buffered to a file, and that is how we want it.
    local body_data = get_body_data()
    if body_data then
      local pargs, err = json_decode(body_data)
      if pargs then
        args.post = pargs

      elseif err then
        log(NOTICE, err)
      end
    end

  elseif options.multipart and find(content_type_lower, "multipart/form-data", 1, true) == 1 then
    if options.request and options.request.params_post then
      local pargs = {}
      for k, v in pairs(options.request.params_post) do
        if type(v) == "table" and v.name and v.content then
          pargs[k] = v.content
        else
          pargs[k] = v
        end
      end

      args.post = decode(pargs, options.schema)

    else
      local pargs, err = parse_multipart(options, content_type)
      if pargs then
        if options.decode then
          args.post = decode(pargs, options.schema)

        else
          args.post = pargs
        end

      elseif err then
        log(NOTICE, err)
      end
    end

  else
    req_read_body()

    -- we don't support file i/o in case the body is
    -- buffered to a file, and that is how we want it.
    local body_data = get_body_data()

    if body_data then
      args.body = body_data
    end
  end

  return args
end


return {
  load        = load,
  infer_value = infer_value,
  decode      = decode,
  _infer      = infer,
  _combine    = combine,
}
