local ffi = require "ffi"
local C = ffi.C
local ffi_str = ffi.string
local ffi_cast = ffi.cast

require "resty.openssl.include.ssl"

local nginx_aux = require("resty.openssl.auxiliary.nginx")
local x509_lib = require("resty.openssl.x509")
local chain_lib = require("resty.openssl.x509.chain")
local stack_lib = require("resty.openssl.stack")
local OPENSSL_3X = require("resty.openssl.version").OPENSSL_3X
local format_error = require("resty.openssl.err").format_error

local _M = {
  SSL_VERIFY_NONE                 = 0x00,
  SSL_VERIFY_PEER                 = 0x01,
  SSL_VERIFY_FAIL_IF_NO_PEER_CERT = 0x02,
  SSL_VERIFY_CLIENT_ONCE          = 0x04,
  SSL_VERIFY_POST_HANDSHAKE       = 0x08,
}

local ops = {
  SSL_OP_NO_EXTENDED_MASTER_SECRET                = 0x00000001,
  SSL_OP_CLEANSE_PLAINTEXT                        = 0x00000002,
  SSL_OP_LEGACY_SERVER_CONNECT                    = 0x00000004,
  SSL_OP_TLSEXT_PADDING                           = 0x00000010,
  SSL_OP_SAFARI_ECDHE_ECDSA_BUG                   = 0x00000040,
  SSL_OP_IGNORE_UNEXPECTED_EOF                    = 0x00000080,
  SSL_OP_DISABLE_TLSEXT_CA_NAMES                  = 0x00000200,
  SSL_OP_ALLOW_NO_DHE_KEX                         = 0x00000400,
  SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS              = 0x00000800,
  SSL_OP_NO_QUERY_MTU                             = 0x00001000,
  SSL_OP_COOKIE_EXCHANGE                          = 0x00002000,
  SSL_OP_NO_TICKET                                = 0x00004000,
  SSL_OP_CISCO_ANYCONNECT                         = 0x00008000,
  SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION   = 0x00010000,
  SSL_OP_NO_COMPRESSION                           = 0x00020000,
  SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION        = 0x00040000,
  SSL_OP_NO_ENCRYPT_THEN_MAC                      = 0x00080000,
  SSL_OP_ENABLE_MIDDLEBOX_COMPAT                  = 0x00100000,
  SSL_OP_PRIORITIZE_CHACHA                        = 0x00200000,
  SSL_OP_CIPHER_SERVER_PREFERENCE                 = 0x00400000,
  SSL_OP_TLS_ROLLBACK_BUG                         = 0x00800000,
  SSL_OP_NO_ANTI_REPLAY                           = 0x01000000,
  SSL_OP_NO_SSLv3                                 = 0x02000000,
  SSL_OP_NO_TLSv1                                 = 0x04000000,
  SSL_OP_NO_TLSv1_2                               = 0x08000000,
  SSL_OP_NO_TLSv1_1                               = 0x10000000,
  SSL_OP_NO_TLSv1_3                               = 0x20000000,
  SSL_OP_NO_DTLSv1                                = 0x04000000,
  SSL_OP_NO_DTLSv1_2                              = 0x08000000,
  SSL_OP_NO_RENEGOTIATION                         = 0x40000000,
  SSL_OP_CRYPTOPRO_TLSEXT_BUG                     = 0x80000000,
}
ops.SSL_OP_NO_SSL_MASK = ops.SSL_OP_NO_SSLv3 + ops.SSL_OP_NO_TLSv1 + ops.SSL_OP_NO_TLSv1_1
                          + ops.SSL_OP_NO_TLSv1_2 + ops.SSL_OP_NO_TLSv1_3
ops.SSL_OP_NO_DTLS_MASK = ops.SSL_OP_NO_DTLSv1 + ops.SSL_OP_NO_DTLSv1_2
for k, v in pairs(ops) do
  _M[k] = v
end

local mt = {__index = _M}

local ssl_ptr_ct = ffi.typeof('SSL*')

local stack_of_ssl_cipher_iter = function(ctx)
  return stack_lib.mt_of("SSL_CIPHER", function(x) return x end, {}, true).__ipairs({ctx = ctx})
end

function _M.from_request()
  -- don't GC this
  local ctx, err = nginx_aux.get_req_ssl()
  if err ~= nil then
    return nil, err
  end

  return setmetatable({
    ctx = ctx,
    -- the cdata is not manage by Lua, don't GC on Lua side
    _managed = false,
    -- this is the client SSL session
    _server = true,
  }, mt)
end

function _M.from_socket(socket)
  if not socket then
    return nil, "expect a ngx.socket.tcp instance at #1"
  end
  -- don't GC this
  local ctx, err = nginx_aux.get_socket_ssl(socket)
  if err ~= nil then
    return nil, err
  end

  return setmetatable({
    ctx = ctx,
    -- the cdata is not manage by Lua, don't GC on Lua side
    _managed = false,
    -- this is the client SSL session
    _server = false,
  }, mt)
end

function _M.istype(l)
  return l and l.ctx and ffi.istype(ssl_ptr_ct, l.ctx)
end

function _M:get_peer_certificate()
  local x509
  if OPENSSL_3X then
    x509 = C.SSL_get1_peer_certificate(self.ctx)
  else
    x509 = C.SSL_get_peer_certificate(self.ctx)
  end

  if x509 == nil then
    return nil
  end
  ffi.gc(x509, C.X509_free)

  local err
  -- always copy, although the ref counter of returned x509 is
  -- already increased by one.
  x509, err = x509_lib.dup(x509)
  if err then
    return nil, err
  end

  return x509
