local json = require "json"

local auth_url = "http://app:3001/internal/dovecot/auth"
local secret_file = "/run/secrets/dovecot_auth_secret"
local http_client
local shared_secret

local function read_secret(path)
  local file = io.open(path, "r")
  if file == nil then
    error("Dovecot authentication secret is unavailable")
  end

  local value = file:read("*a")
  file:close()
  value = string.gsub(value or "", "^%s+", "")
  value = string.gsub(value, "%s+$", "")
  if #value < 32 or #value > 512 or string.find(value, "%s") ~= nil then
    error("Dovecot authentication secret is invalid")
  end
  return value
end

function script_init()
  shared_secret = read_secret(secret_file)
  http_client = dovecot.http.client {
    auto_retry = "no",
    request_max_attempts = 1,
    connect_timeout = "1s",
    request_timeout = "30s",
    request_absolute_timeout = "30s"
  }
  return 0
end

local function failure(result)
  return result, nil
end

local function valid_user(value)
  if type(value) ~= "string" or #value == 0 or #value > 320 then
    return false
  end
  if string.find(value, "/", 1, true) ~= nil
      or string.find(value, "\\", 1, true) ~= nil
      or string.find(value, "\0", 1, true) ~= nil
      or string.find(value, "%s") ~= nil then
    return false
  end
  return string.find(value, "^[^@]+@[^@]+%.[^@]+$") ~= nil
end

local function first_nonempty_string(...)
  for index = 1, select("#", ...) do
    local value = select(index, ...)
    if value ~= nil then
      local normalized = tostring(value)
      if normalized ~= "" then
        return normalized
      end
    end
  end
  return ""
end

function auth_password_verify(request, password)
  local protocol = string.lower(first_nonempty_string(request.protocol, request.service))
  local remote_ip = first_nonempty_string(
    request.remote_ip,
    request.real_remote_ip,
    request.rip,
    request.real_rip
  )
  if protocol ~= "imap" and protocol ~= "pop3" then
    return failure(dovecot.auth.PASSDB_RESULT_USER_DISABLED)
  end

  local http_request = http_client:request {
    url = auth_url,
    method = "POST"
  }
  http_request:add_header("content-type", "application/json")
  http_request:add_header("authorization", "Bearer " .. shared_secret)
  http_request:add_header("connection", "close")
  http_request:set_payload(json.encode {
    username = request.user,
    password = password,
    service = protocol,
    remoteIp = remote_ip
  })

  local submitted, response = pcall(function()
    return http_request:submit()
  end)
  if not submitted then
    return failure(dovecot.auth.PASSDB_RESULT_INTERNAL_FAILURE)
  end

  local status = response:status()
  if status ~= 200 then
    return failure(dovecot.auth.PASSDB_RESULT_INTERNAL_FAILURE)
  end

  local decoded, payload = pcall(json.decode, response:payload())
  if not decoded or type(payload) ~= "table" then
    return failure(dovecot.auth.PASSDB_RESULT_INTERNAL_FAILURE)
  end
  if payload.authenticated == false then
    return failure(dovecot.auth.PASSDB_RESULT_PASSWORD_MISMATCH)
  end
  if payload.authenticated ~= true then
    return failure(dovecot.auth.PASSDB_RESULT_INTERNAL_FAILURE)
  end
  if not valid_user(payload.user) then
    return failure(dovecot.auth.PASSDB_RESULT_INTERNAL_FAILURE)
  end

  return dovecot.auth.PASSDB_RESULT_OK, { user = string.lower(payload.user) }
end
