#include <fcntl.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>

// SDK
#include "esp_http_server.h"
#include "esp_log.h"
#include "esp_vfs.h"

#ifdef CONFIG_CALENDINK_DEPLOY_WEB_PAGES
#include "esp_littlefs.h"
#endif

// Project
#include "api/ota/bundle.cpp"
#include "api/ota/firmware.cpp"
#include "api/ota/frontend.cpp"
#include "api/ota/status.cpp"
#include "api/system/info.cpp"
#include "api/system/reboot.cpp"
#include "api/display/unity.cpp"
#include "api/devices/unity.cpp"
#include "api/tasks/unity.cpp"
#include "api/users/unity.cpp"

internal const char *TAG = "HTTP_SERVER";

constexpr uint8 kGZ_Extension_Length = sizeof(".gz") - 1;

#define FILE_PATH_MAX (ESP_VFS_PATH_MAX + 128)
#define SCRATCH_BUFSIZE 4096
#define MAX_SCRATCH_BUFFERS 10

typedef struct
{
  char *buffers[MAX_SCRATCH_BUFFERS];
  bool in_use[MAX_SCRATCH_BUFFERS];
} scratch_pool_t;

static scratch_pool_t global_scratch_pool = {};

char *get_scratch_buffer()
{
  for (int i = 0; i < MAX_SCRATCH_BUFFERS; i++)
  {
    if (!global_scratch_pool.in_use[i])
    {
      if (global_scratch_pool.buffers[i] == NULL)
      {
        global_scratch_pool.buffers[i] = (char *)malloc(SCRATCH_BUFSIZE);
        if (global_scratch_pool.buffers[i] == NULL)
        {
          ESP_LOGE(TAG, "Failed to allocate scratch buffer from heap!");
          return NULL;
        }
      }
      global_scratch_pool.in_use[i] = true;
      return global_scratch_pool.buffers[i];
    }
  }
  ESP_LOGE(TAG, "All scratch buffers in use! Increase MAX_SCRATCH_BUFFERS");
  return NULL;
}

void free_scratch_buffer(char *buffer)
{
  if (buffer == NULL)
    return;
  for (int i = 0; i < MAX_SCRATCH_BUFFERS; i++)
  {
    if (global_scratch_pool.buffers[i] == buffer)
    {
      global_scratch_pool.in_use[i] = false;
      return;
    }
  }
  ESP_LOGE(TAG, "Attempted to free unknown scratch buffer!");
}

#ifdef CONFIG_CALENDINK_DEPLOY_WEB_PAGES
// Set HTTP response content type according to file extension
internal esp_err_t set_content_type_from_file(httpd_req_t *req,
                                              const char *filepath)
{
  const char *type = "text/plain";
  if (strstr(filepath, ".html"))
  {
    type = "text/html";
  }
  else if (strstr(filepath, ".js"))
  {
    type = "application/javascript";
  }
  else if (strstr(filepath, ".css"))
  {
    type = "text/css";
  }
  else if (strstr(filepath, ".png"))
  {
    type = "image/png";
  }
  else if (strstr(filepath, ".ico"))
  {
    type = "image/x-icon";
  }
  else if (strstr(filepath, ".svg"))
  {
    type = "text/xml";
  }
  return httpd_resp_set_type(req, type);
}

