#define _POSIX_C_SOURCE 200809L

#include "llm.h"

#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include "cjson/cJSON.h"
#include "debug.h"

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

static llm_config_t g_cfg;
static int g_initialized = 0;

static int url_looks_like_websocket(const char* url) {
    if (!url) return 0;
    return (strncmp(url, "ws://", 5) == 0) || (strncmp(url, "wss://", 6) == 0);
}

static int json_string_is_blank(const cJSON* item) {
    if (!item || !cJSON_IsString(item) || !item->valuestring) {
        return 0;
    }
    const unsigned char* p = (const unsigned char*)item->valuestring;
    while (*p) {
        if (!isspace(*p)) {
            return 0;
        }
        p++;
    }
    return 1;
}

static char* perform_http_request(const char* url, const char* body, int is_post) {
    if (!url) {
        return NULL;
    }

    if (url_looks_like_websocket(url)) {
        DEBUG_ERROR("[didactyl] llm config error: base_url must be HTTP(S), got WebSocket URL: %s",
                    url);
        DEBUG_WARN("[didactyl] llm hint: set llm.base_url to an OpenAI-compatible HTTPS endpoint, e.g. https://api.example.com/v1");
        return NULL;
    }

    char auth_header[OW_MAX_KEY_LEN + 32];
    snprintf(auth_header, sizeof(auth_header), "Authorization: Bearer %s", g_cfg.api_key);
    const char* headers[] = {
        "Content-Type: application/json",
        auth_header,
        NULL
    };

    nostr_http_request_t req;
    memset(&req, 0, sizeof(req));
    req.method = is_post ? "POST" : "GET";
    req.url = url;
    req.headers = headers;
    req.timeout_seconds = 60;
    req.follow_redirects = 1;
    req.max_redirects = 3;
    req.user_agent = "didactyl/llm";
    if (is_post) {
        const char* payload = body ? body : "{}";
        req.body = (const unsigned char*)payload;
        req.body_len = strlen(payload);
    }

    if (is_post) {
        size_t body_len = body ? strlen(body) : 0U;
        size_t body_preview_len = body_len > 4000U ? 4000U : body_len;
        DEBUG_INFO("[didactyl] llm request: method=POST url=%s body_bytes=%zu body_preview=%.4000s%s",
                   url,
                   body_len,
                   body ? body : "",
                   body_len > body_preview_len ? "..." : "");
    } else {
        DEBUG_INFO("[didactyl] llm request: method=GET url=%s", url);
    }

    nostr_http_response_t resp;
    int rc = nostr_http_request(&req, &resp);
    if (rc != NOSTR_SUCCESS) {
        DEBUG_ERROR("[didactyl] llm http request failed: transport error rc=%d", rc);
        return NULL;
    }

    if (resp.status_code < 200 || resp.status_code >= 300) {
        DEBUG_ERROR("[didactyl] llm http request failed: status=%ld", resp.status_code);
        if (resp.status_code == 101) {
            DEBUG_WARN("[didactyl] llm hint: received HTTP 101 (Switching Protocols), which usually means llm.base_url points to a WebSocket server instead of an HTTP LLM API");
        }
        if (resp.body && resp.body_len > 0) {
            DEBUG_WARN("[didactyl] llm error response: %.1200s%s",
                       resp.body,
                       resp.body_len > 1200 ? "..." : "");
        }
        nostr_http_response_free(&resp);
        return NULL;
    }

    if (!resp.body) {
        DEBUG_ERROR("[didactyl] llm http request failed: empty response body");
        nostr_http_response_free(&resp);
        return NULL;
    }

    char* out = resp.body;
    free(resp.content_type);
    free(resp.headers_raw);
    memset(&resp, 0, sizeof(resp));
    return out;
}

static char* perform_chat_request(const char* body) {
    char url[OW_MAX_URL_LEN + 64];
    snprintf(url, sizeof(url), "%s/chat/completions", g_cfg.base_url);
    return perform_http_request(url, body, 1);
}