end

function _M:get_peer_cert_chain()
  local stack = C.SSL_get_peer_cert_chain(self.ctx)

  if stack == nil then
    return nil
  end

  return chain_lib.dup(stack)
end

-- TLSv1.3
function _M:set_ciphersuites(ciphers)
  if C.SSL_set_ciphersuites(self.ctx, ciphers) ~= 1 then
    return false, format_error("ssl:set_ciphers: SSL_set_ciphersuites")
  end

  return true
end

-- TLSv1.2 and lower
function _M:set_cipher_list(ciphers)
  if C.SSL_set_cipher_list(self.ctx, ciphers) ~= 1 then
    return false, format_error("ssl:set_ciphers: SSL_set_cipher_list")
  end

  return true
end

function _M:get_ciphers()
  local ciphers = C.SSL_get_ciphers(self.ctx)

  if ciphers == nil then
    return nil
  end

  local ret = {}

  for i, cipher in stack_of_ssl_cipher_iter(ciphers) do
    cipher = C.SSL_CIPHER_get_name(cipher)
    if cipher == nil then
      return nil, format_error("ssl:get_ciphers: SSL_CIPHER_get_name")
    end
    ret[i] = ffi_str(cipher)
  end

  return table.concat(ret, ":")
end

function _M:get_cipher_name()
  local cipher = C.SSL_get_current_cipher(self.ctx)

  if cipher == nil then
    return nil
  end

  cipher = C.SSL_CIPHER_get_name(cipher)
  if cipher == nil then
    return nil, format_error("ssl:get_cipher_name: SSL_CIPHER_get_name")
  end
  return ffi_str(cipher)
end

function _M:set_timeout(tm)
  local session = C.SSL_get_session(self.ctx)

  if session == nil then
    return false, format_error("ssl:set_timeout: SSL_get_session")
  end

  if C.SSL_SESSION_set_timeout(session, tm) ~= 1 then
    return false, format_error("ssl:set_timeout: SSL_SESSION_set_timeout")
  end
  return true
end

function _M:get_timeout()
  local session = C.SSL_get_session(self.ctx)

  if session == nil then
    return false, format_error("ssl:get_timeout: SSL_get_session")
  end

  return tonumber(C.SSL_SESSION_get_timeout(session))
end

local ssl_verify_default_cb = ffi_cast("verify_callback", function()
  return 1
end)

function _M:set_verify(mode, cb)
  if self._verify_cb then
    self._verify_cb:free()
  end

  if cb then
    cb = ffi_cast("verify_callback", cb)
    self._verify_cb = cb
  end

  C.SSL_set_verify(self.ctx, mode, cb or ssl_verify_default_cb)

  return true
end

function _M:free_verify_cb()
  if self._verify_cb then
    self._verify_cb:free()
    self._verify_cb = nil
  end
end

function _M:add_client_ca(x509)
  if not self._server then
    return false, "ssl:add_client_ca is only supported on server side"
  end

  if not x509_lib.istype(x509) then
    return false, "expect a x509 instance at #1"
  end

  if C.SSL_add_client_CA(self.ctx, x509.ctx) ~= 1 then
    return false, format_error("ssl:add_client_ca: SSL_add_client_CA")
  end

  return true
end

function _M:set_options(...)
  local bitmask = 0
  for _, opt in ipairs({...}) do
    bitmask = bit.bor(bitmask, opt)
  end

  bitmask = C.SSL_set_options(self.ctx, bitmask)

  return tonumber(bitmask)
end

function _M:get_options(readable)
  local bitmask = C.SSL_get_options(self.ctx)

  if not readable then
    return tonumber(bitmask)
  end

  local ret = {}
  for k, v in pairs(ops) do
    if bit.band(v, bitmask) > 0 then
      table.insert(ret, k)
    end
  end
  table.sort(ret)

  return ret
end

function _M:clear_options(...)
  local bitmask = 0
  for _, opt in ipairs({...}) do
    bitmask = bit.bor(bitmask, opt)
  end

  bitmask = C.SSL_clear_options(self.ctx, bitmask)

  return tonumber(bitmask)
end

local valid_protocols = {
  ["SSLv3"] = ops.SSL_OP_NO_SSLv3,
  ["TLSv1"] = ops.SSL_OP_NO_TLSv1,
  ["TLSv1.1"] = ops.SSL_OP_NO_TLSv1_1,
  ["TLSv1.2"] = ops.SSL_OP_NO_TLSv1_2,
  ["TLSv1.3"] = ops.SSL_OP_NO_TLSv1_3,
}
local any_tlsv1 = ops.SSL_OP_NO_TLSv1_1 + ops.SSL_OP_NO_TLSv1_2 + ops.SSL_OP_NO_TLSv1_3

function _M:set_protocols(...)
  local bitmask = 0
  for _, prot in ipairs({...}) do
    local b = valid_protocols[prot]
    if not b then
      return nil, "\"" .. prot .. "\" is not a valid protocol"
    end
    bitmask = bit.bor(bitmask, b)
  end

  if bit.band(bitmask, any_tlsv1) > 0 then
    bitmask = bit.bor(bitmask, ops.SSL_OP_NO_TLSv1)
  end

  -- first disable all protocols
  C.SSL_set_options(self.ctx, ops.SSL_OP_NO_SSL_MASK)
  
  -- then enable selected protocols
  return tonumber(C.SSL_clear_options(self.ctx, bitmask))
end

return _M