//  TODO
//  - Error handling
//  - HTTPS support

#ifndef KIT_HTTP1_H
#define KIT_HTTP1_H

#include "types.h"
#include "string_builder.h"
#include "sockets.h"

#include <stdio.h>
#include <assert.h>

#ifdef __cplusplus
extern "C" {
#endif

#define KIT_HTTP1_NEWLINE "\r\n"
#define KIT_HTTP1_SPACE " "
#define KIT_HTTP1_HEADER_SEPARATOR ": "
#define KIT_HTTP1_BUFFER_SIZE 8192

enum {
  KIT_HTTP1_OPTIONS,
  KIT_HTTP1_GET,
  KIT_HTTP1_HEAD,
  KIT_HTTP1_POST,
  KIT_HTTP1_PUT,
  KIT_HTTP1_DELETE,
  KIT_HTTP1_TRACE,
  KIT_HTTP1_CONNECT,
};

typedef struct {
  kit_str_t key;
  kit_str_t value;
} kit_http1_str_pair_t;

typedef KIT_DA(kit_http1_str_pair_t) kit_http1_str_map_t;

typedef struct {
  kit_str_t str;
  i64       position;
} kit_http1_tok_t;

typedef struct {
  kit_str_t           protocol;
  kit_str_t           host;
  kit_str_t           port;
  kit_str_t           address;
  kit_str_t           query_string;
  kit_str_t           hash;
  kit_http1_str_map_t parameters;
} kit_http1_uri_t;

typedef struct {
  i8                  success;
  kit_str_builder_t   protocol;
  kit_str_builder_t   response;
  kit_str_builder_t   response_str;
  kit_http1_str_map_t header;
  kit_str_builder_t   header_str;
  kit_str_builder_t   body;
} kit_http1_response_t;

#ifdef __GNUC__
#  pragma GCC diagnostic push
#  pragma GCC diagnostic ignored "-Wunused-function"
#  pragma GCC diagnostic ignored "-Wunknown-pragmas"
#endif

static kit_str_t kit_http1_tok_tail(kit_http1_tok_t *tok) {
  assert(tok != NULL);

  if (tok == NULL)
    return (kit_str_t) { .size = 0, .values = NULL };

  i64 previous_position = tok->position;
  tok->position         = tok->str.size;

  return (kit_str_t) { .size   = tok->position - previous_position,
                       .values = tok->str.values +
                                 previous_position };
}

static kit_str_t kit_http1_tok_next(kit_http1_tok_t *tok,
                                    kit_str_t        search,
                                    i8               return_tail) {
  assert(tok != NULL);
  if (tok == NULL)
    return (kit_str_t) { .size = 0, .values = NULL };

  i64 hit = tok->position;

  while (hit + search.size <= tok->str.size) {
    kit_str_t s = { .size   = search.size,
                    .values = tok->str.values + hit };
    if (KIT_AR_EQUAL(s, search))
      break;
  }

  if (hit + search.size > tok->str.size) {
    if (return_tail)
      return kit_http1_tok_tail(tok);
    else
      return (kit_str_t) { .size = 0, .values = NULL };
  }

  i64 previous_position = tok->position;
  tok->position         = hit + search.size;

  return (kit_str_t) { .size   = hit - previous_position,
                       .values = tok->str.values +
                                 previous_position };
}

//  TODO
//  - Return error status
static void kit_http1_uri_init(kit_http1_uri_t *uri, kit_str_t input,
                               kit_allocator_t *alloc) {
  static const kit_str_t port_80 = { .size = 2, .values = "80" };

  assert(uri != NULL);
  if (uri == NULL)
    return;

  memset(uri, 0, sizeof *uri);

  kit_http1_tok_t input_tok = { .str = input, .position = 0 };

  uri->protocol = kit_http1_tok_next(&input_tok, SZ("://"), 0);
  kit_str_t host_port_str = kit_http1_tok_next(&input_tok, SZ("/"),
                                               0);

  //  FIXME
  //  Handle invalid host port error
  assert(host_port_str.size > 0);
  if (host_port_str.size == 0)
    return;

  kit_http1_tok_t host_port_tok = { .str      = host_port_str,
                                    .position = 0 };

  uri->host = kit_http1_tok_next(
      &host_port_tok,
      host_port_str.values[0] == '[' ? SZ("]:") : SZ(":"), 1);

  if (uri->host.values[0] == '[')
    uri->host = (kit_str_t) { .size   = uri->host.size - 1,
                              .values = uri->host.values + 1 };

  //  TODO
  //  - HTTPS default port 443
  uri->port = kit_http1_tok_tail(&host_port_tok);
  if (uri->port.size == 0)
    uri->port = port_80;

  uri->address      = kit_http1_tok_next(&input_tok, SZ("?"), 1);
  uri->query_string = kit_http1_tok_next(&input_tok, SZ("#"), 1);

  uri->hash = kit_http1_tok_tail(&input_tok);

  KIT_DA_INIT(uri->parameters, 0, alloc);

  kit_http1_tok_t query_tok = {
    .str      = { .size   = uri->query_string.size,
                  .values = uri->query_string.values },
    .position = 0
  };

  for (;;) {
    kit_str_t key = kit_http1_tok_next(&query_tok, SZ("="), 0);
    if (key.size == 0)
      break;
    kit_str_t value = kit_http1_tok_next(&query_tok, SZ("&"), 1);

    i64 index = uri->parameters.size;

    KIT_DA_RESIZE(uri->parameters, index + 1);

    assert(uri->parameters.size == index + 1);
    if (uri->parameters.size != index + 1)
      //  FIXME
      //  Handle bad alloc error
      break;

    uri->parameters.values[index] = (kit_http1_str_pair_t) {
      .key = key, .value = value
    };
  }
}

static void kit_http1_uri_destroy(kit_http1_uri_t *uri) {
  assert(uri != NULL);
  if (uri == NULL)
    return;

  KIT_DA_DESTROY(uri->parameters);

  memset(uri, 0, sizeof *uri);
}

static kit_str_t kit_http1_method_to_str(i32 method) {
  static kit_str_t methods[] = { { .size = 7, .values = "OPTIONS" },
                                 { .size = 3, .values = "GET" },
                                 { .size = 4, .values = "HEAD" },
                                 { .size = 4, .values = "POST" },
                                 { .size = 3, .values = "PUT" },
                                 { .size = 6, .values = "DELETE" },
                                 { .size = 5, .values = "TRACE" },
                                 { .size = 7, .values = "CONNECT" } };

  assert(method >= 0 && method < sizeof methods / sizeof *methods);
  if (method < 0 || method >= sizeof methods / sizeof *methods)
    return (kit_str_t) { .size = 0, .values = NULL };

  return methods[method];
}

//  TODO
//  - Return error status
static socket_t kit_http1_connect_to_uri(kit_http1_uri_t *uri) {
  assert(uri != NULL);
  if (uri == NULL)
    return INVALID_SOCKET;

  struct addrinfo hints, *result, *rp;

  memset(&hints, 0, sizeof hints);

  hints.ai_family   = AF_UNSPEC;
  hints.ai_socktype = SOCK_STREAM;

  char host_str[128];
  char port_str[128];

  assert(uri->host.size < sizeof host_str);
  memcpy(host_str, uri->host.values, uri->host.size);
  host_str[uri->host.size] = '\0';

  assert(uri->port.size < sizeof port_str);
  memcpy(port_str, uri->port.values, uri->port.size);
  port_str[uri->port.size] = '\0';

  i32 getaddrinfo_result = getaddrinfo(host_str, port_str, &hints,
                                       &result);

  if (getaddrinfo_result != 0)
    return INVALID_SOCKET;

  socket_t fd = INVALID_SOCKET;

  for (rp = result; rp != NULL; rp = rp->ai_next) {
    fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);

    if (fd == INVALID_SOCKET)
      continue;

    i32 connect_result = connect(fd, rp->ai_addr,
                                 (socklen_t) rp->ai_addrlen);

    if (connect_result == -1) {
      closesocket(fd);
      fd = INVALID_SOCKET;
      continue;
    }

    break;
  }

  freeaddrinfo(result);

  return fd;
}

