#include <compat.h> // IWYU pragma: keep

#include <uv.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <tlsuv/tlsuv.h>
#include <tlsuv/http.h>
#include <utarray.h>

#include "config.h"
#include "runtime.h"
#include "modules/fetch.h"
#include "modules/json.h"
#include "modules/timer.h"

typedef struct {
  char *data;
  size_t size;
  size_t capacity;
} fetch_buffer_t;

typedef struct fetch_request_s {
  struct js *js;
  jsval_t promise;
  tlsuv_http_t http_client;
  tlsuv_http_req_t *http_req;
  fetch_buffer_t response_buffer;
  int status_code;
  int completed;
  int failed;
  char *error_msg;
  jsval_t headers_obj;
} fetch_request_t;

static uv_loop_t *fetch_loop = NULL;
static UT_array *pending_requests = NULL;

static void free_fetch_request(fetch_request_t *req) {
  if (!req) return;
  
  if (req->response_buffer.data) free(req->response_buffer.data);
  if (req->error_msg) free(req->error_msg);
  
  free(req);
}

static void remove_pending_request(fetch_request_t *req) {
  if (!req || !pending_requests) return;
  
  fetch_request_t **p = NULL;
  unsigned int i = 0;
  
  while ((p = (fetch_request_t**)utarray_next(pending_requests, p))) {
    if (*p == req) { utarray_erase(pending_requests, i, 1); break; }
    i++;
  }
}

static jsval_t response_text(struct js *js, jsval_t *args, int nargs) {
  (void)args; (void)nargs;
  
  jsval_t fn = js_getcurrentfunc(js);
  jsval_t body = js_get_slot(js, fn, SLOT_DATA);
  jsval_t promise = js_mkpromise(js);
  js_resolve_promise(js, promise, body);
  
  return promise;
}

static jsval_t response_json(struct js *js, jsval_t *args, int nargs) {
  (void)args; (void)nargs;
  
  jsval_t fn = js_getcurrentfunc(js);
  jsval_t body = js_get_slot(js, fn, SLOT_DATA);
  jsval_t parsed = js_json_parse(js, &body, 1);
  jsval_t promise = js_mkpromise(js);
  
  if (js_type(parsed) == JS_ERR) {
    js_reject_promise(js, promise, parsed);
  } else js_resolve_promise(js, promise, parsed);
  
  return promise;
}

static jsval_t create_response(struct js *js, int status, const char *body, size_t body_len) {
  jsval_t response_obj = js_mkobj(js);
  jsval_t body_str = js_mkstr(js, body, body_len);

  js_set(js, response_obj, "ok", status >= 200 && status < 300 ? js_mktrue() : js_mkfalse());
  js_set(js, response_obj, "status", js_mknum(status));
  
  js_set(js, response_obj, "text", js_heavy_mkfun(js, response_text, body_str));
  js_set(js, response_obj, "json", js_heavy_mkfun(js, response_json, body_str));
  
  return response_obj;
}

static void complete_request(fetch_request_t *req) {
  if (req->failed) {
    jsval_t err = js_mkstr(req->js, req->error_msg ? req->error_msg : "Unknown error", req->error_msg ? strlen(req->error_msg) : 13);
    js_reject_promise(req->js, req->promise, err);
  } else {
    const char *body = req->response_buffer.data ? req->response_buffer.data : "";
    size_t body_len = req->response_buffer.data ? req->response_buffer.size : 0;
    jsval_t response = create_response(req->js, req->status_code, body, body_len);
    js_resolve_promise(req->js, req->promise, response);
  }
  
  remove_pending_request(req);
  free_fetch_request(req);
}

static void on_http_close(tlsuv_http_t *client) {
  fetch_request_t *req = (fetch_request_t *)client->data;
  if (req && req->completed) complete_request(req);
}

static void body_cb(tlsuv_http_req_t *http_req, char *body, ssize_t len) {
  fetch_request_t *req = (fetch_request_t *)http_req->data;
  
  if (len == UV_EOF) {
    req->completed = 1;
    tlsuv_http_close(&req->http_client, on_http_close);
    return;
  }
  
  if (len < 0) {
    req->failed = 1;
    req->error_msg = strdup(uv_strerror(len));
    req->completed = 1;
    tlsuv_http_close(&req->http_client, on_http_close);
    return;
  }
  
  if (req->response_buffer.size + len > req->response_buffer.capacity) {
    size_t new_capacity = req->response_buffer.capacity * 2;
    while (new_capacity < req->response_buffer.size + len) new_capacity *= 2;
    char *new_data = realloc(req->response_buffer.data, new_capacity);
    
    if (!new_data) {
      req->failed = 1;
      req->error_msg = strdup("Out of memory");
      req->completed = 1;
      tlsuv_http_close(&req->http_client, on_http_close);
      return;
    }
    
    req->response_buffer.data = new_data;
    req->response_buffer.capacity = new_capacity;
  }
  
  memcpy(req->response_buffer.data + req->response_buffer.size, body, len);
  req->response_buffer.size += len;
}

