#include "auth/auth_http.h"

#include <openssl/crypto.h>
#include <openssl/evp.h>
#include <openssl/hmac.h>

#include <string.h>

#define AUTH_HTTP_COOKIE_VALUE_MAX 512
#define AUTH_HTTP_CSRF_SUFFIX ":csrf:v1"
#define AUTH_HTTP_BINDING_INPUT_MAX (AUTH_CRYPTO_TOKEN_DIGEST_SIZE + 16)

const char *Auth_HTTP_Request_Value(
    Seobeo_Request_Entry *p_request,
    const char           *key)
{
  if (!p_request || !key)
    return NULL;
  void *p_value = Dowa_HashMap_Get_Ptr(p_request, (char *)key);
  return p_value ? ((Seobeo_Request_Entry *)p_value)->value : NULL;
}

boolean Auth_HTTP_Parse_Cookie(
    const char *cookie_header,
    const char *name,
    char       *value_out,
    size_t      capacity)
{
  if (!value_out || capacity == 0)
    return FALSE;
  value_out[0] = '\0';
  if (!cookie_header || !name || name[0] == '\0')
    return FALSE;
  size_t name_length = strlen(name);
  const char *cursor = cookie_header;

  while (*cursor)
  {
    while (*cursor == ' ' || *cursor == '\t')
      cursor++;

    if (strncmp(cursor, name, name_length) == 0 &&
        cursor[name_length] == '=')
    {
      cursor += name_length + 1;
      const char *start = cursor;
      while (*cursor && *cursor != ';')
        cursor++;
      size_t value_length = (size_t)(cursor - start);
      if (value_length >= capacity)
        return FALSE;
      memcpy(value_out, start, value_length);
      value_out[value_length] = '\0';
      return TRUE;
    }

    while (*cursor && *cursor != ';')
      cursor++;
    if (*cursor == ';')
      cursor++;
  }
  return FALSE;
}

boolean Auth_HTTP_Same_Origin(Seobeo_Request_Entry *p_request)
{
  const char *host = Auth_HTTP_Request_Value(p_request, "Host");
  const char *origin = Auth_HTTP_Request_Value(p_request, "Origin");
  if (!host || !origin)
    return FALSE;

  const char *origin_host = strstr(origin, "://");
  if (!origin_host)
    return FALSE;
  origin_host += 3;

  const char *end = strchr(origin_host, '/');
  size_t origin_host_length =
      end ? (size_t)(end - origin_host) : strlen(origin_host);
  return strlen(host) == origin_host_length &&
         strncmp(host, origin_host, origin_host_length) == 0;
}

boolean Auth_HTTP_Derive_CSRF(
    const uint8 *cookie_secret,
    size_t       cookie_secret_length,
    const char  *binding,
    char        *csrf_out,
    size_t       csrf_capacity)
{
  if (!cookie_secret ||
      cookie_secret_length < AUTH_CRYPTO_COOKIE_SECRET_MIN_BYTES ||
      cookie_secret_length > AUTH_CRYPTO_COOKIE_SECRET_MAX_BYTES ||
      !binding || !csrf_out || csrf_capacity < AUTH_CRYPTO_TOKEN_SIZE)
    return FALSE;

  size_t binding_length = strlen(binding);
  size_t suffix_length = strlen(AUTH_HTTP_CSRF_SUFFIX);
  size_t input_length = binding_length + suffix_length;
  if (input_length >= AUTH_HTTP_BINDING_INPUT_MAX)
    return FALSE;

  char input[AUTH_HTTP_BINDING_INPUT_MAX] = {0};
  memcpy(input, binding, binding_length);
  memcpy(input + binding_length, AUTH_HTTP_CSRF_SUFFIX, suffix_length);

  uint8 digest[32] = {0};
  uint32 digest_length = sizeof(digest);
  if (!HMAC(EVP_sha256(), cookie_secret, (int)cookie_secret_length,
            (const uint8 *)input, input_length, digest, &digest_length))
  {
    OPENSSL_cleanse(input, sizeof(input));
    OPENSSL_cleanse(digest, sizeof(digest));
    return FALSE;
  }
  OPENSSL_cleanse(input, sizeof(input));

  size_t encoded_length = Auth_Crypto_Base64url_Encode(
      digest, sizeof(digest), csrf_out, csrf_capacity);
  OPENSSL_cleanse(digest, sizeof(digest));
  return encoded_length == AUTH_CRYPTO_TOKEN_SIZE - 1;
}

