#include "tor_ctl.h"
#include "timer.h"

struct os_connect {
  char host[256];
  char port[6];
  char url[512];
};

enum DONAR_TIMER_DECISION on_os_connect_timeout(struct evt_core_ctx* ctx, void* user_data) {
  struct os_connect *oc = user_data;
  struct evt_core_fdinfo newfdinfo;
  struct evt_core_cat newcat;
  newfdinfo.cat = &newcat;
  newfdinfo.url = oc->url;

  int fd = create_tcp_client(oc->host, oc->port);
  if (fd <= 0) return DONAR_TIMER_CONTINUE;

  newfdinfo.fd = fd;
  newfdinfo.cat->name = "torctl-server-success";
  struct evt_core_fdinfo *reg = evt_core_add_fd (ctx, &newfdinfo);

  printf("[%s][torctl] onion service %s up (cat: %s, fd: %d)\n", current_human_datetime (), reg->url, reg->cat->name, reg->fd);
  free(oc);
  return DONAR_TIMER_STOP;
}

int tor_ctl_connect(struct tor_ctl* ctx, char* addr, char* service) {
  int sock = create_tcp_client (addr, service);
  int sock2 = dup(sock);
  ctx->rsock = NULL;
  ctx->wsock = NULL;

  ctx->rsock = fdopen(sock, "r");
  if (ctx->rsock == NULL) {
    return -1;
  }
  setbuf(ctx->rsock, NULL);

  ctx->wsock = fdopen(sock2, "w");
  if (ctx->wsock == NULL) {
    return -1;
  }
  setbuf(ctx->wsock, NULL);

  fprintf (ctx->wsock, "authenticate \"\"\n");
  int error_code = 0;
  fscanf (ctx->rsock, "%d", &error_code);
  if (error_code != 250) {
    tor_ctl_close (ctx);
    return -1;
  }
  fscanf(ctx->rsock," OK");

  return 0;
}

void tor_ctl_list_onions(struct tor_ctl* ctx) {
  fprintf(ctx->wsock, "getinfo onions/current\n");

  char delimiter, buffer[1024] = {0};
  fscanf(ctx->rsock, " 250%c", &delimiter);
  if (delimiter == '-') {
    fscanf(ctx->rsock, "onions/current=%s", buffer);
    if (strlen(buffer) > 0) printf("key: %s\n", buffer);
  } else if (delimiter == '+') {
    fscanf(ctx->rsock, "onions/current= ");
    while (1) {
      fgets(buffer, 1024, ctx->rsock);
      if (strcmp(buffer, "250 OK\r\n") == 0) break;
      if (strcmp(buffer, ".\r\n") == 0) continue;
      printf("line: %s", buffer);
    }
  } else {
    printf("deli:%c\n", delimiter);
  }
}