static void resp_cb(tlsuv_http_resp_t *resp, void *data) {
  (void)data;
  fetch_request_t *req = (fetch_request_t *)resp->req->data;
  
  if (resp->code < 0) {
    req->failed = 1;
    req->error_msg = strdup(uv_strerror(resp->code));
    req->completed = 1;
    tlsuv_http_close(&req->http_client, on_http_close);
    return;
  }
  
  req->status_code = resp->code;
  resp->body_cb = body_cb;
}

static jsval_t do_fetch_microtask(struct js *js, jsval_t *args, int nargs) {
  (void)args;
  (void)nargs;
  
  jsval_t current_func = js_getcurrentfunc(js);
  jsval_t url_val = js_get(js, current_func, "url");
  jsval_t options_val = js_get(js, current_func, "options");
  jsval_t promise = js_get(js, current_func, "promise");
  
  char *url_str = js_getstr(js, url_val, NULL);
  if (!url_str) {
    jsval_t err = js_mkstr(js, "Invalid URL", 11);
    js_reject_promise(js, promise, err);
    return js_mkundef();
  }
  
  fetch_request_t *req = calloc(1, sizeof(fetch_request_t));
  if (!req) {
    jsval_t err = js_mkstr(js, "Out of memory", 13);
    js_reject_promise(js, promise, err);
    return js_mkundef();
  }
  
  req->js = js;
  req->promise = promise;
  req->headers_obj = options_val;
  
  req->response_buffer.capacity = 16384;
  req->response_buffer.data = malloc(req->response_buffer.capacity);
  req->response_buffer.size = 0;
  
  if (!req->response_buffer.data) {
    jsval_t err = js_mkstr(js, "Out of memory", 13);
    js_reject_promise(js, promise, err);
    free(req);
    return js_mkundef();
  }
  
  if (!pending_requests) utarray_new(pending_requests, &ut_ptr_icd);
  utarray_push_back(pending_requests, &req);
  
  const char *scheme_end = strstr(url_str, "://");
  if (!scheme_end) {
    jsval_t err = js_mkstr(js, "Invalid URL: no scheme", 22);
    js_reject_promise(js, promise, err);
    remove_pending_request(req);
    free_fetch_request(req);
    return js_mkundef();
  }
  
  const char *host_start = scheme_end + 3;
  const char *path_start = strchr(host_start, '/');
  const char *at_in_host = NULL;
  
  for (const char *p = host_start; p < (path_start ? path_start : host_start + strlen(host_start)); p++) {
    if (*p == '@') at_in_host = p;
  }
  if (at_in_host) host_start = at_in_host + 1;
  
  size_t scheme_len = scheme_end - url_str;
  size_t host_len = path_start ? (size_t)(path_start - host_start) : strlen(host_start);
  const char *path = path_start ? path_start : "/";
  
  if (host_len == 0) {
    jsval_t err = js_mkstr(js, "Invalid URL: no host", 20);
    js_reject_promise(js, promise, err);
    remove_pending_request(req);
    free_fetch_request(req);
    return js_mkundef();
  }
  
  char *host_url = calloc(1, scheme_len + 3 + host_len + 1);
  if (!host_url) {
    jsval_t err = js_mkstr(js, "Out of memory", 13);
    js_reject_promise(js, promise, err);
    remove_pending_request(req);
    free_fetch_request(req);
    return js_mkundef();
  }
  snprintf(host_url, scheme_len + 3 + host_len + 1, "%.*s://%.*s", (int)scheme_len, url_str, (int)host_len, host_start);
  
  int rc = tlsuv_http_init(fetch_loop, &req->http_client, host_url);
  free(host_url);
  
  if (rc != 0) {
    jsval_t err = js_mkstr(js, "Failed to initialize HTTP client", 33);
    js_reject_promise(js, promise, err);
    remove_pending_request(req);
    free_fetch_request(req);
    return js_mkundef();
  }
  
  req->http_client.data = req;
  
  char *method = "GET";
  char *body = NULL;
  size_t body_len = 0;
  
  if (js_type(options_val) == JS_OBJ) {
    jsval_t method_val = js_get(js, options_val, "method");
    if (js_type(method_val) == JS_STR) {
      char *method_str = js_getstr(js, method_val, NULL);
      if (method_str) method = method_str;
    }
    
    jsval_t body_val = js_get(js, options_val, "body");
    if (js_type(body_val) == JS_STR) {
      body = js_getstr(js, body_val, NULL);
      if (body) body_len = strlen(body);
    }
  }
  
  req->http_req = tlsuv_http_req(&req->http_client, method, path, resp_cb, req);
  
  if (!req->http_req) {
    jsval_t err = js_mkstr(js, "Failed to create HTTP request", 30);
    js_reject_promise(js, promise, err);
    tlsuv_http_close(&req->http_client, NULL);
    remove_pending_request(req);
    free_fetch_request(req);
    return js_mkundef();
  }
  
  req->http_req->data = req;
  
  char user_agent[256];
  snprintf(user_agent, sizeof(user_agent), "ant/%s", ANT_VERSION);
  tlsuv_http_req_header(req->http_req, "User-Agent", user_agent);
  
  if (js_type(options_val) == JS_OBJ) {
    jsval_t headers_val = js_get(js, options_val, "headers");
    if (js_type(headers_val) == JS_OBJ) {
      js_prop_iter_t iter = js_prop_iter_begin(js, headers_val);
      const char *key;
      size_t key_len;
      jsval_t value;
      
      while (js_prop_iter_next(&iter, &key, &key_len, &value)) {
        char *value_str = js_getstr(js, value, NULL);
        if (value_str) {
          char *key_str = strndup(key, key_len);
          if (key_str) { tlsuv_http_req_header(req->http_req, key_str, value_str); free(key_str); }
        }
      }
      js_prop_iter_end(&iter);
    }
  }
  
  if (body && body_len > 0) {
    tlsuv_http_req_data(req->http_req, body, body_len, body_cb);
  }
  
  return js_mkundef();
}