static char* build_request_json(const char* system_prompt, const char* user_message) {
    cJSON* root = cJSON_CreateObject();
    cJSON* messages = cJSON_CreateArray();
    if (!root || !messages) {
        cJSON_Delete(root);
        cJSON_Delete(messages);
        return NULL;
    }

    cJSON_AddStringToObject(root, "model", g_cfg.model);
    cJSON_AddNumberToObject(root, "max_tokens", g_cfg.max_tokens);
    cJSON_AddNumberToObject(root, "temperature", g_cfg.temperature);

    cJSON* system_msg = cJSON_CreateObject();
    cJSON* user_msg = cJSON_CreateObject();
    if (!system_msg || !user_msg) {
        cJSON_Delete(root);
        return NULL;
    }

    cJSON_AddStringToObject(system_msg, "role", "system");
    cJSON_AddStringToObject(system_msg, "content", system_prompt ? system_prompt : "");

    cJSON_AddStringToObject(user_msg, "role", "user");
    cJSON_AddStringToObject(user_msg, "content", user_message ? user_message : "");

    cJSON_AddItemToArray(messages, system_msg);
    cJSON_AddItemToArray(messages, user_msg);
    cJSON_AddItemToObject(root, "messages", messages);

    char* body = cJSON_PrintUnformatted(root);
    cJSON_Delete(root);
    return body;
}

static int parse_tool_calls(cJSON* msg, llm_response_t* out) {
    cJSON* tc = cJSON_GetObjectItemCaseSensitive(msg, "tool_calls");
    if (!tc || !cJSON_IsArray(tc)) {
        out->tool_calls = NULL;
        out->tool_call_count = 0;
        return 0;
    }

    int n = cJSON_GetArraySize(tc);
    if (n <= 0) {
        out->tool_calls = NULL;
        out->tool_call_count = 0;
        return 0;
    }

    llm_tool_call_t* calls = (llm_tool_call_t*)calloc((size_t)n, sizeof(llm_tool_call_t));
    if (!calls) return -1;

    int actual = 0;
    for (int i = 0; i < n; i++) {
        cJSON* item = cJSON_GetArrayItem(tc, i);
        cJSON* id = item ? cJSON_GetObjectItemCaseSensitive(item, "id") : NULL;
        cJSON* fn = item ? cJSON_GetObjectItemCaseSensitive(item, "function") : NULL;
        cJSON* name = fn ? cJSON_GetObjectItemCaseSensitive(fn, "name") : NULL;
        cJSON* args = fn ? cJSON_GetObjectItemCaseSensitive(fn, "arguments") : NULL;

        if (!id || !cJSON_IsString(id) || !id->valuestring ||
            !name || !cJSON_IsString(name) || !name->valuestring) {
            continue;
        }

        calls[actual].id = strdup(id->valuestring);
        calls[actual].name = strdup(name->valuestring);
        calls[actual].arguments_json = strdup((args && cJSON_IsString(args) && args->valuestring) ? args->valuestring : "{}");
        if (!calls[actual].id || !calls[actual].name || !calls[actual].arguments_json) {
            free(calls[actual].id);
            free(calls[actual].name);
            free(calls[actual].arguments_json);
            continue;
        }
        actual++;
    }

    if (actual == 0) {
        free(calls);
        out->tool_calls = NULL;
        out->tool_call_count = 0;
        return 0;
    }

    out->tool_calls = calls;
    out->tool_call_count = actual;
    return 0;
}