int tor_ctl_add_onion(struct tor_ctl* ctx, struct tor_os_str* tos, uint16_t* port, uint64_t port_per_os, enum TOR_ONION_FLAGS flags) {
  int err = 0;
  char buffer1[1024] = {0};
  char buffer2[1024] = {0};
  int to_create = tos->size - tos->filled;

  /* Add onion services loaded from file */
  for (int i = 0; i < tos->filled; i++) {
    fprintf(ctx->wsock, "add_onion %s ", tos->keys[i].priv);
    for (int j = 0; j < port_per_os; j++) {
      fprintf(ctx->wsock, "Port=%d,%s:%d ", port[i*port_per_os+j], ctx->os_endpoint, port[i*port_per_os+j]);
    }
    if (flags == TOR_ONION_FLAG_NONE) fprintf(ctx->wsock, "\n");
    else {
      fprintf(ctx->wsock, "Flags=");
      if (flags & TOR_ONION_FLAG_NON_ANONYMOUS) fprintf(ctx->wsock, "NonAnonymous,");
      fprintf(ctx->wsock, "\n");
    }

    fscanf(ctx->rsock, "%d", &err);
    if (err != 250) {
      printf("err: %d\n", err);
      return -1;
    }
    fscanf(ctx->rsock, "-ServiceID=%s\n", buffer1);
    printf("Added onion service %s.onion from file\n", buffer1);
    fscanf(ctx->rsock, "250 OK");
  }

  /* Complete by creating new onion services */
  for (int i = tos->filled; i < tos->size; i++) {
    fprintf(ctx->wsock, "add_onion NEW:ED25519-V3 ");
    for (int j = 0; j < port_per_os; j++) {
      fprintf(ctx->wsock, "Port=%d,%s:%d ", port[i*port_per_os+j], ctx->os_endpoint, port[i*port_per_os+j]);
    }
    if (flags == TOR_ONION_FLAG_NONE) fprintf(ctx->wsock, "\n");
    else {
      fprintf(ctx->wsock, "Flags=");
      if (flags & TOR_ONION_FLAG_NON_ANONYMOUS) fprintf(ctx->wsock, "NonAnonymous,");
      fprintf(ctx->wsock, "\n");
    }

    //fprintf(ctx->wsock, "add_onion NEW:RSA1024 Port=%d\n", port[i]);

    err = 0;
    fscanf(ctx->rsock, "%d", &err);

    if (err != 250) {
      fprintf(stderr, "Got error %d instead of 250\n", err);
      return -2;
    }
    err = fscanf(ctx->rsock, "-ServiceID=%s\n", buffer1);
    if (err <= 0) return -3;
    printf("Created onion service %s.onion\n", buffer1);
    err = fscanf(ctx->rsock, "250-PrivateKey=%s\n", buffer2);
    if (err <= 0) return -4;
    //printf("Onion service private key: %s\n", buffer);
    if (tor_os_append(tos, buffer1, buffer2) != 0) return -5;
    err = fscanf(ctx->rsock, "250 OK");
    if (err < 0) return -6;
  }

  if (to_create > 0) {
    tor_os_persist(tos);
  }

  return 0;
}

void tor_ctl_close(struct tor_ctl* ctx) {
  fclose(ctx->rsock);
  fclose(ctx->wsock);
}

int on_torctl_server_auth_read(struct evt_core_ctx* ctx, struct evt_core_fdinfo* fdinfo) {
  char *expected = "authenticate \"\"\n";
  size_t to_read = strlen(expected);
  char buffer[128] = {0};
  ssize_t nread = recv(fdinfo->fd, buffer, to_read, MSG_PEEK);
  if (nread == -1 && errno == EAGAIN) return EVT_CORE_FD_EXHAUSTED;
  if (nread != to_read) return EVT_CORE_FD_EXHAUSTED;

  recv(fdinfo->fd, buffer, to_read, 0);
  if (strstr(buffer, "authenticate") == NULL) {
    fprintf(stderr, "Unable to find string 'authenticate' in receveived command: '%s'\n", buffer);
    exit(EXIT_FAILURE);
  }

  evt_core_mv_fd2 (ctx, fdinfo, "torctl-server-auth-write");
  return EVT_CORE_FD_EXHAUSTED;
}

int on_torctl_server_auth_write(struct evt_core_ctx* ctx, struct evt_core_fdinfo* fdinfo) {
  char *response = "250 OK\r\n";
  ssize_t nwrite = send(fdinfo->fd, response, sizeof(response), 0);
  if (nwrite != sizeof(response)) {
    perror("@FIXME: Unproper handling of sockets in torctl_server_auth_write.");
    exit(EXIT_FAILURE);
  }
  evt_core_mv_fd2 (ctx, fdinfo, "torctl-server-add-onion-read");
  return EVT_CORE_FD_EXHAUSTED;
}

