local cjson = require "cjson"
local Set   = require "pl.Set"


local setmetatable = setmetatable
local tostring = tostring
local ipairs = ipairs
local table = table


local function invalidate_cache(self, old_entity, err, err_t)
  if err then
    return nil, err, err_t
  end
  if old_entity then
    self:post_crud_event("update", old_entity)
  end
end


local _SNIs = {}


-- Truthy if all the names on the list don't exist on the db or exist but are
-- associated to the given certificate
-- if the cert id is nil, all encountered snis will return an error
function _SNIs:check_list_is_new(name_list, valid_cert_id)
  for i=1, #name_list do
    local name = name_list[i]
    local row, err, err_t = self:select_by_name(name)
    if err then
      return nil, err, err_t
    end
    if row and row.certificate.id ~= valid_cert_id then
      local msg = name ..
                  " already associated with existing " ..
                  "certificate '" .. row.certificate.id .. "'"
      local err_t = self.errors:schema_violation({ snis = msg })
      return nil, tostring(err_t), err_t
    end
  end

  return true
end


-- Creates one instance of SNI for each name in name_list
-- All created instances will be associated to the given certificate
function _SNIs:insert_list(cert_pk, name_list)
  cert_pk = self.db.certificates.schema:extract_pk_values(cert_pk)
  for _, name in ipairs(name_list) do
    local _, err, err_t = self:insert({
      name         = name,
      certificate  = cert_pk,
    })
    if err then
      return nil, err, err_t
    end
  end

  return true
end


-- Deletes all SNIs on the given name list
function _SNIs:delete_list(name_list)
  local err_list = {}
  local errors_len = 0
  local first_err_t
  for i = 1, #name_list do
    local ok, err, err_t = self:delete_by_name(name_list[i])
    if not ok then
      errors_len = errors_len + 1
      err_list[errors_len] = err
      first_err_t = first_err_t or err_t
    end
  end

  if errors_len > 0 then
    return nil, table.concat(err_list, ","), first_err_t
  end

  return true
end


-- Returns the name list for a given certificate
function _SNIs:list_for_certificate(cert_pk, options)
  local name_list = setmetatable({}, cjson.array_mt)

  for sni, err, err_t in self:each_for_certificate(cert_pk, nil, options) do
    if err then
      return nil, err, err_t
    end

    table.insert(name_list, sni.name)
  end

  table.sort(name_list)

  return name_list
end


-- Replaces the names of a given certificate
-- It does not try to insert SNIs which are already inserted
-- It does not try to delete SNIs which don't exist
function _SNIs:update_list(cert_pk, new_list)
  -- Get the names currently associated to the certificate
  local current_list, err, err_t = self:list_for_certificate(cert_pk)
  if not current_list then
    return nil, err, err_t
  end

  local delete_list = Set.values(Set(current_list) - Set(new_list))
  local insert_list = Set.values(Set(new_list) - Set(current_list))

  local ok, err, err_t = self:insert_list(cert_pk, insert_list)
  if not ok then
    return nil, err, err_t
  end

  -- ignoring errors here
  -- returning 4xx here risks invalid states and is confusing to the user
  self:delete_list(delete_list)

  return true
end


-- invalidates the *old* name when updating it to a new name
function _SNIs:update(pk, entity, options)
  local _, err, err_t = invalidate_cache(self, self:select(pk))
  if err then
    return nil, err, err_t
  end

  return self.super.update(self, pk, entity, options)
end


-- invalidates the *old* name when updating it to a new name
function _SNIs:update_by_name(name, entity, options)
  local _, err, err_t = invalidate_cache(self, self:select_by_name(name))
  if err then
    return nil, err, err_t
  end

  return self.super.update_by_name(self, name, entity, options)
end


-- invalidates the *old* name when updating it to a new name
function _SNIs:upsert(pk, entity, options)
  local _, err, err_t = invalidate_cache(self, self:select(pk))
  if err then
    return nil, err, err_t
  end

  return self.super.upsert(self, pk, entity, options)
end


-- invalidates the *old* name when updating it to a new name
function _SNIs:upsert_by_name(name, entity, options)
  local _, err, err_t = invalidate_cache(self, self:select_by_name(name))
  if err then
    return nil, err, err_t
  end

  return self.super.upsert_by_name(self, name, entity, options)
end


return _SNIs
