#include "connectors/connector.h"

#include <stdio.h>
#include <string.h>
#include <time.h>

static boolean json_is_bounded(const char *json, size_t length)
{
  if (!json || length > CONNECTOR_MAX_JSON_BYTES)
    return FALSE;
  int32 depth = 0;
  boolean string = FALSE, escaped = FALSE;
  for (size_t i = 0; i < length; ++i) {
    char c = json[i];
    if (string) {
      if (escaped)
        escaped = FALSE;
      else if (c == '\\')
        escaped = TRUE;
      else if (c == '"')
        string = FALSE;
    } else if (c == '"')
      string = TRUE;
    else if (c == '{' || c == '[') {
      if (++depth > 32)
        return FALSE;
    } else if (c == '}' || c == ']') {
      if (--depth < 0)
        return FALSE;
    }
  }
  return !string && depth == 0;
}

static Connector_Status default_transport(
    const Connector_HTTP_Request *request, const char *access_token,
    Connector_HTTP_Response *response, Dowa_Arena *arena, void *context)
{
  (void)context;
  if (!request || !response || !arena)
    return CONNECTOR_INVALID;
  Seobeo_Client_Request *client = Seobeo_Client_Request_Create(request->url);
  if (!client)
    return CONNECTOR_PROVIDER_ERROR;
  Seobeo_Client_Request_Set_Method(client, request->method);
  Seobeo_Client_Request_Set_Timeout_Milliseconds(client, 15000);
  if (access_token && access_token[0]) {
    char authorization[2300];
    int32 length = snprintf(
        authorization, sizeof(authorization), "Authorization: Bearer %s",
        access_token);
    if (length <= 0 || (size_t)length >= sizeof(authorization)) {
      Seobeo_Client_Request_Destroy(client);
      return CONNECTOR_INVALID;
    }
    Seobeo_Client_Request_Add_Header_Array(client, authorization);
  }
  if (request->content_type) {
    char header[256];
    int32 length = snprintf(
        header, sizeof(header), "Content-Type: %s", request->content_type);
    if (length <= 0 || (size_t)length >= sizeof(header)) {
      Seobeo_Client_Request_Destroy(client);
      return CONNECTOR_INVALID;
    }
    Seobeo_Client_Request_Add_Header_Array(client, header);
  }
  if (request->body && request->body_length)
    Seobeo_Client_Request_Set_Body(
        client, request->body, request->body_length);
  if (request->download_path)
    Seobeo_Client_Request_Set_Download_Path(client, request->download_path);
  Seobeo_Client_Response *provider = Seobeo_Client_Request_Execute(client);
  if (!provider) {
    Seobeo_Client_Request_Destroy(client);
    return CONNECTOR_PROVIDER_ERROR;
  }
  response->status_code = provider->status_code;
  response->body_length = provider->body_length;
  if (provider->body && !request->download_path) {
    response->body = Dowa_Arena_Allocate(arena, provider->body_length + 1);
    if (!response->body) {
      Seobeo_Client_Response_Destroy(provider);
      Seobeo_Client_Request_Destroy(client);
      return CONNECTOR_ERROR;
    }
    memcpy(response->body, provider->body, provider->body_length);
    response->body[provider->body_length] = '\0';
  } else
    response->body = NULL;
  Seobeo_Client_Response_Destroy(provider);
  Seobeo_Client_Request_Destroy(client);
  return CONNECTOR_OK;
}

static Connector_Status transport(
    const Connector_Google_Config *config,
    const Connector_HTTP_Request *request, const char *access_token,
    Connector_HTTP_Response *response, Dowa_Arena *arena)
{
  Connector_HTTP_Transport implementation =
      config->transport ? config->transport : default_transport;
  return implementation(
      request, access_token, response, arena, config->transport_context);
}

static char *url_join(
    Dowa_Arena *arena, const char *base, const char *path, const char *query)
{
  if (!arena || !base)
    return NULL;
  size_t length = strlen(base) + (path ? strlen(path) : 0) +
      (query && query[0] ? strlen(query) + 1 : 0) + 1;
  char *url = Dowa_Arena_Allocate(arena, length);
  if (!url)
    return NULL;
  snprintf(
      url, length, "%s%s%s%s", base, path ? path : "",
      query && query[0] ? "?" : "", query && query[0] ? query : "");
  return url;
}

