local ffi = require "ffi"
local C = ffi.C
local ffi_gc = ffi.gc
local ffi_str = ffi.string

require "resty.openssl.include.pkcs12"
require "resty.openssl.include.bio"
require "resty.openssl.include.stack"
local bio_util = require "resty.openssl.auxiliary.bio"
local format_error = require("resty.openssl.err").format_error
local pkey_lib = require "resty.openssl.pkey"
local x509_lib = require "resty.openssl.x509"
local stack_lib = require "resty.openssl.stack"
local objects_lib = require "resty.openssl.objects"
local ctx_lib = require "resty.openssl.ctx"
local OPENSSL_3X = require("resty.openssl.version").OPENSSL_3X

local stack_of_x509_new = stack_lib.new_of("X509")
local stack_of_x509_add = stack_lib.add_of("X509")
local stack_of_x509_iter = stack_lib.mt_of("X509", x509_lib.dup, {}).__ipairs

local ptr_ptr_of_pkey = ffi.typeof("EVP_PKEY*[1]")
local ptr_ptr_of_x509 = ffi.typeof("X509*[1]")
local ptr_ptr_of_stack = ffi.typeof("OPENSSL_STACK*[1]")

local function decode(p12, passphrase)
  local bio = C.BIO_new_mem_buf(p12, #p12)
  if bio == nil then
    return nil, "pkcs12.decode: BIO_new_mem_buf() failed"
  end
  ffi_gc(bio, C.BIO_free)

  local p12 = C.d2i_PKCS12_bio(bio, nil)
  if p12 == nil then
    return nil, format_error("pkcs12.decode: d2i_PKCS12_bio")
  end
  ffi_gc(p12, C.PKCS12_free)

  local ppkey = ptr_ptr_of_pkey()
  local px509 = ptr_ptr_of_x509()
  local pstack = ptr_ptr_of_stack()
  local stack = stack_of_x509_new()
  if stack == nil then
    return nil, "pkcs12.decode: OPENSSL_sk_new_null() failed"
  end

  -- assign a valid OPENSSL_STACK so gc is taken care of
  pstack[0] = stack

  local code = C.PKCS12_parse(p12, passphrase or "", ppkey, px509, pstack)
  if code ~= 1 then
    return nil, format_error("pkcs12.decode: PKCS12_parse")
  end

  local cacerts
  local n = C.OPENSSL_sk_num(stack)
  if n > 0 then
    cacerts = {}
    local iter = stack_of_x509_iter({ ctx = stack })
    for i=1, n do
      local _, c = iter()
      cacerts[i] = c
    end
  end

  local friendly_name = C.X509_alias_get0(px509[0], nil)
  if friendly_name ~= nil then
    friendly_name = ffi_str(friendly_name)
  end

  return {
    key = pkey_lib.new(ppkey[0]),
    cert = x509_lib.new(px509[0]),
    friendly_name = friendly_name,
    cacerts = cacerts,
    -- store reference to the stack, so it's not GC'ed unexpectedly
    _stack = stack,
  }
end

local function encode(opts, passphrase, properties)
  if passphrase and type(passphrase) ~= "string" then
    return nil, "pkcs12.encode: expect passphrase to be a string"
  end
  local pkey = opts.key
  if not pkey_lib.istype(pkey) then
    return nil, "pkcs12.encode: expect key to be a pkey instance"
  end
  local cert = opts.cert
  if not x509_lib.istype(cert) then
    return nil, "pkcs12.encode: expect cert to be a x509 instance"
  end

  local ok, err = cert:check_private_key(pkey)
  if not ok then
    return nil, "pkcs12.encode: key doesn't match cert: " .. err
  end

  local nid_key = opts.nid_key
  if nid_key then
    nid_key, err = objects_lib.txtnid2nid(nid_key)
    if err then
      return nil, "pkcs12.encode: invalid nid_key"
    end
  end

  local nid_cert = opts.nid_cert
  if nid_cert then
    nid_cert, err = objects_lib.txtnid2nid(nid_cert)
    if err then
      return nil, "pkcs12.encode: invalid nid_cert"
    end
  end

  local x509stack
  local cacerts = opts.cacerts
  if cacerts then
    if type(cacerts) ~= "table" then
      return nil, "pkcs12.encode: expect cacerts to be a table"
    end
    if #cacerts > 0 then
      -- stack lib handles gc
      x509stack = stack_of_x509_new()
      for _, c in ipairs(cacerts) do
        if C.X509_up_ref(c.ctx) ~= 1 then
          return nil, "pkcs12.encode: failed to add cacerts: X509_up_ref failed"
        end
        local ok, err = stack_of_x509_add(x509stack, c.ctx)
        if not ok then
          return nil, "pkcs12.encode: failed to add cacerts: " .. err
        end
      end
    end
  end

  local p12
  if OPENSSL_3X then
    p12 = C.PKCS12_create_ex(passphrase or "", opts.friendly_name,
                              pkey.ctx, cert.ctx, x509stack,
                              nid_key or 0, nid_cert or 0,
                              opts.iter or 0, opts.mac_iter or 0, 0,
                              ctx_lib.get_libctx(), properties)
  else
    p12 = C.PKCS12_create(passphrase or "", opts.friendly_name,
                            pkey.ctx, cert.ctx, x509stack,
                            nid_key or 0, nid_cert or 0,
                            opts.iter or 0, opts.mac_iter or 0, 0)
  end
  if p12 == nil then
    return nil, format_error("pkcs12.encode: PKCS12_create")
  end
  ffi_gc(p12, C.PKCS12_free)

  return bio_util.read_wrap(C.i2d_PKCS12_bio, p12)
end

return {
  decode = decode,
  loads = decode,
  encode = encode,
  dumps = encode,
}
