#define _POSIX_C_SOURCE 200809L

#include "tools_internal.h"

#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <sys/wait.h>
#include <unistd.h>

#include "cjson/cJSON.h"

#include "../../nostr_core_lib/nostr_core/nostr_http.h"

static char* json_error_local(const char* msg) {
    cJSON* root = cJSON_CreateObject();
    if (!root) return NULL;
    cJSON_AddBoolToObject(root, "success", 0);
    cJSON_AddStringToObject(root, "error", msg ? msg : "unknown error");
    char* out = cJSON_PrintUnformatted(root);
    cJSON_Delete(root);
    return out;
}

static cJSON* parse_args_local(const char* args_json) {
    const char* raw = args_json ? args_json : "{}";
    cJSON* args = cJSON_Parse(raw);
    if (!args || !cJSON_IsObject(args)) {
        cJSON_Delete(args);
        return NULL;
    }
    return args;
}

static int is_safe_relative_path_local(const char* path) {
    if (!path || path[0] == '\0') return 0;
    if (path[0] == '/') return 0;
    if (strstr(path, "..") != NULL) return 0;
    if (strchr(path, '\\') != NULL) return 0;
    return 1;
}

static int build_tool_path_local(tools_context_t* ctx, const char* rel_path, char* out, size_t out_size) {
    if (!ctx || !ctx->cfg || !rel_path || !out || out_size == 0) return -1;
    if (!is_safe_relative_path_local(rel_path)) return -1;

    const char* cwd = ctx->cfg->tools.shell.working_directory[0] != '\0'
                          ? ctx->cfg->tools.shell.working_directory
                          : ".";

    int n = 0;
    if (strcmp(cwd, ".") == 0) {
        n = snprintf(out, out_size, "%s", rel_path);
    } else {
        n = snprintf(out, out_size, "%s/%s", cwd, rel_path);
    }

    if (n < 0 || (size_t)n >= out_size) return -1;
    return 0;
}

static char* shell_quote_single_local(const char* in) {
    if (!in) return NULL;

    size_t len = strlen(in);
    size_t extra = 2U;
    for (size_t i = 0; i < len; i++) {
        if (in[i] == '\'') {
            extra += 4U;
        } else {
            extra += 1U;
        }
    }

    char* out = (char*)malloc(extra + 1U);
    if (!out) return NULL;

    size_t j = 0;
    out[j++] = '\'';
    for (size_t i = 0; i < len; i++) {
        if (in[i] == '\'') {
            out[j++] = '\'';
            out[j++] = '\\';
            out[j++] = '\'';
            out[j++] = '\'';
        } else {
            out[j++] = in[i];
        }
    }
    out[j++] = '\'';
    out[j] = '\0';

    return out;
}