int on_torctl_server_add_onion_read(struct evt_core_ctx* ctx, struct evt_core_fdinfo* fdinfo) {
  char buffer[1024] = {0};
  char *strtok_ptr, *str_target, *token;
  int i, port1;
  char *service_id = "iu7aeep42k5ky3fwcfag5el2raelfcwuilsstqhcz3c6bmxilr2nuayd.onion"; //@FIXME hardcoded url

  ssize_t nread = recv(fdinfo->fd, buffer, sizeof(buffer), MSG_PEEK);
  if (nread == -1 && errno == EAGAIN) return EVT_CORE_FD_EXHAUSTED;
  if (nread == -1) {
    perror("an error occured...");
    exit(EXIT_FAILURE);
  }
  if (buffer[nread-1] != '\n') return EVT_CORE_FD_EXHAUSTED;
  nread = recv(fdinfo->fd, buffer, sizeof(buffer), 0);
  printf("[%s][torctl] Received command: %s\n", current_human_datetime (), buffer);

  for (i = 0, str_target = buffer ; ; str_target = NULL, i++) {
    token = strtok_r(str_target, " ", &strtok_ptr);
    if (i == 0 && (token == NULL || strcmp(token, "add_onion") != 0)) {
      fprintf(stderr, "wrong command. expected 'add_onion' but got '%s'\n", token);
      exit(EXIT_FAILURE);
    }
    if (i == 1 && token == NULL) {
      fprintf(stderr, "command add_onion requires at least one parameter, specifying key algo\n");
      exit(EXIT_FAILURE);
    }
    if (i < 2) continue;
    if (token == NULL) break;

    struct os_connect *oc = malloc(sizeof(struct os_connect));
    int captured = sscanf(token, "Port=%d,%[^:]:%s", &port1, oc->host, oc->port);
    if (captured != 3) {
      free(oc);
      continue;
    }

    sprintf(oc->url, "torctl:%s:%d", service_id, port1);
    printf("[%s][torctl] will create onion service %s:%d <-> %s:%s in background (%s)\n", current_human_datetime (), service_id, port1, oc->host, oc->port, oc->url);
    set_timeout(ctx, 100, oc, on_os_connect_timeout);
  }

  evt_core_mv_fd2 (ctx, fdinfo, "torctl-server-add-onion-write");
  return EVT_CORE_FD_EXHAUSTED;
}

int on_torctl_server_add_onion_write(struct evt_core_ctx* ctx, struct evt_core_fdinfo* fdinfo) {
  //@FIXME hardcoded response
  char *answer = "250-ServiceID=iu7aeep42k5ky3fwcfag5el2raelfcwuilsstqhcz3c6bmxilr2nuayd\r\n250-PrivateKey=ED25519-V3:ULk3Q/TFqngKCDDzeM93YC80IDOjz13PKTx718UjE0Svf+u/QZmN9EHzUCqCa1ZkNAXSQJIzcOVeJ8OL8Zg5Xg==\r\n250 OK\r\n";

  ssize_t nwrite;
  nwrite = send(fdinfo->fd, answer, strlen(answer), 0);
  if (nwrite != strlen(answer)) goto error;

  printf("[%s][torctl] Sent add-onion reply\n", current_human_datetime ());
  evt_core_mv_fd2(ctx,fdinfo,"torctl-server-add-onion-read");
  return EVT_CORE_FD_EXHAUSTED;

error:
  perror("@FIXME: unproper handling of non blocking sockets, you have been bitten in torctl_server_add_onion_write\n");
  exit(EXIT_FAILURE);
}

void tor_ctl_server_init(struct evt_core_ctx *ctx) {
  struct evt_core_cat template = {0};

  template.cb = on_torctl_server_auth_read;
  template.err_cb = NULL;
  template.name = "torctl-server-auth-read";
  template.flags = EPOLLIN | EPOLLET;
  evt_core_add_cat (ctx, &template);

  template.cb = on_torctl_server_auth_write;
  template.err_cb = NULL;
  template.name = "torctl-server-auth-write";
  template.flags = EPOLLOUT | EPOLLET;
  evt_core_add_cat (ctx, &template);

  template.cb = on_torctl_server_add_onion_read;
  template.err_cb = NULL;
  template.name = "torctl-server-add-onion-read";
  template.flags = EPOLLIN | EPOLLET;
  evt_core_add_cat (ctx, &template);

  template.cb = on_torctl_server_add_onion_write;
  template.err_cb = NULL;
  template.name = "torctl-server-add-onion-write";
  template.flags = EPOLLOUT | EPOLLET;
  evt_core_add_cat (ctx, &template);
}

void tor_ctl_server_handle(struct evt_core_ctx *ctx, int fd) {
  struct evt_core_fdinfo *reg_fdinfo;
  struct evt_core_fdinfo fdinfo;
  struct evt_core_cat cat;
  char url[256];

  fdinfo.cat = &cat;
  fdinfo.cat->name = "torctl-server-auth-read";
  fdinfo.fd = fd;
  fdinfo.other = NULL;
  fdinfo.free_other = NULL;
  sprintf(url, "tor-ctl-server:%d", fd);
  fdinfo.url = url;

  reg_fdinfo = evt_core_add_fd (ctx, &fdinfo);
}