char *Connector_Google_Authorization_URL(
    const Connector_Google_Config *config, const Connector_OAuth_Start *start,
    Dowa_Arena *arena)
{
  if (!config || !start || !arena)
    return NULL;
  char client[1024], redirect[2048];
  if (!Connector_Form_Encode(config->client_id, client, sizeof(client)) ||
      !Connector_Form_Encode(
          config->redirect_uri, redirect, sizeof(redirect)))
    return NULL;
  const char *scopes =
      "openid%20email%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fdrive"
      "%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fgmail.readonly"
      "%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fgmail.compose";
  size_t length = strlen(config->oauth_authorize_url) + strlen(client) +
      strlen(redirect) + strlen(start->state) + strlen(start->code_challenge) +
      strlen(scopes) + 256;
  char *url = Dowa_Arena_Allocate(arena, length);
  if (!url)
    return NULL;
  snprintf(
      url, length,
      "%s?client_id=%s&redirect_uri=%s&response_type=code&scope=%s"
      "&access_type=offline&prompt=consent&state=%s"
      "&code_challenge=%s&code_challenge_method=S256",
      config->oauth_authorize_url, client, redirect, scopes, start->state,
      start->code_challenge);
  return url;
}

static Connector_Status token_request(
    const Connector_Google_Config *config, const char *body,
    Connector_HTTP_Response *response, Connector_Provider_Error *error,
    Dowa_Arena *arena)
{
  Connector_HTTP_Request request = {
    .method = "POST",
    .url = config->oauth_token_url,
    .content_type = "application/x-www-form-urlencoded",
    .body = body,
    .body_length = strlen(body),
    .download_path = NULL
  };
  Connector_Status status = transport(
      config, &request, NULL, response, arena);
  if (status != CONNECTOR_OK) {
    if (error)
      snprintf(error->code, sizeof(error->code), "transport_error");
    return status;
  }
  if (response->status_code >= 200 && response->status_code < 300)
    return CONNECTOR_OK;
  if (error) {
    error->http_status = response->status_code;
    snprintf(error->code, sizeof(error->code), "token_endpoint_error");
    if (json_is_bounded(response->body, response->body_length)) {
      Dowa_JSON_Value parsed = Dowa_JSON_Parse(
          response->body, (int32)response->body_length, arena);
      if (parsed.type == DOWA_JSON_OBJECT) {
        char *code = Dowa_JSON_Get_String(parsed.object_val, "error");
        char *description =
            Dowa_JSON_Get_String(parsed.object_val, "error_description");
        if (code)
          snprintf(error->code, sizeof(error->code), "%s", code);
        if (description)
          snprintf(
              error->description, sizeof(error->description), "%s",
              description);
      }
    }
  }
  return CONNECTOR_PROVIDER_ERROR;
}

static boolean copy_json_string(
    Dowa_JSON_Entry *object, const char *key, char *output, size_t output_size)
{
  char *value = Dowa_JSON_Get_String(object, key);
  if (!value || strlen(value) >= output_size)
    return FALSE;
  strcpy(output, value);
  return TRUE;
}