char* execute_local_http_fetch(tools_context_t* ctx, const char* args_json) {
    if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable");

    cJSON* args = parse_args_local(args_json);
    if (!args) return json_error_local("invalid arguments JSON");

    cJSON* url = cJSON_GetObjectItemCaseSensitive(args, "url");
    cJSON* method = cJSON_GetObjectItemCaseSensitive(args, "method");
    cJSON* headers = cJSON_GetObjectItemCaseSensitive(args, "headers");
    cJSON* body = cJSON_GetObjectItemCaseSensitive(args, "body");
    cJSON* timeout = cJSON_GetObjectItemCaseSensitive(args, "timeout_seconds");
    cJSON* maxb = cJSON_GetObjectItemCaseSensitive(args, "max_bytes");

    if (!url || !cJSON_IsString(url) || !url->valuestring || url->valuestring[0] == '\0') {
        cJSON_Delete(args);
        return json_error_local("local_http_fetch requires string url");
    }
    if (headers && !cJSON_IsArray(headers)) {
        cJSON_Delete(args);
        return json_error_local("local_http_fetch headers must be an array when provided");
    }
    if (body && !cJSON_IsString(body)) {
        cJSON_Delete(args);
        return json_error_local("local_http_fetch body must be a string when provided");
    }

    const char* method_str = (method && cJSON_IsString(method) && method->valuestring && method->valuestring[0] != '\0')
                                 ? method->valuestring
                                 : "GET";

    if (body && cJSON_IsString(body) && body->valuestring && strcasecmp(method_str, "GET") == 0) {
        cJSON_Delete(args);
        return json_error_local("local_http_fetch GET requests cannot include body");
    }

    int default_timeout = ctx->cfg->tools.local_http_fetch_default_timeout_seconds > 0
                              ? ctx->cfg->tools.local_http_fetch_default_timeout_seconds
                              : 20;
    int max_timeout = ctx->cfg->tools.local_http_fetch_max_timeout_seconds > 0
                          ? ctx->cfg->tools.local_http_fetch_max_timeout_seconds
                          : 120;
    if (default_timeout > max_timeout) {
        default_timeout = max_timeout;
    }

    int timeout_seconds = (timeout && cJSON_IsNumber(timeout)) ? (int)timeout->valuedouble : default_timeout;
    if (timeout_seconds <= 0) timeout_seconds = default_timeout;
    if (timeout_seconds > max_timeout) timeout_seconds = max_timeout;

    int hard_max = ctx->cfg->tools.shell.max_output_bytes > 0 ? ctx->cfg->tools.shell.max_output_bytes : 65536;
    int max_bytes = (maxb && cJSON_IsNumber(maxb)) ? (int)maxb->valuedouble : hard_max;
    if (max_bytes <= 0 || max_bytes > hard_max) max_bytes = hard_max;

    const char* default_header = "Accept: */*";
    int hdr_count = 1;
    if (headers && cJSON_IsArray(headers)) {
        hdr_count += cJSON_GetArraySize(headers);
    }

    const char** req_headers = (const char**)calloc((size_t)hdr_count + 1U, sizeof(const char*));
    if (!req_headers) {
        cJSON_Delete(args);
        return json_error_local("local_http_fetch allocation failure");
    }
    int hi = 0;
    req_headers[hi++] = default_header;
    if (headers && cJSON_IsArray(headers)) {
        int n = cJSON_GetArraySize(headers);
        for (int i = 0; i < n; i++) {
            cJSON* h = cJSON_GetArrayItem(headers, i);
            if (h && cJSON_IsString(h) && h->valuestring && h->valuestring[0] != '\0') {
                req_headers[hi++] = h->valuestring;
            }
        }
    }
    req_headers[hi] = NULL;

    nostr_http_request_t req;
    memset(&req, 0, sizeof(req));
    req.method = method_str;
    req.url = url->valuestring;
    req.headers = req_headers;
    req.timeout_seconds = timeout_seconds;
    req.max_response_bytes = (size_t)max_bytes;
    req.follow_redirects = 1;
    req.max_redirects = 3;
    req.user_agent = "didactyl/local_http_fetch";
    if (body && cJSON_IsString(body) && body->valuestring) {
        req.body = (const unsigned char*)body->valuestring;
        req.body_len = strlen(body->valuestring);
    }

    nostr_http_response_t resp;
    int rc = nostr_http_request(&req, &resp);

    cJSON* out = cJSON_CreateObject();
    if (!out) {
        free(req_headers);
        cJSON_Delete(args);
        if (rc == NOSTR_SUCCESS) {
            nostr_http_response_free(&resp);
        }
        return NULL;
    }

    int http_ok = (rc == NOSTR_SUCCESS && resp.status_code >= 200 && resp.status_code < 300) ? 1 : 0;
    int success = http_ok ? 1 : 0;

    cJSON_AddBoolToObject(out, "success", success);
    cJSON_AddStringToObject(out, "url", url->valuestring);
    cJSON_AddStringToObject(out, "method", method_str);
    cJSON_AddNumberToObject(out, "status_code", rc == NOSTR_SUCCESS ? resp.status_code : 0);
    cJSON_AddBoolToObject(out, "http_ok", http_ok);
    cJSON_AddBoolToObject(out, "truncated", (rc == NOSTR_SUCCESS && resp.truncated) ? 1 : 0);
    cJSON_AddNumberToObject(out, "bytes_received", rc == NOSTR_SUCCESS ? (double)resp.body_len : 0.0);

    if (rc == NOSTR_SUCCESS && resp.content_type && resp.content_type[0] != '\0') {
        cJSON_AddStringToObject(out, "content_type", resp.content_type);
    }

    if (rc != NOSTR_SUCCESS) {
        cJSON_AddStringToObject(out, "curl_error", "request failed");
    }

    cJSON_AddStringToObject(out, "body", (rc == NOSTR_SUCCESS && resp.body) ? resp.body : "");

    if (rc == NOSTR_SUCCESS) {
        nostr_http_response_free(&resp);
    }

    char* json = cJSON_PrintUnformatted(out);
    cJSON_Delete(out);
    cJSON_Delete(args);
    free(req_headers);
    return json;
}