//  TODO
//  - Return error status
static kit_str_builder_t kit_http1_buffered_read(
    socket_t fd, kit_allocator_t *alloc) {
  kit_str_builder_t buf;
  KIT_DA_INIT(buf, KIT_HTTP1_BUFFER_SIZE, alloc);

  //  FIXME
  //  Handle bad alloc error
  assert(buf.size == KIT_HTTP1_BUFFER_SIZE);
  if (buf.size != KIT_HTTP1_BUFFER_SIZE)
    return buf;

  i64 offset = 0;

  for (;;) {
    i64 chunk = buf.size - offset;
    i64 n     = recv(fd, buf.values + offset, (socklen_t) chunk, 0);
    if (n < chunk)
      break;
    offset += n;
    KIT_DA_RESIZE(buf, offset + KIT_HTTP1_BUFFER_SIZE);

    //  FIXME
    //  Handle bad alloc error
    assert(buf.size == offset + KIT_HTTP1_BUFFER_SIZE);
    if (buf.size != offset + KIT_HTTP1_BUFFER_SIZE)
      break;
  }

  return buf;
}

//  TODO
//  - Return error status
static kit_http1_response_t kit_http1_request(
    i32 method, kit_http1_uri_t *uri, kit_str_t body,
    kit_allocator_t *alloc) {

  socket_t fd = kit_http1_connect_to_uri(uri);

  assert(fd != INVALID_SOCKET);
  if (fd == INVALID_SOCKET) {
    kit_http1_response_t res;
    memset(&res, 0, sizeof res);
    res.success = 0;
    return res;
  }

  kit_str_builder_t req;

  //  reserve
  KIT_DA_INIT(req, 512, alloc);

  assert(req.size == 512);
  if (req.size != 512) {
    //  bad alloc
    //

    closesocket(fd);

    kit_http1_response_t res;
    memset(&res, 0, sizeof res);
    res.success = 0;
    return res;
  }

  req.size = 0;

  //  FIXME
  //  Handle errors
  //

  kit_str_append(&req, kit_http1_method_to_str(method));
  kit_str_append(&req, SZ(" /"));
  kit_str_append(&req, uri->address);
  kit_str_append(&req,
                 uri->query_string.size == 0 ? SZ("") : SZ("?"));
  kit_str_append(&req, uri->query_string);
  kit_str_append(&req, SZ(" HTTP/1.1"));
  kit_str_append(&req, SZ(KIT_HTTP1_NEWLINE));

  kit_str_append(&req, SZ("Host: "));
  kit_str_append(&req, uri->host);
  kit_str_append(&req, SZ(":"));
  kit_str_append(&req, uri->port);
  kit_str_append(&req, SZ(KIT_HTTP1_NEWLINE));

  kit_str_append(&req, SZ("Accept: */*"));
  kit_str_append(&req, SZ(KIT_HTTP1_NEWLINE));

  kit_str_append(
      &req,
      SZ("Content-Type: text/plain; version=0.0.4; charset=utf-8"));
  kit_str_append(&req, SZ(KIT_HTTP1_NEWLINE));

  //  FIXME
  //  Use our own print instead
  char content_length_str[11];
  sprintf(content_length_str, "%lld", body.size);

  kit_str_append(&req, SZ("Content-Length: "));
  kit_str_append(&req,
                 (kit_str_t) { .size   = strlen(content_length_str),
                               .values = content_length_str });
  kit_str_append(&req, SZ(KIT_HTTP1_NEWLINE));

  kit_str_append(&req, SZ(KIT_HTTP1_NEWLINE));
  kit_str_append(&req, body);

  //  Append '\0' at the end
  //
  {
    i64 n = req.size;
    KIT_DA_RESIZE(req, n + 1);

    assert(req.size == n + 1);
    if (req.size != n + 1) {
      KIT_DA_DESTROY(req);
      closesocket(fd);

      kit_http1_response_t res;
      memset(&res, 0, sizeof res);
      res.success = 0;
      return res;
    }

    req.size      = n;
    req.values[n] = '\0';
  }

  i64 req_size = req.size;
  i64 n        = send(fd, req.values, (socklen_t) req.size, 0);

  KIT_DA_DESTROY(req);

  if (n != req_size) {
    //  send failed
    //

    closesocket(fd);

    kit_http1_response_t res;
    memset(&res, 0, sizeof res);
    res.success = 0;
    return res;
  }

  kit_str_builder_t buffer = kit_http1_buffered_read(fd, alloc);

  closesocket(fd);

  kit_http1_response_t res;
  memset(&res, 0, sizeof res);
  res.success = 1;

  kit_http1_tok_t buf_tok = { .str      = { .size   = buffer.size,
                                            .values = buffer.values },
                              .position = 0 };

  res.protocol = kit_str_build(
      kit_http1_tok_next(&buf_tok, SZ(KIT_HTTP1_SPACE), 0), alloc);
  res.response = kit_str_build(
      kit_http1_tok_next(&buf_tok, SZ(KIT_HTTP1_SPACE), 0), alloc);
  res.response_str = kit_str_build(
      kit_http1_tok_next(&buf_tok, SZ(KIT_HTTP1_NEWLINE), 0), alloc);

  res.header_str = kit_str_build(
      kit_http1_tok_next(&buf_tok,
                         SZ(KIT_HTTP1_NEWLINE KIT_HTTP1_NEWLINE), 0),
      alloc);

  res.body = kit_str_build(kit_http1_tok_tail(&buf_tok), alloc);

  kit_http1_tok_t header_tok = {
    .str      = (kit_str_t) { .size   = res.header_str.size,
                              .values = res.header_str.values },
    .position = 0
  };

  KIT_DA_INIT(res.header, 0, alloc);

  for (;;) {
    kit_str_t key = kit_http1_tok_next(
        &header_tok, SZ(KIT_HTTP1_HEADER_SEPARATOR), 0);
    if (key.size == 0)
      break;
    kit_str_t value = kit_http1_tok_next(&header_tok,
                                         SZ(KIT_HTTP1_NEWLINE), 1);

    i64 index = res.header.size;

    KIT_DA_RESIZE(res.header, index + 1);

    assert(res.header.size == index + 1);
    if (res.header.size != index + 1) {
      //  FIXME
      //  Handle bad alloc error
      res.success = 0;
      break;
    }

    res.header.values[index] = (kit_http1_str_pair_t) {
      .key = key, .value = value
    };
  }

  return res;
}

static void kit_http1_response_destroy(
    kit_http1_response_t *response) {
  assert(response != NULL);
  if (response == NULL)
    return;

  KIT_DA_DESTROY(response->protocol);
  KIT_DA_DESTROY(response->response);
  KIT_DA_DESTROY(response->response_str);
  KIT_DA_DESTROY(response->header);
  KIT_DA_DESTROY(response->header_str);
  KIT_DA_DESTROY(response->body);

  memset(response, 0, sizeof *response);
}

#ifdef __GNUC__
#  pragma GCC diagnostic pop
#endif

#ifdef __cplusplus
}
#endif

#endif