static int parse_llm_response(const char* json, llm_response_t* out) {
    memset(out, 0, sizeof(*out));

    cJSON* root = cJSON_Parse(json);
    if (!root) return -1;

    cJSON* choices = cJSON_GetObjectItemCaseSensitive(root, "choices");
    cJSON* first = (choices && cJSON_IsArray(choices) && cJSON_GetArraySize(choices) > 0)
                       ? cJSON_GetArrayItem(choices, 0)
                       : NULL;
    cJSON* msg = first ? cJSON_GetObjectItemCaseSensitive(first, "message") : NULL;
    if (!msg || !cJSON_IsObject(msg)) {
        cJSON_Delete(root);
        return -1;
    }

    cJSON* finish_reason = first ? cJSON_GetObjectItemCaseSensitive(first, "finish_reason") : NULL;
    if (finish_reason && cJSON_IsString(finish_reason) && finish_reason->valuestring) {
        out->finish_reason = strdup(finish_reason->valuestring);
    }

    cJSON* content = cJSON_GetObjectItemCaseSensitive(msg, "content");
    if (content && cJSON_IsString(content) && content->valuestring) {
        out->content = strdup(content->valuestring);
    }

    if (parse_tool_calls(msg, out) != 0) {
        cJSON_Delete(root);
        llm_response_free(out);
        return -1;
    }

    cJSON_Delete(root);
    return 0;
}

int llm_init(const llm_config_t* config) {
    if (!config) {
        return -1;
    }
    memset(&g_cfg, 0, sizeof(g_cfg));
    g_cfg = *config;
    g_initialized = 1;
    return 0;
}

char* llm_chat(const char* system_prompt, const char* user_message) {
    if (!g_initialized) {
        return NULL;
    }

    char* body = build_request_json(system_prompt, user_message);
    if (!body) return NULL;

    char* raw = perform_chat_request(body);
    free(body);
    if (!raw) return NULL;

    llm_response_t parsed;
    if (parse_llm_response(raw, &parsed) != 0) {
        DEBUG_ERROR("[didactyl] failed to parse llm response (non-tool path): %.1200s%s",
                    raw,
                    strlen(raw) > 1200 ? "..." : "");
        free(raw);
        return NULL;
    }
    free(raw);

    char* answer = parsed.content ? strdup(parsed.content) : NULL;
    llm_response_free(&parsed);
    return answer;
}

int llm_chat_with_tools_messages(const char* messages_json,
                                 const char* tools_json,
                                 const char* tool_choice,
                                 llm_response_t* out_response) {
    if (!g_initialized || !out_response || !messages_json) {
        return -1;
    }

    cJSON* root = cJSON_CreateObject();
    if (!root) {
        return -1;
    }

    cJSON_AddStringToObject(root, "model", g_cfg.model);
    cJSON_AddNumberToObject(root, "max_tokens", g_cfg.max_tokens);
    cJSON_AddNumberToObject(root, "temperature", g_cfg.temperature);

    cJSON* messages = cJSON_Parse(messages_json);
    if (!messages || !cJSON_IsArray(messages)) {
        cJSON_Delete(messages);
        cJSON_Delete(root);
        return -1;
    }

    int filtered_count = 0;
    for (int i = cJSON_GetArraySize(messages) - 1; i >= 0; i--) {
        cJSON* msg = cJSON_GetArrayItem(messages, i);
        if (!msg || !cJSON_IsObject(msg)) {
            continue;
        }

        cJSON* role = cJSON_GetObjectItemCaseSensitive(msg, "role");
        cJSON* content = cJSON_GetObjectItemCaseSensitive(msg, "content");
        cJSON* tool_calls = cJSON_GetObjectItemCaseSensitive(msg, "tool_calls");

        int role_is_textual = (role && cJSON_IsString(role) && role->valuestring &&
                               (strcmp(role->valuestring, "system") == 0 ||
                                strcmp(role->valuestring, "user") == 0 ||
                                strcmp(role->valuestring, "assistant") == 0));
        int has_tool_calls = (tool_calls && cJSON_IsArray(tool_calls) && cJSON_GetArraySize(tool_calls) > 0);

        if (role_is_textual && !has_tool_calls && json_string_is_blank(content)) {
            cJSON_DeleteItemFromArray(messages, i);
            filtered_count++;
        }
    }

    if (filtered_count > 0) {
        DEBUG_INFO("[didactyl] llm request sanitizer: removed %d empty text message(s) before provider request",
                   filtered_count);
    }

    cJSON_AddItemToObject(root, "messages", messages);

    if (tools_json) {
        cJSON* tools = cJSON_Parse(tools_json);
        if (tools && cJSON_IsArray(tools)) {
            cJSON_AddItemToObject(root, "tools", tools);
            cJSON_AddStringToObject(root, "tool_choice", tool_choice ? tool_choice : "auto");
        } else {
            cJSON_Delete(tools);
        }
    }

    char* body = cJSON_PrintUnformatted(root);
    cJSON_Delete(root);
    if (!body) return -1;

    char* raw = perform_chat_request(body);
    free(body);
    if (!raw) return -1;

    int rc = parse_llm_response(raw, out_response);
    if (rc != 0) {
        DEBUG_ERROR("[didactyl] failed to parse llm response (tools path): %.1200s%s",
                    raw,
                    strlen(raw) > 1200 ? "..." : "");
    }
    free(raw);
    return rc;
}