static jsval_t js_fetch(struct js *js, jsval_t *args, int nargs) {
  if (nargs < 1) return js_mkerr(js, "fetch requires at least 1 argument");
  if (js_type(args[0]) != JS_STR) return js_mkerr(js, "fetch URL must be a string");
  
  jsval_t url_val = args[0];
  jsval_t options_val = nargs > 1 ? args[1] : js_mkundef();
  
  jsval_t promise = js_mkpromise(js);
  jsval_t wrapper_obj = js_mkobj(js);
  
  js_set_slot(js, wrapper_obj, SLOT_CFUNC, js_mkfun(do_fetch_microtask));
  js_set(js, wrapper_obj, "url", url_val);
  js_set(js, wrapper_obj, "options", options_val);
  js_set(js, wrapper_obj, "promise", promise);
  
  queue_microtask(js, js_obj_to_func(wrapper_obj));
  
  return promise;
}

void init_fetch_module() {
  if (!fetch_loop) fetch_loop = uv_default_loop();
  struct js *js = rt->js;
  js_set(js, js_glob(js), "fetch", js_mkfun(js_fetch));
}

int has_pending_fetches(void) {
  return (pending_requests && utarray_len(pending_requests) > 0) || (fetch_loop && uv_loop_alive(fetch_loop));
}

void fetch_poll_events(void) {
  if (fetch_loop && fetch_loop == uv_default_loop() && (rt->flags & ANT_RUNTIME_EXT_EVENT_LOOP)) return;
  if (fetch_loop && uv_loop_alive(fetch_loop)) {
    uv_run(fetch_loop, UV_RUN_ONCE);
    if (pending_requests && utarray_len(pending_requests) > 0) usleep(1000);
  }
}

void fetch_gc_update_roots(GC_FWD_ARGS) {
  if (!pending_requests) return;
  unsigned int len = utarray_len(pending_requests);
  for (unsigned int i = 0; i < len; i++) {
    fetch_request_t **reqp = (fetch_request_t **)utarray_eltptr(pending_requests, i);
    if (reqp && *reqp) {
      (*reqp)->promise = fwd_val(ctx, (*reqp)->promise);
      (*reqp)->headers_obj = fwd_val(ctx, (*reqp)->headers_obj);
    }
  }
}
