-- Copyright (c) Kong Inc. 2020

local cjson = require "cjson"
local buffer = require "string.buffer"
local pb = require "pb"
local grpc_tools = require "kong.tools.grpc"
local grpc_frame = grpc_tools.frame
local grpc_unframe = grpc_tools.unframe

local setmetatable = setmetatable

local ngx = ngx
local decode_base64 = ngx.decode_base64
local encode_base64 = ngx.encode_base64

local encode_json = cjson.encode
local decode_json = cjson.decode

local deco = {}
deco.__index = deco


local text_encoding_from_mime = {
  ["application/grpc-web"] = "plain",
  ["application/grpc-web-text"] = "base64",
  ["application/grpc-web+proto"] = "plain",
  ["application/grpc-web-text+proto"] = "base64",
  ["application/grpc-web+json"] = "plain",
  ["application/grpc-web-text+json"] = "base64",
  ["application/json"] = "plain",
}

local framing_form_mime = {
  ["application/grpc-web"] = "grpc",
  ["application/grpc-web-text"] = "grpc",
  ["application/grpc-web+proto"] = "grpc",
  ["application/grpc-web-text+proto"] = "grpc",
  ["application/grpc-web+json"] = "grpc",
  ["application/grpc-web-text+json"] = "grpc",
  ["application/json"] = "none",
}

local msg_encodign_from_mime = {
  ["application/grpc-web"] = "proto",
  ["application/grpc-web-text"] = "proto",
  ["application/grpc-web+proto"] = "proto",
  ["application/grpc-web-text+proto"] = "proto",
  ["application/grpc-web+json"] = "json",
  ["application/grpc-web-text+json"] = "json",
  ["application/json"] = "json",
}


-- parse, compile and load .proto file
-- returns a table mapping valid request URLs to input/output types
local _proto_info = {}
local function get_proto_info(fname)
  local info = _proto_info[fname]
  if info then
    return info
  end

  info = {}
  local grpc_tools_instance = grpc_tools.new()
  grpc_tools_instance:each_method(fname, function(parsed, srvc, mthd)
    info[("/%s.%s/%s"):format(parsed.package, srvc.name, mthd.name)] = {
      mthd.input_type,
      mthd.output_type,
    }
  end, true)

  _proto_info[fname] = info
  return info
end

-- return input and output names of the method specified by the url path
-- TODO: memoize
local function rpc_types(path, protofile)
  if not protofile then
    return nil
  end

  local info = get_proto_info(protofile)
  local types = info[path]
  if not types then
    return nil, ("Unknown path %q"):format(path)
  end

  return types[1], types[2]
end


function deco.new(mimetype, path, protofile)
  local text_encoding = text_encoding_from_mime[mimetype]
  local framing = framing_form_mime[mimetype]
  local msg_encoding = msg_encodign_from_mime[mimetype]

  local input_type, output_type
  if msg_encoding ~= "proto" then
    if not protofile then
      return nil, "transcoding requests require a .proto file defining the service"
    end

    input_type, output_type = rpc_types(path, protofile)
    if not input_type then
      return nil, output_type
    end
  end

  return setmetatable({
    mimetype = mimetype,
    text_encoding = text_encoding,
    framing = framing,
    msg_encoding = msg_encoding,
    input_type = input_type,
    output_type = output_type,
  }, deco)
end


function deco:upstream(body)
  if self.text_encoding == "base64" then
    body = decode_base64(body)
  end

  if self.msg_encoding == "json" then
    local msg = body
    if self.framing == "grpc" then
      msg = grpc_unframe(body)
    end

    body = grpc_frame(0x0, pb.encode(self.input_type, decode_json(msg)))
  end

  return body
end


function deco:downstream(chunk)
  if self.msg_encoding ~= "proto" then
    local body = (self.downstream_body or "") .. chunk

    local out = buffer.new()
    local msg, body = grpc_unframe(body)

    while msg do
      msg = encode_json(pb.decode(self.output_type, msg))
      if self.framing == "grpc" then
        msg = grpc_frame(0x0, msg)
      end

      out:put(msg)
      msg, body = grpc_unframe(body)
    end

    self.downstream_body = body
    chunk = out:get()
  end

  if self.text_encoding == "base64" then
    chunk = encode_base64(chunk)
  end

  return chunk
end


function deco:frame(ftype, msg)
  local f = grpc_frame(ftype, msg)

  if self.text_encoding == "base64" then
    f = encode_base64(f)
  end

  return f
end


return deco