boolean Auth_HTTP_Verify_CSRF(
    Seobeo_Request_Entry *p_request,
    const uint8          *cookie_secret,
    size_t                cookie_secret_length,
    const char           *binding)
{
  if (!Auth_HTTP_Same_Origin(p_request) || !binding)
    return FALSE;

  const char *provided = Auth_HTTP_Request_Value(p_request, "X-CSRF-Token");
  return Auth_HTTP_Verify_CSRF_Token(
      cookie_secret, cookie_secret_length, binding, provided);
}

boolean Auth_HTTP_Verify_CSRF_Token(
    const uint8 *cookie_secret,
    size_t       cookie_secret_length,
    const char  *binding,
    const char  *provided_token)
{
  if (!binding || !provided_token || provided_token[0] == '\0')
    return FALSE;
  char expected[AUTH_CRYPTO_TOKEN_SIZE] = {0};
  char provided_digest[AUTH_CRYPTO_TOKEN_DIGEST_SIZE] = {0};
  char expected_digest[AUTH_CRYPTO_TOKEN_DIGEST_SIZE] = {0};
  boolean valid = FALSE;

  if (Auth_HTTP_Derive_CSRF(
          cookie_secret, cookie_secret_length, binding,
          expected, sizeof(expected)) &&
      Auth_Crypto_Token_Digest(
          provided_token, provided_digest, sizeof(provided_digest)) ==
          AUTH_CRYPTO_OK &&
      Auth_Crypto_Token_Digest(
          expected, expected_digest, sizeof(expected_digest)) ==
          AUTH_CRYPTO_OK)
  {
    valid = CRYPTO_memcmp(
        provided_digest, expected_digest, sizeof(provided_digest)) == 0;
  }

  OPENSSL_cleanse(expected, sizeof(expected));
  OPENSSL_cleanse(provided_digest, sizeof(provided_digest));
  OPENSSL_cleanse(expected_digest, sizeof(expected_digest));
  return valid;
}

Auth_HTTP_Resolve_Result Auth_HTTP_Resolve_Authenticated_User(
    Seobeo_Request_Entry         *p_request,
    Auth_Store                   *p_store,
    const uint8                  *cookie_secret,
    size_t                        cookie_secret_length,
    int64                         current_unix,
    int64                         session_idle_ttl_secs,
    Auth_HTTP_Authenticated_User *p_user)
{
  if (!p_user)
    return AUTH_HTTP_RESOLVE_ERROR;
  memset(p_user, 0, sizeof(*p_user));
  if (!p_request || !p_store || !cookie_secret ||
      cookie_secret_length < AUTH_CRYPTO_COOKIE_SECRET_MIN_BYTES ||
      cookie_secret_length > AUTH_CRYPTO_COOKIE_SECRET_MAX_BYTES ||
      current_unix < 0 || session_idle_ttl_secs <= 0)
    return AUTH_HTTP_RESOLVE_ERROR;

  char session_token[AUTH_HTTP_COOKIE_VALUE_MAX] = {0};
  const char *cookie_header = Auth_HTTP_Request_Value(p_request, "Cookie");
  if (!cookie_header ||
      !Auth_HTTP_Parse_Cookie(
          cookie_header, AUTH_HTTP_SESSION_COOKIE_NAME,
          session_token, sizeof(session_token)) ||
      session_token[0] == '\0')
  {
    OPENSSL_cleanse(session_token, sizeof(session_token));
    return AUTH_HTTP_RESOLVE_NOT_FOUND;
  }

  if (Auth_Crypto_Token_Digest(
          session_token, p_user->token_digest,
          sizeof(p_user->token_digest)) != AUTH_CRYPTO_OK)
  {
    OPENSSL_cleanse(session_token, sizeof(session_token));
    OPENSSL_cleanse(p_user, sizeof(*p_user));
    return AUTH_HTTP_RESOLVE_NOT_FOUND;
  }
  OPENSSL_cleanse(session_token, sizeof(session_token));

  Auth_Store_Result result = Auth_Store_Find_Session(
      p_store, p_user->token_digest, current_unix,
      &p_user->session, &p_user->user);
  if (result != AUTH_STORE_OK)
  {
    OPENSSL_cleanse(p_user, sizeof(*p_user));
    return result == AUTH_STORE_ERROR
        ? AUTH_HTTP_RESOLVE_ERROR
        : AUTH_HTTP_RESOLVE_NOT_FOUND;
  }

  Auth_Store_Touch_Session(
      p_store, p_user->token_digest, current_unix, session_idle_ttl_secs);
  if (!Auth_HTTP_Derive_CSRF(
          cookie_secret, cookie_secret_length, p_user->token_digest,
          p_user->csrf_token, sizeof(p_user->csrf_token)))
  {
    OPENSSL_cleanse(p_user, sizeof(*p_user));
    return AUTH_HTTP_RESOLVE_ERROR;
  }
  return AUTH_HTTP_RESOLVE_OK;
}