char* execute_local_shell_exec(tools_context_t* ctx, const char* args_json) {
    if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable");
    if (!ctx->cfg->tools.shell.enabled) return json_error_local("shell tool disabled");

    cJSON* args = parse_args_local(args_json);
    if (!args) return json_error_local("invalid arguments JSON");

    cJSON* command = cJSON_GetObjectItemCaseSensitive(args, "command");
    if (!command || !cJSON_IsString(command) || !command->valuestring || command->valuestring[0] == '\0') {
        cJSON_Delete(args);
        return json_error_local("local_shell_exec requires string command");
    }

    const char* cwd = ctx->cfg->tools.shell.working_directory[0] != '\0'
                          ? ctx->cfg->tools.shell.working_directory
                          : ".";
    int timeout_s = ctx->cfg->tools.shell.timeout_seconds > 0 ? ctx->cfg->tools.shell.timeout_seconds : 30;

    char* quoted_cwd = shell_quote_single_local(cwd);
    char* quoted_cmd = shell_quote_single_local(command->valuestring);
    cJSON_Delete(args);

    if (!quoted_cwd || !quoted_cmd) {
        free(quoted_cwd);
        free(quoted_cmd);
        return json_error_local("allocation failure");
    }

    int needed = snprintf(NULL,
                          0,
                          "cd %s && timeout %ds sh -lc %s 2>&1",
                          quoted_cwd,
                          timeout_s,
                          quoted_cmd);
    if (needed <= 0) {
        free(quoted_cwd);
        free(quoted_cmd);
        return json_error_local("failed to build shell command");
    }

    char* cmd = (char*)malloc((size_t)needed + 1U);
    if (!cmd) {
        free(quoted_cwd);
        free(quoted_cmd);
        return json_error_local("allocation failure");
    }

    snprintf(cmd,
             (size_t)needed + 1U,
             "cd %s && timeout %ds sh -lc %s 2>&1",
             quoted_cwd,
             timeout_s,
             quoted_cmd);

    free(quoted_cwd);
    free(quoted_cmd);

    FILE* fp = popen(cmd, "r");
    free(cmd);
    if (!fp) return json_error_local("failed to execute command");

    int max_bytes = ctx->cfg->tools.shell.max_output_bytes > 0 ? ctx->cfg->tools.shell.max_output_bytes : 65536;
    char* output = (char*)calloc((size_t)max_bytes + 1U, 1U);
    if (!output) {
        pclose(fp);
        return json_error_local("allocation failure");
    }

    size_t used = 0;
    while (!feof(fp) && used < (size_t)max_bytes) {
        size_t n = fread(output + used, 1, (size_t)max_bytes - used, fp);
        used += n;
        if (n == 0) break;
    }

    int raw_status = pclose(fp);
    int exit_status = raw_status;
    if (raw_status != -1) {
        if (WIFEXITED(raw_status)) {
            exit_status = WEXITSTATUS(raw_status);
        } else if (WIFSIGNALED(raw_status)) {
            exit_status = 128 + WTERMSIG(raw_status);
        }
    }

    cJSON* out = cJSON_CreateObject();
    if (!out) {
        free(output);
        return NULL;
    }

    cJSON_AddBoolToObject(out, "success", exit_status == 0 ? 1 : 0);
    cJSON_AddNumberToObject(out, "exit_status", exit_status);
    cJSON_AddStringToObject(out, "output", output);
    free(output);

    char* json = cJSON_PrintUnformatted(out);
    cJSON_Delete(out);
    return json;
}