Connector_Status Connector_Google_Exchange_Code(
    const Connector_Google_Config *config, const char *code,
    const char *verifier, Connector_Account *account,
    Connector_Provider_Error *error, Dowa_Arena *arena)
{
  if (!config || !code || !verifier || !account || !arena)
    return CONNECTOR_INVALID;
  if (error)
    memset(error, 0, sizeof(*error));
  char encoded_code[4096], encoded_verifier[512], encoded_client[1024];
  char encoded_secret[2048], encoded_redirect[2048];
  if (!Connector_Form_Encode(code, encoded_code, sizeof(encoded_code))) {
    if (error)
      snprintf(error->code, sizeof(error->code), "encode_code_failed");
    return CONNECTOR_INVALID;
  }
  if (!Connector_Form_Encode(
          verifier, encoded_verifier, sizeof(encoded_verifier))) {
    if (error)
      snprintf(error->code, sizeof(error->code), "encode_verifier_failed");
    return CONNECTOR_INVALID;
  }
  if (!Connector_Form_Encode(
          config->client_id, encoded_client, sizeof(encoded_client))) {
    if (error)
      snprintf(error->code, sizeof(error->code), "encode_client_id_failed");
    return CONNECTOR_INVALID;
  }
  if (!Connector_Form_Encode(
          config->client_secret, encoded_secret, sizeof(encoded_secret))) {
    if (error)
      snprintf(error->code, sizeof(error->code), "encode_client_secret_failed");
    return CONNECTOR_INVALID;
  }
  if (!Connector_Form_Encode(
          config->redirect_uri, encoded_redirect, sizeof(encoded_redirect))) {
    if (error)
      snprintf(error->code, sizeof(error->code), "encode_redirect_uri_failed");
    return CONNECTOR_INVALID;
  }
  char body[12288];
  int32 body_length = snprintf(
      body, sizeof(body),
      "code=%s&client_id=%s&client_secret=%s&redirect_uri=%s"
      "&code_verifier=%s&grant_type=authorization_code",
      encoded_code, encoded_client, encoded_secret, encoded_redirect,
      encoded_verifier);
  if (body_length <= 0 || (size_t)body_length >= sizeof(body)) {
    if (error)
      snprintf(error->code, sizeof(error->code), "token_request_too_large");
    return CONNECTOR_INVALID;
  }
  Connector_HTTP_Response token = {0};
  Connector_Status status = token_request(config, body, &token, error, arena);
  if (status != CONNECTOR_OK)
    return status;
  if (error)
    error->http_status = token.status_code;
  if (!token.body || token.body_length == 0) {
    if (error) {
      snprintf(error->code, sizeof(error->code), "empty_token_response");
      snprintf(
          error->description, sizeof(error->description),
          "Google token endpoint returned no response body");
    }
    return CONNECTOR_PROVIDER_ERROR;
  }
  if (!json_is_bounded(token.body, token.body_length)) {
    if (error) {
      snprintf(error->code, sizeof(error->code), "invalid_token_body");
      snprintf(
          error->description, sizeof(error->description),
          "Google token response was not bounded JSON (%zu bytes)",
          token.body_length);
    }
    return CONNECTOR_PROVIDER_ERROR;
  }
  Dowa_JSON_Value parsed = Dowa_JSON_Parse(
      token.body, (int32)token.body_length, arena);
  if (parsed.type != DOWA_JSON_OBJECT) {
    if (error) {
      snprintf(error->code, sizeof(error->code), "invalid_token_json");
      snprintf(
          error->description, sizeof(error->description),
          "Google token response JSON could not be parsed (%zu bytes)",
          token.body_length);
    }
    return CONNECTOR_PROVIDER_ERROR;
  }
  Dowa_JSON_Entry *object = parsed.object_val;
  memset(account, 0, sizeof(*account));
  strcpy(account->provider, "google");
  if (!copy_json_string(
          object, "access_token", account->access_token,
          sizeof(account->access_token))) {
    if (error)
      snprintf(error->code, sizeof(error->code), "invalid_token_response");
    return CONNECTOR_PROVIDER_ERROR;
  }
  char *refresh = Dowa_JSON_Get_String(object, "refresh_token");
  if (refresh && strlen(refresh) < sizeof(account->refresh_token))
    strcpy(account->refresh_token, refresh);
  char *scope = Dowa_JSON_Get_String(object, "scope");
  if (scope && strlen(scope) < sizeof(account->scopes))
    strcpy(account->scopes, scope);
  double expires = Dowa_JSON_Get_Number(object, "expires_in");
  account->expires_at = (int64)time(NULL) + (int64)expires;

  Connector_HTTP_Request identity_request = {
    .method = "GET", .url = config->identity_url
  };
  Connector_HTTP_Response identity = {0};
  status = transport(
      config, &identity_request, account->access_token, &identity, arena);
  if (status != CONNECTOR_OK || identity.status_code < 200 ||
      identity.status_code >= 300 ||
      !json_is_bounded(identity.body, identity.body_length)) {
    if (error) {
      error->http_status = identity.status_code;
      snprintf(error->code, sizeof(error->code), "userinfo_failed");
      snprintf(
          error->description, sizeof(error->description),
          "Google user-info lookup failed after token exchange");
    }
    return CONNECTOR_PROVIDER_ERROR;
  }
  parsed = Dowa_JSON_Parse(
      identity.body, (int32)identity.body_length, arena);
  if (parsed.type != DOWA_JSON_OBJECT)
    return CONNECTOR_PROVIDER_ERROR;
  object = parsed.object_val;
  if (!copy_json_string(
          object, "sub", account->provider_subject,
          sizeof(account->provider_subject)) ||
      !copy_json_string(object, "email", account->email, sizeof(account->email))) {
    if (error)
      snprintf(error->code, sizeof(error->code), "invalid_userinfo_response");
    return CONNECTOR_PROVIDER_ERROR;
  }
  int32 written = snprintf(
      account->account_id, sizeof(account->account_id), "google:%s",
      account->provider_subject);
  return written > 0 && (size_t)written < sizeof(account->account_id)
      ? CONNECTOR_OK : CONNECTOR_PROVIDER_ERROR;
}

