#include "../beaker.h"
#include "beaker_globals.h"
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <unistd.h>

void set_handler(const char *path, RequestHandler handler) {

  if (handler_count < MAX_HANDLERS) {

    strncpy(handlers[handler_count].path, path, MAX_PATH_LEN - 1);
    handlers[handler_count].path[MAX_PATH_LEN - 1] = '\0';

    handlers[handler_count].handler = handler;

    handler_count++;
  } else {

    beaker_log("WARN",
               "set_handler: Maximum number of handlers reached. Cannot "
               "register handler for '%s'.\n",
               path);
  }
}

const char *get_mime_type(const char *file_path) {

  const char *ext = strrchr(file_path, '.');
  if (!ext) {
    return "application/octet-stream";
  }
  ext++;

  if (strcmp(ext, "html") == 0 || strcmp(ext, "htm") == 0)
    return "text/html";
  if (strcmp(ext, "css") == 0)
    return "text/css";
  if (strcmp(ext, "js") == 0)
    return "application/javascript";
  if (strcmp(ext, "json") == 0)
    return "application/json";
  if (strcmp(ext, "jpg") == 0 || strcmp(ext, "jpeg") == 0)
    return "image/jpeg";
  if (strcmp(ext, "png") == 0)
    return "image/png";
  if (strcmp(ext, "gif") == 0)
    return "image/gif";
  if (strcmp(ext, "ico") == 0)
    return "image/x-icon";
  if (strcmp(ext, "svg") == 0)
    return "image/svg+xml";
  if (strcmp(ext, "pdf") == 0)
    return "application/pdf";
  if (strcmp(ext, "txt") == 0)
    return "text/plain";

  return "application/octet-stream";
}

static int canonicalize_path(char *canonical, const char *path,
                             size_t max_len) {
  char components[MAX_URL_PARAMS][MAX_KEY_LEN];
  int component_count = 0;

  char *path_copy = strdup(path);
  if (!path_copy) {
    return -1;
  }

  char *token = strtok(path_copy, "/");
  while (token) {
    if (strcmp(token, ".") == 0) {

    } else if (strcmp(token, "..") == 0) {

      if (component_count > 0) {
        component_count--;
      } else {

        beaker_log("SECURITY", "Path traversal attempt: %s\n", path);
        free(path_copy);
        return -1;
      }
    } else if (strlen(token) > 0) {

      if (component_count < MAX_URL_PARAMS) {
        strncpy(components[component_count], token, MAX_KEY_LEN - 1);
        components[component_count][MAX_KEY_LEN - 1] = '\0';
        component_count++;
      }
    }
    token = strtok(NULL, "/");
  }

  free(path_copy);

  canonical[0] = '\0';
  for (int i = 0; i < component_count; i++) {
    if (strlen(canonical) + strlen(components[i]) + 2 > max_len) {
      beaker_log("ERROR", "Canonical path too long\n");
      return -1;
    }
    strcat(canonical, "/");
    strcat(canonical, components[i]);
  }

  if (canonical[0] == '\0') {
    strcpy(canonical, "/");
  }

  return 0;
}

static bool is_safe_path_component(const char *component) {

  if (strstr(component, "..") || strstr(component, "//")) {
    return false;
  }

  for (size_t i = 0; component[i]; i++) {
    if (component[i] < 32 && component[i] != '\t') {
      return false;
    }
  }

  return true;
}

static int url_decode(char *dst, const char *src, size_t dst_size) {
  size_t i = 0, j = 0;

  while (src[i] && j < dst_size - 1) {
    if (src[i] == '%') {

      if (src[i + 1] && src[i + 2]) {
        char hex[3] = {src[i + 1], src[i + 2], '\0'};
        char *endptr;
        long value = strtol(hex, &endptr, 16);

        if (*endptr != '\0') {
          beaker_log("SECURITY", "Invalid URL encoding: %%%s\n", hex);
          return -1;
        }

        if (value == 0) {
          beaker_log("SECURITY", "Null byte in URL encoding\n");
          return -1;
        }

        dst[j++] = (char)value;
        i += 3;
      } else {
        beaker_log("SECURITY", "Incomplete URL encoding at end\n");
        return -1;
      }
    } else if (src[i] == '+') {

      dst[j++] = ' ';
      i++;
    } else {
      dst[j++] = src[i++];
    }
  }

  dst[j] = '\0';
  return 0;
}

char *parse_request_url(const char *request_line, UrlParams *params) {
  char method[16];
  char raw_url_full[MAX_PATH_LEN];
  char http_version[16];

  if (sscanf(request_line, "%15s %255s %15s", method, raw_url_full,
             http_version) != 3) {
    beaker_log("ERROR", "parse_request_url: Malformed request line\n");
    return NULL;
  }

  params->count = 0;

  char *working_raw = strdup(raw_url_full);
  if (!working_raw) {
    beaker_log_errno("Failed to allocate memory for URL copy");
    return NULL;
  }

  char *query_start = strchr(working_raw, '?');
  if (query_start) {
    *query_start = '\0';
  }

  char decoded_path[MAX_PATH_LEN];
  if (url_decode(decoded_path, working_raw, sizeof(decoded_path)) != 0) {
    beaker_log("SECURITY", "Invalid URL encoding in path\n");
    free(working_raw);
    return NULL;
  }

  char canonical_path[MAX_PATH_LEN];
  if (canonicalize_path(canonical_path, decoded_path, sizeof(canonical_path)) !=
      0) {
    beaker_log("SECURITY", "Path canonicalization failed\n");
    free(working_raw);
    return NULL;
  }

  char *path_check = strdup(canonical_path);
  if (path_check) {
    char *token = strtok(path_check, "/");
    while (token) {
      if (!is_safe_path_component(token)) {
        beaker_log("SECURITY", "Unsafe path component: %s\n", token);
        free(path_check);
        free(working_raw);
        return NULL;
      }
      token = strtok(NULL, "/");
    }
    free(path_check);
  }

  if (query_start) {
    char *query_string = query_start + 1;

    char *pair;
    char *saveptr;

    pair = strtok_r(query_string, "&", &saveptr);
    while (pair && params->count < MAX_URL_PARAMS) {
      char *equals = strchr(pair, '=');
      if (equals) {
        *equals = '\0';

        char *raw_key = pair;
        char *raw_val = equals + 1;

        if (url_decode(params->params[params->count].key, raw_key,
                       MAX_KEY_LEN) == 0 &&
            url_decode(params->params[params->count].value, raw_val,
                       MAX_VALUE_LEN) == 0) {
          params->count++;
        }
      }
      pair = strtok_r(NULL, "&", &saveptr);
    }
  }

  char *final_path = strdup(canonical_path);
  free(working_raw);
  return final_path;
}