int llm_chat_with_tools(const char* system_prompt,
                        const char* user_message,
                        const char* tools_json,
                        llm_response_t* out_response) {
    cJSON* messages = cJSON_CreateArray();
    cJSON* system_msg = cJSON_CreateObject();
    cJSON* user_msg = cJSON_CreateObject();
    if (!messages || !system_msg || !user_msg) {
        cJSON_Delete(messages);
        cJSON_Delete(system_msg);
        cJSON_Delete(user_msg);
        return -1;
    }

    cJSON_AddStringToObject(system_msg, "role", "system");
    cJSON_AddStringToObject(system_msg, "content", system_prompt ? system_prompt : "");
    cJSON_AddStringToObject(user_msg, "role", "user");
    cJSON_AddStringToObject(user_msg, "content", user_message ? user_message : "");
    cJSON_AddItemToArray(messages, system_msg);
    cJSON_AddItemToArray(messages, user_msg);

    char* messages_json = cJSON_PrintUnformatted(messages);
    cJSON_Delete(messages);
    if (!messages_json) {
        return -1;
    }

    int rc = llm_chat_with_tools_messages(messages_json, tools_json, "auto", out_response);
    free(messages_json);
    return rc;
}

void llm_response_free(llm_response_t* response) {
    if (!response) return;
    free(response->content);
    free(response->finish_reason);
    for (int i = 0; i < response->tool_call_count; i++) {
        free(response->tool_calls[i].id);
        free(response->tool_calls[i].name);
        free(response->tool_calls[i].arguments_json);
    }
    free(response->tool_calls);
    memset(response, 0, sizeof(*response));
}

int llm_get_config(llm_config_t* out_config) {
    if (!g_initialized || !out_config) {
        return -1;
    }
    *out_config = g_cfg;
    return 0;
}

int llm_set_config(const llm_config_t* config) {
    if (!g_initialized || !config) {
        return -1;
    }
    g_cfg = *config;
    return 0;
}

char* llm_get_json_path(const char* base_url_override, const char* path) {
    if (!g_initialized || !path || path[0] == '\0') {
        return NULL;
    }

    const char* base_url = (base_url_override && base_url_override[0] != '\0')
                               ? base_url_override
                               : g_cfg.base_url;

    char url[OW_MAX_URL_LEN + 128];
    snprintf(url, sizeof(url), "%s%s", base_url, path);
    return perform_http_request(url, NULL, 0);
}

char* llm_list_models_json(const char* base_url_override) {
    return llm_get_json_path(base_url_override, "/models");
}

void llm_cleanup(void) {
    if (!g_initialized) {
        return;
    }
    memset(&g_cfg, 0, sizeof(g_cfg));
    g_initialized = 0;
}