// Handler to serve static files from LittleFS
internal esp_err_t static_file_handler(httpd_req_t *req)
{
  char filepath[FILE_PATH_MAX];

  // Construct real file path
  strlcpy(filepath, "/www", sizeof(filepath));
  if (req->uri[strlen(req->uri) - 1] == '/')
  {
    strlcat(filepath, "/index.html", sizeof(filepath));
  }
  else
  {
    strlcat(filepath, req->uri, sizeof(filepath));
  }

  // Default to index.html if file doesn't exist (SPA routing support)
  struct stat file_stat;
  if (stat(filepath, &file_stat) == -1)
  {
    // Try gzipped first, then fallback to index.html
    char filepath_gz[FILE_PATH_MAX + kGZ_Extension_Length];
    snprintf(filepath_gz, sizeof(filepath_gz), "%s.gz", filepath);
    if (stat(filepath_gz, &file_stat) == 0)
    {
      strlcpy(filepath, filepath_gz, sizeof(filepath));
      httpd_resp_set_hdr(req, "Content-Encoding", "gzip");
    }
    else
    {
      ESP_LOGW(TAG, "File not found: %s, falling back to index.html", filepath);
      snprintf(filepath, sizeof(filepath), "%s/index.html", "/www");
      if (stat(filepath, &file_stat) == -1)
      {
        // If index.html doesn't exist, try index.html.gz
        snprintf(filepath_gz, sizeof(filepath_gz), "%s/index.html.gz", "/www");
        if (stat(filepath_gz, &file_stat) == 0)
        {
          strlcpy(filepath, filepath_gz, sizeof(filepath));
          httpd_resp_set_hdr(req, "Content-Encoding", "gzip");
        }
        else
        {
          ESP_LOGE(TAG, "index.html not found too.");
          httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "File not found");
          return ESP_FAIL;
        }
      }
    }
  }
  else
  {
    // Since ESP32 handles .gz transparently if we tell it to via headers
    // If we requested explicitly a .gz file, set the header
    if (strstr(filepath, ".gz"))
    {
      httpd_resp_set_hdr(req, "Content-Encoding", "gzip");
    }
  }

  int fd = open(filepath, O_RDONLY, 0);
  if (fd == -1)
  {
    ESP_LOGE(TAG, "Failed to open file: %s", filepath);
    httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR,
                        "Failed to read existing file");
    return ESP_FAIL;
  }

  set_content_type_from_file(req, filepath);

  char *chunk = get_scratch_buffer();
  if (chunk == NULL)
  {
    close(fd);
    httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Server busy");
    return ESP_FAIL;
  }

  ssize_t read_bytes;

  do
  {
    read_bytes = read(fd, chunk, SCRATCH_BUFSIZE);
    if (read_bytes == -1)
    {
      ESP_LOGE(TAG, "Failed to read file: %s", filepath);
    }
    else if (read_bytes > 0)
    {
      if (httpd_resp_send_chunk(req, chunk, read_bytes) != ESP_OK)
      {
        close(fd);
        ESP_LOGE(TAG, "File sending failed!");
        free_scratch_buffer(chunk);
        httpd_resp_sendstr_chunk(req, NULL); // Abort sending
        return ESP_FAIL;
      }
    }
  } while (read_bytes > 0);

  close(fd);
  free_scratch_buffer(chunk);
  httpd_resp_send_chunk(req, NULL, 0); // End response

  return ESP_OK;
}
#endif

// Handler for CORS Preflight OPTIONS requests
internal esp_err_t cors_options_handler(httpd_req_t *req)
{
  httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
  httpd_resp_set_hdr(req, "Access-Control-Allow-Methods",
                     "GET, POST, DELETE, OPTIONS");
  httpd_resp_set_hdr(req, "Access-Control-Allow-Headers", "Content-Type");
  httpd_resp_set_status(req, "204 No Content");
  httpd_resp_send(req, NULL, 0);
  return ESP_OK;
}