bool serve_static_file_with_mime(const char *request_path_relative_to_static,
                                 const char *mime_type) {
  char full_static_path[MAX_PATH_LEN];

  if (request_path_relative_to_static == NULL ||
      strlen(request_path_relative_to_static) == 0) {
    beaker_log("ERROR", "serve_static_file_with_mime: Empty path provided\n");
    return false;
  }

  if (strstr(request_path_relative_to_static, "..") != NULL ||
      strstr(request_path_relative_to_static, "//") != NULL ||
      request_path_relative_to_static[0] == '/') {
    beaker_log("SECURITY", "Attempted directory traversal: %s\n",
               request_path_relative_to_static);
    send_status("403 Forbidden");
    return true;
  }

  snprintf(full_static_path, sizeof(full_static_path), "%s%s", STATIC_DIR,
           request_path_relative_to_static);

  FILE *fp = fopen(full_static_path, "rb");
  if (fp == NULL) {
    beaker_log("ERROR",
               "serve_static_file_with_mime: File '%s' not found or could not "
               "be opened. %s\n",
               full_static_path, strerror(errno));
    return false;
  }

  struct stat st;

  if (fstat(fileno(fp), &st) < 0) {
    beaker_log_errno_format(
        "ERROR", "serve_static_file_with_mime: fstat failed for '%s'.\n",
        full_static_path);
    fclose(fp);
    send_status("500 Internal Server Error");
    return true;
  }

  long file_size = st.st_size;
  current_response_status = 200;
  current_response_size = file_size;

  if (!mime_type || mime_type[0] == '\0') {
    mime_type = get_mime_type(full_static_path);
  }

  char http_header[BUFFER_SIZE];

  snprintf(http_header, sizeof(http_header),
           "HTTP/1.1 200 OK\r\n"
           "Content-Type: %s\r\n"
           "Content-Length: %ld\r\n"
           "Connection: close\r\n"
           "\r\n",
           mime_type, file_size);

  if (send(current_client_socket, http_header, strlen(http_header), 0) < 0) {
    beaker_log_errno_format(
        "ERROR",
        "serve_static_file_with_mime: Failed to send header for '%s'.\n",
        full_static_path);
    fclose(fp);
    return true;
  }

  char file_buffer[BUFFER_SIZE];
  size_t bytes_read;
  bool send_error = false;

  while (!send_error && !feof(fp) && !ferror(fp) &&
         (bytes_read = fread(file_buffer, 1, sizeof(file_buffer), fp)) > 0) {
    if (send(current_client_socket, file_buffer, bytes_read, 0) < 0) {
      beaker_log_errno_format(
          "ERROR",
          "serve_static_file_with_mime: Failed to send content for '%s'.\n",
          full_static_path);
      send_error = true;
    }
  }

  if (ferror(fp)) {
    beaker_log_errno_format(
        "ERROR", "serve_static_file_with_mime: Failed to read '%s'.\n",
        full_static_path);
    fclose(fp);
    return true;
  }

  fclose(fp);
  return true;
}

bool serve_static_file(const char *request_path_relative_to_static) {
  return serve_static_file_with_mime(request_path_relative_to_static, NULL);
}

bool serve_data(const char *data, size_t size, const char *mime_type) {

  if (current_client_socket == -1) {
    beaker_log("ERROR",
               "serve_data: No client socket set. Cannot send data.\n");
    return false;
  }

  if (data == NULL || size == 0) {
    beaker_log("ERROR", "serve_data: Invalid data or size.\n");
    return false;
  }

  char http_header[BUFFER_SIZE];
  current_response_status = 200;
  current_response_size = size;

  snprintf(http_header, sizeof(http_header),
           "HTTP/1.1 200 OK\r\n"
           "Content-Type: %s\r\n"
           "Content-Length: %zu\r\n"
           "Connection: close\r\n"
           "\r\n",
           mime_type, size);

  if (send(current_client_socket, http_header, strlen(http_header), 0) < 0) {
    beaker_log_errno_format("ERROR", "serve_data: Failed to send header.\n");
    return false;
  }

  size_t bytes_sent = 0;
  while (bytes_sent < size) {
    size_t chunk =
        (size - bytes_sent > BUFFER_SIZE) ? BUFFER_SIZE : (size - bytes_sent);
    if (send(current_client_socket, data + bytes_sent, chunk, 0) < 0) {
      beaker_log_errno_format("ERROR", "serve_data: Failed to send content.\n");
      return false;
    }
    bytes_sent += chunk;
  }

  return true;
}