char* execute_local_file_read(tools_context_t* ctx, const char* args_json) {
    if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable");

    cJSON* args = parse_args_local(args_json);
    if (!args) return json_error_local("invalid arguments JSON");

    cJSON* path = cJSON_GetObjectItemCaseSensitive(args, "path");
    cJSON* maxb = cJSON_GetObjectItemCaseSensitive(args, "max_bytes");
    if (!path || !cJSON_IsString(path) || !path->valuestring) {
        cJSON_Delete(args);
        return json_error_local("local_file_read requires string path");
    }

    int hard_max = ctx->cfg->tools.shell.max_output_bytes > 0 ? ctx->cfg->tools.shell.max_output_bytes : 65536;
    int max_bytes = (maxb && cJSON_IsNumber(maxb)) ? (int)maxb->valuedouble : hard_max;
    if (max_bytes <= 0 || max_bytes > hard_max) max_bytes = hard_max;

    char file_path[PATH_MAX];
    if (build_tool_path_local(ctx, path->valuestring, file_path, sizeof(file_path)) != 0) {
        cJSON_Delete(args);
        return json_error_local("local_file_read path is not allowed");
    }

    FILE* fp = fopen(file_path, "rb");
    cJSON_Delete(args);
    if (!fp) return json_error_local("local_file_read failed to open file");

    char* buf = (char*)calloc((size_t)max_bytes + 1U, 1U);
    if (!buf) {
        fclose(fp);
        return json_error_local("allocation failure");
    }

    size_t n = fread(buf, 1, (size_t)max_bytes, fp);
    int truncated = !feof(fp) ? 1 : 0;
    fclose(fp);
    buf[n] = '\0';

    cJSON* out = cJSON_CreateObject();
    if (!out) {
        free(buf);
        return NULL;
    }

    cJSON_AddBoolToObject(out, "success", 1);
    cJSON_AddStringToObject(out, "path", file_path);
    cJSON_AddNumberToObject(out, "bytes_read", (double)n);
    cJSON_AddBoolToObject(out, "truncated", truncated);
    cJSON_AddStringToObject(out, "content", buf);
    free(buf);

    char* json = cJSON_PrintUnformatted(out);
    cJSON_Delete(out);
    return json;
}

char* execute_local_file_write(tools_context_t* ctx, const char* args_json) {
    if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable");

    cJSON* args = parse_args_local(args_json);
    if (!args) return json_error_local("invalid arguments JSON");

    cJSON* path = cJSON_GetObjectItemCaseSensitive(args, "path");
    cJSON* content = cJSON_GetObjectItemCaseSensitive(args, "content");
    cJSON* append = cJSON_GetObjectItemCaseSensitive(args, "append");
    if (!path || !cJSON_IsString(path) || !path->valuestring ||
        !content || !cJSON_IsString(content) || !content->valuestring) {
        cJSON_Delete(args);
        return json_error_local("local_file_write requires string path and content");
    }

    char file_path[PATH_MAX];
    if (build_tool_path_local(ctx, path->valuestring, file_path, sizeof(file_path)) != 0) {
        cJSON_Delete(args);
        return json_error_local("local_file_write path is not allowed");
    }

    const char* content_str = content->valuestring;
    size_t len = strlen(content_str);
    int do_append = (append && cJSON_IsBool(append) && cJSON_IsTrue(append)) ? 1 : 0;

    FILE* fp = fopen(file_path, do_append ? "ab" : "wb");
    cJSON_Delete(args);
    if (!fp) return json_error_local("local_file_write failed to open file");

    size_t n = fwrite(content_str, 1, len, fp);
    fclose(fp);
    if (n != len) return json_error_local("local_file_write failed to write all bytes");

    cJSON* out = cJSON_CreateObject();
    if (!out) return NULL;
    cJSON_AddBoolToObject(out, "success", 1);
    cJSON_AddStringToObject(out, "path", file_path);
    cJSON_AddNumberToObject(out, "bytes_written", (double)n);
    cJSON_AddBoolToObject(out, "append", do_append);
    char* json = cJSON_PrintUnformatted(out);
    cJSON_Delete(out);
    return json;
}