internal httpd_handle_t start_webserver(void)
{
#ifdef CONFIG_CALENDINK_DEPLOY_WEB_PAGES
  esp_vfs_littlefs_conf_t conf = {};
  conf.base_path = "/www";
  conf.partition_label = g_Active_WWW_Partition == 0 ? "www_0" : "www_1";
  ESP_LOGI(TAG, "Mounting LittleFS partition: %s", conf.partition_label);
  conf.format_if_mount_failed = false;
  conf.dont_mount = false;
  esp_err_t ret = esp_vfs_littlefs_register(&conf);
  if (ret != ESP_OK)
  {
    if (ret == ESP_FAIL)
    {
      ESP_LOGE(TAG, "Failed to mount or format filesystem");
    }
    else if (ret == ESP_ERR_NOT_FOUND)
    {
      ESP_LOGE(TAG, "Failed to find LittleFS partition");
    }
    else
    {
      ESP_LOGE(TAG, "Failed to initialize LittleFS (%s)", esp_err_to_name(ret));
    }
  }
  else
  {
    ESP_LOGI(TAG, "LittleFS mounted on /www");
  }
#endif

  httpd_config_t config = HTTPD_DEFAULT_CONFIG();
  config.uri_match_fn = httpd_uri_match_wildcard;
  config.max_uri_handlers = 26;
  config.max_open_sockets = 24;
  config.lru_purge_enable = true;
  config.stack_size = 16384;

  httpd_handle_t server = NULL;
  ESP_LOGI(TAG, "Starting HTTP Server on port: '%d'", config.server_port);

  if (httpd_start(&server, &config) == ESP_OK)
  {
    // Register CORS OPTIONS handler for API routes
    httpd_uri_t cors_options_uri = {.uri = "/api/*",
                                    .method = HTTP_OPTIONS,
                                    .handler = cors_options_handler,
                                    .user_ctx = NULL};
    httpd_register_uri_handler(server, &cors_options_uri);

    // Register system API routes
    httpd_register_uri_handler(server, &api_system_info_uri);
    httpd_register_uri_handler(server, &api_system_reboot_uri);
    httpd_register_uri_handler(server, &api_display_image_uri);
    httpd_register_uri_handler(server, &api_ota_status_uri);
    httpd_register_uri_handler(server, &api_ota_frontend_uri);
    httpd_register_uri_handler(server, &api_ota_firmware_uri);
    httpd_register_uri_handler(server, &api_ota_bundle_uri);

    // Register todo list API routes
    httpd_register_uri_handler(server, &api_users_get_uri);
    httpd_register_uri_handler(server, &api_users_post_uri);
    httpd_register_uri_handler(server, &api_users_update_uri);
    httpd_register_uri_handler(server, &api_users_delete_uri);
    httpd_register_uri_handler(server, &api_tasks_upcoming_uri);
    httpd_register_uri_handler(server, &api_tasks_get_uri);
    httpd_register_uri_handler(server, &api_tasks_post_uri);
    httpd_register_uri_handler(server, &api_tasks_update_uri);
    httpd_register_uri_handler(server, &api_tasks_delete_uri);

    // Register device API routes
    httpd_register_uri_handler(server, &api_devices_get_uri);
    httpd_register_uri_handler(server, &api_devices_register_uri);
    httpd_register_uri_handler(server, &api_devices_layout_uri);
    httpd_register_uri_handler(server, &api_devices_screen_info_uri);
    httpd_register_uri_handler(server, &api_devices_screen_image_uri);

    // Populate dummy data for development (debug builds only)
#ifndef NDEBUG
    seed_users();
    seed_tasks();
#endif

#ifdef CONFIG_CALENDINK_DEPLOY_WEB_PAGES
    // Register static file handler last as a catch-all wildcard if deployed
    httpd_uri_t static_get_uri = {.uri = "/*",
                                  .method = HTTP_GET,
                                  .handler = static_file_handler,
                                  .user_ctx = NULL};
    httpd_register_uri_handler(server, &static_get_uri);
#endif

    return server;
  }

  ESP_LOGE(TAG, "Error starting server!");
  return NULL;
}

internal void stop_webserver(httpd_handle_t server, uint8_t partition_index)
{
  if (server)
  {
    httpd_stop(server);
#ifdef CONFIG_CALENDINK_DEPLOY_WEB_PAGES
    esp_vfs_littlefs_unregister(partition_index == 0 ? "www_0" : "www_1");
#endif
  }
}