Connector_Status Connector_Google_Refresh(
    const Connector_Google_Config *config, Connector_Account *account,
    Dowa_Arena *arena)
{
  if (!config || !account || !account->refresh_token[0] || !arena)
    return CONNECTOR_INVALID;
  char refresh[4096], client[1024], secret[2048], body[8192];
  if (!Connector_Form_Encode(
          account->refresh_token, refresh, sizeof(refresh)) ||
      !Connector_Form_Encode(config->client_id, client, sizeof(client)) ||
      !Connector_Form_Encode(
          config->client_secret, secret, sizeof(secret)))
    return CONNECTOR_INVALID;
  int32 length = snprintf(
      body, sizeof(body),
      "refresh_token=%s&client_id=%s&client_secret=%s"
      "&grant_type=refresh_token",
      refresh, client, secret);
  if (length <= 0 || (size_t)length >= sizeof(body))
    return CONNECTOR_INVALID;
  Connector_HTTP_Response token = {0};
  Connector_Status status = token_request(config, body, &token, NULL, arena);
  if (status != CONNECTOR_OK || !json_is_bounded(token.body, token.body_length))
    return status == CONNECTOR_OK ? CONNECTOR_PROVIDER_ERROR : status;
  Dowa_JSON_Value parsed = Dowa_JSON_Parse(
      token.body, (int32)token.body_length, arena);
  if (parsed.type != DOWA_JSON_OBJECT ||
      !copy_json_string(
          parsed.object_val, "access_token", account->access_token,
          sizeof(account->access_token)))
    return CONNECTOR_PROVIDER_ERROR;
  account->expires_at = (int64)time(NULL) +
      (int64)Dowa_JSON_Get_Number(parsed.object_val, "expires_in");
  return CONNECTOR_OK;
}

Connector_Status Connector_Google_Revoke(
    const Connector_Google_Config *config, const char *token, Dowa_Arena *arena)
{
  if (!config || !token || !arena)
    return CONNECTOR_INVALID;
  char encoded[4096], body[4200];
  if (!Connector_Form_Encode(token, encoded, sizeof(encoded)))
    return CONNECTOR_INVALID;
  snprintf(body, sizeof(body), "token=%s", encoded);
  Connector_HTTP_Request request = {
    .method = "POST", .url = config->oauth_revoke_url,
    .content_type = "application/x-www-form-urlencoded",
    .body = body, .body_length = strlen(body)
  };
  Connector_HTTP_Response response = {0};
  Connector_Status status = transport(config, &request, NULL, &response, arena);
  return status == CONNECTOR_OK && response.status_code >= 200 &&
      response.status_code < 300 ? CONNECTOR_OK : CONNECTOR_PROVIDER_ERROR;
}

Connector_Status Connector_Google_Execute(
    const Connector_Google_Config *config, Connector_Operation operation,
    const Connector_Provider_Request *request, const char *access_token,
    Connector_Provider_Response *response, Dowa_Arena *arena)
{
  if (!config || !request || !access_token || !response || !arena ||
      !Connector_Operation_Is_Allowed(operation) ||
      request->body_length > CONNECTOR_MAX_JSON_BYTES)
    return CONNECTOR_INVALID;
  const char *base = operation <= CONNECTOR_OP_DRIVE_UPDATE
      ? ((operation == CONNECTOR_OP_DRIVE_UPLOAD ||
          operation == CONNECTOR_OP_DRIVE_UPDATE) && request->body_length
             ? config->drive_upload_url : config->drive_api_url)
      : config->gmail_api_url;
  char *url = url_join(arena, base, request->path, request->query);
  if (!url)
    return CONNECTOR_ERROR;
  Connector_HTTP_Request provider_request = {
    .method = request->method,
    .url = url,
    .content_type = request->content_type,
    .body = request->body,
    .body_length = request->body_length,
    .download_path = request->download_path
  };
  Connector_HTTP_Response provider_response = {0};
  Connector_Status status = transport(
      config, &provider_request, access_token, &provider_response, arena);
  if (status != CONNECTOR_OK)
    return status;
  response->provider_status = provider_response.status_code;
  response->body = provider_response.body;
  response->body_length = provider_response.body_length;
  response->status = provider_response.status_code >= 200 &&
      provider_response.status_code < 300
      ? CONNECTOR_OK : CONNECTOR_PROVIDER_ERROR;
  return response->status;
}
