#include "agent.h"

#include "app_base.h"
#include "imgui.h"
#include "core/icons_tabler.h"
#include "core/selectable_text.h"
#include "core/logger.h"

#include "../../../../cpp/vendor/sqlite3/sqlite3.h"

#include <atomic>
#include <chrono>
#include <condition_variable>
#include <cstdarg>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <ctime>
#include <deque>
#include <filesystem>
#include <mutex>
#include <random>
#include <sstream>
#include <string>
#include <thread>
#include <vector>

#ifdef _WIN32
  #ifndef WIN32_LEAN_AND_MEAN
  #define WIN32_LEAN_AND_MEAN
  #endif
  #include <windows.h>
#else
  #include <errno.h>
  #include <fcntl.h>
  #include <signal.h>
  #include <sys/stat.h>
  #include <sys/types.h>
  #include <sys/wait.h>
  #include <unistd.h>
#endif

namespace app_agent {

// ----------------------------------------------------------------------------
// Logger con tags
// ----------------------------------------------------------------------------

namespace {

std::mutex  g_log_mu;
std::string g_log_path;

std::string iso_now_ms() {
    using namespace std::chrono;
    auto now = system_clock::now();
    auto t = system_clock::to_time_t(now);
    auto ms = duration_cast<milliseconds>(now.time_since_epoch()).count() % 1000;
    char buf[40];
    std::strftime(buf, sizeof(buf), "%Y-%m-%dT%H:%M:%S", std::gmtime(&t));
    char out[64];
    std::snprintf(out, sizeof(out), "%s.%03lldZ", buf, (long long)ms);
    return out;
}

} // namespace

void chat_log(const char* tag, const char* fmt, ...) {
    if (!tag) tag = "chat";
    char body[2048];
    va_list ap;
    va_start(ap, fmt);
    std::vsnprintf(body, sizeof(body), fmt, ap);
    va_end(ap);
    std::string ts  = iso_now_ms();
    std::string ln  = ts + " [chat:" + tag + "] " + body + "\n";

    {
        std::lock_guard<std::mutex> lk(g_log_mu);
        std::fputs(ln.c_str(), stderr);
        std::fflush(stderr);
        if (!g_log_path.empty()) {
            FILE* f = std::fopen(g_log_path.c_str(), "ab");
            if (f) {
                std::fputs(ln.c_str(), f);
                std::fclose(f);
            }
        }
    }
    // Mirror al logger global (ventana Settings → Logs...) — asi el usuario
    // puede ver los eventos de chat sin abrir chat.log en disco.
    if (std::strcmp(tag, "error") == 0) {
        fn_log::log_error("[chat:%s] %s", tag, body);
    } else {
        fn_log::log_info ("[chat:%s] %s", tag, body);
    }
}

const char* chat_log_path() {
    return g_log_path.c_str();
}

namespace {

// ----------------------------------------------------------------------------
// Pequeño extractor JSON (sin dependencias).  Solo lo justo para sacar el
// valor string asociado a una clave dentro de un objeto JSON. Maneja escapes
// basicos y devuelve "" si no encuentra la clave.
// ----------------------------------------------------------------------------

std::string json_unescape(const char* s, size_t n) {
    std::string out;
    out.reserve(n);
    for (size_t i = 0; i < n; ++i) {
        char c = s[i];
        if (c == '\\' && i + 1 < n) {
            char nx = s[++i];
            switch (nx) {
                case 'n':  out += '\n'; break;
                case 't':  out += '\t'; break;
                case 'r':  out += '\r'; break;
                case '"':  out += '"'; break;
                case '\\': out += '\\'; break;
                case '/':  out += '/'; break;
                case 'b':  out += '\b'; break;
                case 'f':  out += '\f'; break;
                case 'u': {
                    if (i + 4 < n) {
                        unsigned cp = 0;
                        for (int k = 0; k < 4; ++k) {
                            char h = s[i + 1 + k];
                            cp <<= 4;
                            if      (h >= '0' && h <= '9') cp |= (h - '0');
                            else if (h >= 'a' && h <= 'f') cp |= (h - 'a' + 10);
                            else if (h >= 'A' && h <= 'F') cp |= (h - 'A' + 10);
                        }
                        i += 4;
                        // utf-8 encode
                        if (cp < 0x80) {
                            out += (char)cp;
                        } else if (cp < 0x800) {
                            out += (char)(0xC0 | (cp >> 6));
                            out += (char)(0x80 | (cp & 0x3F));
                        } else {
                            out += (char)(0xE0 | (cp >> 12));
                            out += (char)(0x80 | ((cp >> 6) & 0x3F));
                            out += (char)(0x80 | (cp & 0x3F));
                        }
                    }
                    break;
                }
                default:   out += nx; break;
            }
        } else {
            out += c;
        }
    }
    return out;
}

// Devuelve el offset del valor (excluyendo comillas si string) o -1 si no
// existe. Solo busca claves a nivel "shallow" — no recursivo. Suficiente
// para el formato de stream-json de claude -p.
bool json_find_string(const std::string& obj, const char* key, std::string* out) {
    std::string pat = "\"";
    pat += key;
    pat += "\"";
    size_t pos = obj.find(pat);
    while (pos != std::string::npos) {
        size_t i = pos + pat.size();
        while (i < obj.size() && (obj[i] == ' ' || obj[i] == '\t')) ++i;
        if (i >= obj.size() || obj[i] != ':') {
            pos = obj.find(pat, pos + 1);
            continue;
        }
        ++i;
        while (i < obj.size() && (obj[i] == ' ' || obj[i] == '\t')) ++i;
        if (i >= obj.size() || obj[i] != '"') return false;  // no es string
        ++i;
        size_t start = i;
        while (i < obj.size()) {
            if (obj[i] == '\\' && i + 1 < obj.size()) { i += 2; continue; }
            if (obj[i] == '"') break;
            ++i;
        }
        if (i >= obj.size()) return false;
        *out = json_unescape(obj.data() + start, i - start);
        return true;
    }
    return false;
}

std::string make_uuid_v4() {
    std::random_device rd;
    std::mt19937_64 gen(rd());
    std::uniform_int_distribution<uint64_t> dis;
    uint64_t a = dis(gen), b = dis(gen);
    char buf[40];
    std::snprintf(buf, sizeof(buf),
                  "%08x-%04x-4%03x-%04x-%012llx",
                  (unsigned)(a & 0xFFFFFFFF),
                  (unsigned)((a >> 32) & 0xFFFF),
                  (unsigned)((a >> 48) & 0x0FFF),
                  (unsigned)(0x8000 | (b & 0x3FFF)),
                  (unsigned long long)((b >> 16) & 0xFFFFFFFFFFFFULL));
    return buf;
}

// ----------------------------------------------------------------------------
// Estado del chat
// ----------------------------------------------------------------------------

struct ChatMessage {
    enum Kind { USER, ASSISTANT, TOOL_USE, TOOL_RESULT, SYSTEM, ERROR_MSG };
    Kind        kind;
    std::string text;        // user/assistant: texto. tool_*: descripcion.
    std::string tool_name;   // para TOOL_USE
    std::string tool_input;  // resumen del input
};

struct State {
    std::string ops_db;
    std::string app_db;
    std::string app_dir;
    std::string claude_cmd;       // "claude" o "wsl"
    std::vector<std::string> claude_pre_args; // ["claude"] si wsl, vacio si nativo
    std::string mcp_config_path;       // path Linux del mcp.json para --mcp-config
    bool        ready = false;
    bool        first_turn_done = false;
    std::string session_id;       // uuid

    std::mutex                mu;
    std::vector<ChatMessage>  history;
    std::atomic<bool>         busy{false};
    std::thread               worker;

    char  input_buf[8192] = {};
    bool  scroll_to_bottom = false;
    bool  show_raw = false;
    std::vector<std::string> raw_lines;
};

State* g_st = nullptr;

// ----------------------------------------------------------------------------
// Detectar disponibilidad de claude
// ----------------------------------------------------------------------------

// Captura stdout+stderr de un comando corto y devuelve {exit_code, output}.
// Si falla creando el proceso, devuelve {-1, ""}.
struct ProbeResult { int rc; std::string out; };

#ifdef _WIN32
ProbeResult probe_capture(const std::string& cmdline) {
    SECURITY_ATTRIBUTES sa{sizeof(sa), nullptr, TRUE};
    HANDLE r=nullptr, w=nullptr;
    if (!CreatePipe(&r, &w, &sa, 0)) return {-1, ""};
    SetHandleInformation(r, HANDLE_FLAG_INHERIT, 0);
    STARTUPINFOA si{};
    si.cb = sizeof(si);
    si.dwFlags = STARTF_USESTDHANDLES;
    si.hStdInput  = GetStdHandle(STD_INPUT_HANDLE);
    si.hStdOutput = w;
    si.hStdError  = w;
    PROCESS_INFORMATION pi{};
    std::string mutable_cmd = cmdline;
    BOOL ok = CreateProcessA(
        nullptr, mutable_cmd.data(), nullptr, nullptr, TRUE,
        CREATE_NO_WINDOW, nullptr, nullptr, &si, &pi);
    CloseHandle(w);
    if (!ok) {
        CloseHandle(r);
        DWORD err = GetLastError();
        chat_log("detect", "CreateProcess fallo (err=%lu) para: %s",
                  (unsigned long)err, cmdline.c_str());
        return {-1, ""};
    }
    std::string out;
    char buf[1024];
    DWORD n = 0;
    while (ReadFile(r, buf, sizeof(buf), &n, nullptr) && n > 0) {
        out.append(buf, n);
    }
    CloseHandle(r);
    DWORD code = 0;
    WaitForSingleObject(pi.hProcess, 5000);  // 5s timeout
    GetExitCodeProcess(pi.hProcess, &code);
    CloseHandle(pi.hProcess);
    CloseHandle(pi.hThread);
    return {(int)code, out};
}
#else
ProbeResult probe_capture(const std::string& cmdline) {
    std::string c = cmdline + " 2>&1";
    FILE* p = popen(c.c_str(), "r");
    if (!p) return {-1, ""};
    std::string out;
    char buf[1024];
    size_t n;
    while ((n = std::fread(buf, 1, sizeof(buf), p)) > 0) {
        out.append(buf, n);
    }
    int rc = pclose(p);
    if (rc == -1) return {-1, out};
    if (WIFEXITED(rc)) rc = WEXITSTATUS(rc);
    return {rc, out};
}
#endif

void detect_claude() {
    if (!g_st) return;
#ifdef _WIN32
    // Probe 1: wsl --status (verifica que WSL existe).
    auto p_status = probe_capture("wsl.exe --status");
    chat_log("detect", "wsl.exe --status -> rc=%d, out=%.200s",
              p_status.rc, p_status.out.c_str());

    // Probe 2: bash login shell carga ~/.profile asi que claude (en
    // ~/.local/bin/) entra en PATH. Capturamos la ruta absoluta para usarla
    // luego con `wsl.exe --exec <path>` y bypassear el problema de quoting
    // de wsl.exe en modo `--`.
    auto p_path = probe_capture(
        "wsl.exe -- bash -lc \"command -v claude\"");
    chat_log("detect", "wsl bash -lc 'command -v claude' -> rc=%d, out=%.200s",
              p_path.rc, p_path.out.c_str());

    // Trim del path resultante (puede tener \r\n)
    std::string claude_path = p_path.out;
    while (!claude_path.empty() &&
           (claude_path.back() == '\n' || claude_path.back() == '\r' ||
            claude_path.back() == ' '  || claude_path.back() == '\t')) {
        claude_path.pop_back();
    }

    bool wsl_works    = (p_status.rc == 0);
    bool claude_works = (p_path.rc == 0) && !claude_path.empty();

    if (claude_works) {
        g_st->claude_cmd = "wsl.exe";
        // --exec NO usa shell, evita el problema de wsl.exe que reconcatena
        // y rompe el quoting de los args (perdiamos las comillas de
        // --append-system-prompt y de "$@" → exit 127). Path absoluto al
        // binario claude detectado en el probe anterior.
        g_st->claude_pre_args = {"--exec", claude_path};
        g_st->ready = true;
        chat_log("detect", "OK — claude en %s, usaremos wsl.exe --exec",
                  claude_path.c_str());
    } else if (wsl_works) {
        // wsl funciona pero no localizamos claude. Como fallback intentamos
        // un path estandar conocido — el primer send dira si funciono.
        g_st->claude_cmd = "wsl.exe";
        g_st->claude_pre_args = {"--exec", "/home/lucas/.local/bin/claude"};
        g_st->ready = true;
        chat_log("detect", "wsl OK pero no localizamos claude — "
                            "fallback a /home/lucas/.local/bin/claude");
    } else {
        g_st->ready = false;
        chat_log("detect", "wsl --status fallo — panel Chat deshabilitado");
    }
#else
    auto p = probe_capture("command -v claude");
    chat_log("detect", "command -v claude -> rc=%d, out=%.200s",
              p.rc, p.out.c_str());
    if (p.rc == 0) {
        g_st->claude_cmd = "claude";
        g_st->claude_pre_args = {};
        g_st->ready = true;
        auto v = probe_capture("claude --version");
        chat_log("detect", "claude --version -> rc=%d, out=%.200s",
                  v.rc, v.out.c_str());
    } else {
        g_st->ready = false;
        chat_log("detect", "claude no esta en PATH — panel deshabilitado");
    }
#endif
}

// ----------------------------------------------------------------------------
// Subprocess: enviar mensaje y leer stream-json hasta EOF
// ----------------------------------------------------------------------------

#ifndef _WIN32

// Lanza claude -p, escribe `prompt` en stdin, lee stdout linea a linea.
// Cada linea bien-formada (JSON object) se pasa a `on_line`. Devuelve el
// exit code, o -1 si fork/exec falla.
int run_claude_streaming(
    const std::vector<std::string>& argv,
    const std::string& prompt,
    void (*on_line)(const std::string&))
{
    int in_pipe[2], out_pipe[2];
    if (pipe(in_pipe) != 0 || pipe(out_pipe) != 0) return -1;

    pid_t pid = fork();
    if (pid < 0) {
        chat_log("error", "fork() fallo: errno=%d (%s)", errno, std::strerror(errno));
        close(in_pipe[0]); close(in_pipe[1]);
        close(out_pipe[0]); close(out_pipe[1]);
        return -1;
    }
    if (pid == 0) {
        // child
        dup2(in_pipe[0], STDIN_FILENO);
        dup2(out_pipe[1], STDOUT_FILENO);
        // stderr al stderr del padre (debugging visible en consola)
        close(in_pipe[0]); close(in_pipe[1]);
        close(out_pipe[0]); close(out_pipe[1]);
        std::vector<char*> args;
        for (auto& a : argv) args.push_back(const_cast<char*>(a.c_str()));
        args.push_back(nullptr);
        execvp(args[0], args.data());
        _exit(127);
    }
    // parent
    close(in_pipe[0]);
    close(out_pipe[1]);
    // escribe prompt y cierra
    if (!prompt.empty()) {
        ssize_t n = write(in_pipe[1], prompt.data(), prompt.size());
        chat_log("io", "stdin write %zd/%zu bytes",
                  (ssize_t)n, prompt.size());
    }
    close(in_pipe[1]);

    size_t total_in = 0;
    int    lines    = 0;
    std::string buf;
    char chunk[4096];
    for (;;) {
        ssize_t r = read(out_pipe[0], chunk, sizeof(chunk));
        if (r <= 0) break;
        total_in += (size_t)r;
        buf.append(chunk, (size_t)r);
        size_t nl;
        while ((nl = buf.find('\n')) != std::string::npos) {
            std::string line = buf.substr(0, nl);
            buf.erase(0, nl + 1);
            if (!line.empty()) { on_line(line); ++lines; }
        }
    }
    if (!buf.empty()) { on_line(buf); ++lines; }
    chat_log("io", "stdout EOF — total=%zu bytes, %d lineas", total_in, lines);
    close(out_pipe[0]);
    int status = 0;
    waitpid(pid, &status, 0);
    if (WIFEXITED(status)) return WEXITSTATUS(status);
    if (WIFSIGNALED(status)) {
        chat_log("error", "subprocess signaled %d", WTERMSIG(status));
    }
    return -1;
}

#else  // _WIN32

int run_claude_streaming(
    const std::vector<std::string>& argv,
    const std::string& prompt,
    void (*on_line)(const std::string&))
{
    // Construye command line con escape minimo
    std::string cmd;
    for (size_t i = 0; i < argv.size(); ++i) {
        if (i) cmd += ' ';
        const std::string& a = argv[i];
        bool need_q = a.empty() || a.find_first_of(" \t\"") != std::string::npos;
        if (need_q) {
            cmd += '"';
            for (char c : a) {
                if (c == '"') cmd += "\\\"";
                else cmd += c;
            }
            cmd += '"';
        } else {
            cmd += a;
        }
    }

    SECURITY_ATTRIBUTES sa{sizeof(sa), nullptr, TRUE};
    HANDLE in_r=nullptr, in_w=nullptr, out_r=nullptr, out_w=nullptr;
    if (!CreatePipe(&in_r, &in_w, &sa, 0)) return -1;
    SetHandleInformation(in_w, HANDLE_FLAG_INHERIT, 0);
    if (!CreatePipe(&out_r, &out_w, &sa, 0)) {
        CloseHandle(in_r); CloseHandle(in_w);
        return -1;
    }
    SetHandleInformation(out_r, HANDLE_FLAG_INHERIT, 0);

    STARTUPINFOA si{};
    si.cb = sizeof(si);
    si.dwFlags = STARTF_USESTDHANDLES;
    si.hStdInput  = in_r;
    si.hStdOutput = out_w;
    si.hStdError  = GetStdHandle(STD_ERROR_HANDLE);
    PROCESS_INFORMATION pi{};
    chat_log("spawn", "CreateProcess cmdline=%s", cmd.c_str());
    BOOL ok = CreateProcessA(
        nullptr, cmd.data(), nullptr, nullptr, TRUE,
        CREATE_NO_WINDOW, nullptr, nullptr, &si, &pi);
    CloseHandle(in_r);
    CloseHandle(out_w);
    if (!ok) {
        DWORD err = GetLastError();
        chat_log("error", "CreateProcess fallo, GetLastError=%lu",
                  (unsigned long)err);
        CloseHandle(in_w); CloseHandle(out_r);
        return -1;
    }

    if (!prompt.empty()) {
        DWORD wn = 0;
        WriteFile(in_w, prompt.data(), (DWORD)prompt.size(), &wn, nullptr);
        chat_log("io", "stdin write %lu/%zu bytes",
                  (unsigned long)wn, prompt.size());
    }
    CloseHandle(in_w);

    size_t total_in = 0;
    int    lines    = 0;
    std::string buf;
    char chunk[4096];
    for (;;) {
        DWORD rn = 0;
        BOOL r = ReadFile(out_r, chunk, sizeof(chunk), &rn, nullptr);
        if (!r || rn == 0) break;
        total_in += (size_t)rn;
        buf.append(chunk, (size_t)rn);
        size_t nl;
        while ((nl = buf.find('\n')) != std::string::npos) {
            std::string line = buf.substr(0, nl);
            // strip trailing CR
            if (!line.empty() && line.back() == '\r') line.pop_back();
            buf.erase(0, nl + 1);
            if (!line.empty()) { on_line(line); ++lines; }
        }
    }
    if (!buf.empty()) { on_line(buf); ++lines; }
    chat_log("io", "stdout EOF — total=%zu bytes, %d lineas",
              total_in, lines);
    CloseHandle(out_r);
    WaitForSingleObject(pi.hProcess, INFINITE);
    DWORD code = 0;
    GetExitCodeProcess(pi.hProcess, &code);
    CloseHandle(pi.hProcess);
    CloseHandle(pi.hThread);
    return (int)code;
}

#endif

// ----------------------------------------------------------------------------
// Procesado de eventos stream-json
// ----------------------------------------------------------------------------

void on_stream_line(const std::string& line) {
    if (!g_st) return;
    // Filtrar lineas no-JSON (logs spurios) — empiezan con '{'
    if (line.empty() || line[0] != '{') return;

    {
        std::lock_guard<std::mutex> lk(g_st->mu);
        if (g_st->raw_lines.size() > 500) {
            g_st->raw_lines.erase(g_st->raw_lines.begin(),
                                  g_st->raw_lines.begin() + 200);
        }
        g_st->raw_lines.push_back(line);
    }

    std::string type;
    if (!json_find_string(line, "type", &type)) return;

    // System init: capturar session_id si todavia no lo tenemos
    if (type == "system") {
        std::string sid, model;
        json_find_string(line, "session_id", &sid);
        json_find_string(line, "model", &model);
        chat_log("parse", "system init: session=%s model=%s",
                  sid.c_str(), model.c_str());
        if (!sid.empty()) {
            std::lock_guard<std::mutex> lk(g_st->mu);
            if (g_st->session_id.empty()) g_st->session_id = sid;
        }
        return;
    }

    // Assistant message: extraer text content
    if (type == "assistant") {
        // El campo "text" puede aparecer dentro del array content[].
        // Buscamos todos los "text":"..." que no sean del campo de tool_input.
        std::string s = line;
        size_t pos = 0;
        while (true) {
            size_t k = s.find("\"text\"", pos);
            if (k == std::string::npos) break;
            size_t i = k + 6;
            while (i < s.size() && (s[i] == ' ' || s[i] == '\t')) ++i;
            if (i >= s.size() || s[i] != ':') { pos = k + 1; continue; }
            ++i;
            while (i < s.size() && (s[i] == ' ' || s[i] == '\t')) ++i;
            if (i >= s.size() || s[i] != '"') { pos = k + 1; continue; }
            ++i;
            size_t start = i;
            while (i < s.size()) {
                if (s[i] == '\\' && i + 1 < s.size()) { i += 2; continue; }
                if (s[i] == '"') break;
                ++i;
            }
            std::string text = json_unescape(s.data() + start, i - start);
            if (!text.empty()) {
                std::lock_guard<std::mutex> lk(g_st->mu);
                ChatMessage m;
                m.kind = ChatMessage::ASSISTANT;
                m.text = text;
                g_st->history.push_back(std::move(m));
                g_st->scroll_to_bottom = true;
            }
            pos = i + 1;
        }
        // Detectar tool_use
        if (line.find("\"type\":\"tool_use\"") != std::string::npos) {
            std::string tname;
            json_find_string(line, "name", &tname);
            // Resumen del input — toma el primer campo string
            std::string summary;
            std::string command;
            json_find_string(line, "command", &command);
            // claude usa input:{...}; sacar la parte legible
            size_t ip = line.find("\"input\":");
            if (ip != std::string::npos) {
                size_t end = line.find("}", ip);
                if (end != std::string::npos) {
                    summary = line.substr(ip, end - ip + 1);
                    if (summary.size() > 240) summary = summary.substr(0, 240) + "...";
                }
            }
            chat_log("tools", "tool_use name=%s cmd=%s",
                      tname.c_str(), command.c_str());
            std::lock_guard<std::mutex> lk(g_st->mu);
            ChatMessage m;
            m.kind = ChatMessage::TOOL_USE;
            m.tool_name = tname;
            m.tool_input = summary;
            g_st->history.push_back(std::move(m));
            g_st->scroll_to_bottom = true;
        }
        return;
    }

    if (type == "user") {
        // Mensajes user wrappean tool_result. Si contiene "tool_use_id" y
        // "content", lo mostramos como TOOL_RESULT.
        if (line.find("\"tool_use_id\"") != std::string::npos) {
            std::string content;
            // El "content" puede ser string o array — intento string primero
            if (!json_find_string(line, "content", &content)) {
                // saca lo que haya entre el primer "text":" y "
                size_t k = line.find("\"text\":");
                if (k != std::string::npos) {
                    size_t s = line.find('"', k + 7);
                    size_t e = (s == std::string::npos) ? std::string::npos
                                                        : line.find('"', s + 1);
                    if (e != std::string::npos)
                        content = json_unescape(line.data() + s + 1, e - s - 1);
                }
            }
            if (content.size() > 1500) content = content.substr(0, 1500) + "\n... (truncado)";
            std::lock_guard<std::mutex> lk(g_st->mu);
            ChatMessage m;
            m.kind = ChatMessage::TOOL_RESULT;
            m.text = content;
            g_st->history.push_back(std::move(m));
            g_st->scroll_to_bottom = true;
        }
        return;
    }

    if (type == "result") {
        std::string sub;
        json_find_string(line, "subtype", &sub);
        chat_log("parse", "result subtype=%s", sub.c_str());
        if (sub == "error_max_turns" || sub == "error_during_execution") {
            std::string err = "(claude error: " + sub + ")";
            std::lock_guard<std::mutex> lk(g_st->mu);
            ChatMessage m;
            m.kind = ChatMessage::ERROR_MSG;
            m.text = err;
            g_st->history.push_back(std::move(m));
            g_st->scroll_to_bottom = true;
        }
        return;
    }
}

// ----------------------------------------------------------------------------
// System prompt para el agente
// ----------------------------------------------------------------------------

std::string build_system_prompt() {
    return
        "Eres el Agente — copiloto OSINT del usuario sobre la operations.db\n"
        "abierta en graph_explorer. Tu trabajo es explorar, investigar y\n"
        "conectar piezas. El usuario VE el grafo en pantalla; tu lo\n"
        "modificas y razonas sobre el.\n\n"
        "TUS HERRAMIENTAS — todas las operaciones del grafo estan expuestas\n"
        "como tools tipadas del MCP server `graph_explorer`. Tendran prefijo\n"
        "`mcp__graph_explorer__` en el listado de tools. NO uses Bash para\n"
        "tareas del grafo — usa siempre las tools tipadas. (Bash queda para\n"
        "cosas raras como debugging.)\n\n"
        "Tools disponibles:\n"
        "  info, node_list, node_show, node_search,\n"
        "  node_create, node_update, node_delete,\n"
        "  rel_create, rel_delete, rel_list,\n"
        "  table_list, table_page, table_promote, table_demote,\n"
        "  enricher_list, enricher_run, query\n\n"
        "WORKFLOW:\n"
        "1. Empieza por `info` para ver counts y tipos del grafo.\n"
        "2. Para investigar antes de mutar usa `node_search` (FTS) o `query`\n"
        "   (SQL libre, solo SELECT). Lee `node_show` para detalle de uno.\n"
        "3. Cada mutacion (create/update/delete/promote) refresca el viewport\n"
        "   automaticamente via marker file — el usuario lo VE al instante.\n"
        "4. Para enriquecer un nodo (fetch web, extraer entidades, etc.) usa\n"
        "   `enricher_list` para ver opciones aplicables al tipo, luego\n"
        "   `enricher_run`. Los jobs corren async; el resultado aparece en\n"
        "   el viewport sin que tengas que esperar.\n"
        "5. Si el usuario pide algo ambiguo, ejecuta tu mejor interpretacion\n"
        "   y describe el resultado. Solo pregunta para evitar destruccion\n"
        "   (delete masivo, sobreescritura de datos importantes).\n"
        "6. Resume brevemente al final — el usuario ve los cambios en el\n"
        "   viewport, asi que no narres lo obvio. Lo importante: que falta,\n"
        "   que sugerencias de proximo paso, hallazgos no obvios.\n\n"
        "Tu objetivo: ser un copiloto util para investigacion OSINT, no un\n"
        "ejecutor mecanico. Propon, conecta, descubre patrones. Firmas\n"
        "como Agente cuando tenga sentido marcar identidad.";
}

// Convierte un path Windows (UNC \\wsl.localhost\<distro>\... o C:\...) al
// equivalente WSL (/home/... o /mnt/c/...). En POSIX es no-op. Necesario
// para escribir el mcp.json: claude corre en WSL y lee paths Linux.
std::string to_linux_path(const std::string& p) {
#ifndef _WIN32
    return p;
#else
    if (p.empty()) return p;
    if (p[0] == '/') return p;
    auto is_sep = [](char c) { return c == '\\' || c == '/'; };

    // UNC: \\<server>\<share>\rest
    if (p.size() >= 2 && is_sep(p[0]) && is_sep(p[1])) {
        size_t i = 2;
        while (i < p.size() && !is_sep(p[i])) ++i;
        std::string server = p.substr(2, i - 2);
        for (auto& c : server) c = (char)std::tolower((unsigned char)c);
        if (server == "wsl.localhost" || server == "wsl$") {
            if (i < p.size()) ++i;
            while (i < p.size() && !is_sep(p[i])) ++i;
            std::string rest = p.substr(i);
            for (char& c : rest) if (c == '\\') c = '/';
            return rest.empty() ? std::string("/") : rest;
        }
        std::string out = p;
        for (char& c : out) if (c == '\\') c = '/';
        return out;
    }

    // Drive letter: X:\... -> /mnt/x/...
    if (p.size() >= 3 && std::isalpha((unsigned char)p[0]) && p[1] == ':' &&
        is_sep(p[2])) {
        std::string out = "/mnt/";
        out.push_back((char)std::tolower((unsigned char)p[0]));
        for (size_t i = 2; i < p.size(); ++i) {
            out.push_back(p[i] == '\\' ? '/' : p[i]);
        }
        return out;
    }
    return p;
#endif
}

// Escribe el fichero mcp.json que claude leera al arrancar para spawnar
// el MCP server gx-cli. Devuelve el path para --mcp-config (Linux dentro
// de WSL en Windows; native en Linux). El contenido del fichero usa paths
// Linux porque claude corre dentro de WSL.
std::string write_mcp_config() {
    if (!g_st) return "";
    std::string config_path_native;       // path para fopen
    std::string config_path_for_claude;   // path que se pasa a --mcp-config

    // Ponemos mcp.json junto a app_db (mismo dir que chat.log).
    {
        std::string p = g_st->app_db;
        size_t slash = p.find_last_of("/\\");
        size_t dir_end = (slash == std::string::npos) ? 0 : slash + 1;
        config_path_native = p.substr(0, dir_end) + "mcp.json";
    }
#ifdef _WIN32
    // claude corre en WSL — necesita el path Linux del config file.
    // Pero to_linux_path solo convierte paths ABSOLUTOS. Si app_db era
    // relativo (caso normal cuando cwd = directorio del exe), config_path
    // tambien lo es. Resolvemos primero a absoluto Windows.
    {
        char buf[MAX_PATH * 2];
        DWORD n = GetFullPathNameA(config_path_native.c_str(),
                                    (DWORD)sizeof(buf), buf, nullptr);
        if (n > 0 && n < sizeof(buf)) {
            config_path_native = buf;  // ahora absoluto: C:\Users\...\mcp.json
        }
    }
    config_path_for_claude = to_linux_path(config_path_native);
#else
    config_path_for_claude = config_path_native;
#endif

    // gx-cli vive en <exe_dir>/assets/ tras el deploy con la
    // convencion assets/. Fallback al app_dir del repo en modo dev.
    std::string app_dir_linux = to_linux_path(g_st->app_dir);
    std::string gxcli_path;
    {
        std::string assets_gx = fn::asset_path(
#ifdef _WIN32
            "gx-cli.exe"
#else
            "gx-cli"
#endif
        );
        std::error_code _ec;
        if (std::filesystem::exists(assets_gx, _ec)) {
            gxcli_path = to_linux_path(assets_gx);
        } else {
            gxcli_path = app_dir_linux + "/gx-cli";
        }
    }

    // No metemos env: en el config para que herede del proceso padre
    // (claude, que ya tiene GX_OPS_DB/GX_APP_DB/GX_APP_DIR via WSLENV).
    std::ostringstream js;
    js << "{\n"
       << "  \"mcpServers\": {\n"
       << "    \"graph_explorer\": {\n"
       << "      \"command\": \"" << gxcli_path << "\",\n"
       << "      \"args\": [\"mcp-server\"]\n"
       << "    }\n"
       << "  }\n"
       << "}\n";

    FILE* f = std::fopen(config_path_native.c_str(), "wb");
    if (!f) {
        chat_log("error", "no pude escribir mcp.json en %s",
                  config_path_native.c_str());
        return "";
    }
    std::string content = js.str();
    std::fwrite(content.data(), 1, content.size(), f);
    std::fclose(f);
    chat_log("init", "mcp.json escrito en %s (claude lo lee como %s)",
              config_path_native.c_str(), config_path_for_claude.c_str());
    chat_log("init", "MCP server: %s mcp-server", gxcli_path.c_str());
    return config_path_for_claude;
}

// ----------------------------------------------------------------------------
// Worker: ejecuta una vuelta completa y termina
// ----------------------------------------------------------------------------

void worker_send(std::string user_text) {
    if (!g_st) return;
    if (g_st->session_id.empty()) g_st->session_id = make_uuid_v4();
    chat_log("spawn", "turno %d, session_id=%s, prompt_len=%zu bytes",
              g_st->first_turn_done ? 2 : 1,
              g_st->session_id.c_str(), user_text.size());

    std::vector<std::string> argv;
    argv.push_back(g_st->claude_cmd);
    for (auto& a : g_st->claude_pre_args) argv.push_back(a);
    argv.push_back("-p");
    argv.push_back("--input-format"); argv.push_back("text");
    argv.push_back("--output-format"); argv.push_back("stream-json");
    argv.push_back("--verbose");
    argv.push_back("--allowed-tools");
    // mcp__graph_explorer__* expone las 17 tools tipadas. Bash queda como
    // fallback para casos raros; el system prompt empuja a usar las MCP.
    argv.push_back("mcp__graph_explorer__* Bash Read Glob Grep");
    argv.push_back("--permission-mode");
    argv.push_back("bypassPermissions");
    if (!g_st->mcp_config_path.empty()) {
        argv.push_back("--mcp-config");
        argv.push_back(g_st->mcp_config_path);
    }
    if (g_st->first_turn_done) {
        argv.push_back("--resume"); argv.push_back(g_st->session_id);
    } else {
        argv.push_back("--session-id"); argv.push_back(g_st->session_id);
        argv.push_back("--append-system-prompt");
        argv.push_back(build_system_prompt());
        // En la primera vuelta inyectamos las env-vars como contexto explicito
        // porque no podemos confiar en que claude las herede en todos los
        // entornos (Windows wsl.exe las propaga, Linux nativo tambien, pero
        // documentarlo en el mensaje hace al agente independiente).
    }

    // Setenv para el subprocess (claude hereda env, lo pasa a Bash tool calls).
#ifndef _WIN32
    setenv("GX_OPS_DB",  g_st->ops_db.c_str(),  1);
    setenv("GX_APP_DB",  g_st->app_db.c_str(),  1);
    setenv("GX_APP_DIR", g_st->app_dir.c_str(), 1);
    // Asegura que gx-cli esta en PATH anteponiendo app_dir.
    std::string path = g_st->app_dir;
    if (const char* p = std::getenv("PATH")) {
        path += ":";
        path += p;
    }
    setenv("PATH", path.c_str(), 1);
    chat_log("env", "GX_OPS_DB=%s",  g_st->ops_db.c_str());
    chat_log("env", "GX_APP_DB=%s",  g_st->app_db.c_str());
    chat_log("env", "GX_APP_DIR=%s", g_st->app_dir.c_str());
#else
    // WSLENV /p traduce paths Windows -> WSL automaticamente, pero SOLO si
    // el path es absoluto. Los relativos pasan literales (con backslash) y
    // dentro de WSL no resuelven a nada — por eso el agente abria la BD
    // equivocada. Resolvemos con GetFullPathNameA antes de setear.
    auto abs_win = [](const std::string& p) -> std::string {
        if (p.empty()) return p;
        char buf[MAX_PATH * 2];
        DWORD n = GetFullPathNameA(p.c_str(),
                                    (DWORD)sizeof(buf), buf, nullptr);
        if (n == 0 || n >= sizeof(buf)) return p;
        return std::string(buf);
    };
    std::string ops_abs  = abs_win(g_st->ops_db);
    std::string appdb_abs = abs_win(g_st->app_db);
    std::string appdir_abs;
    // app_dir suele ser ya UNC \\wsl.localhost\... o ruta WSL — no queremos
    // que GetFullPathNameA lo "windowsifique". Si empieza por \\ lo dejamos.
    if (g_st->app_dir.size() >= 2 &&
        g_st->app_dir[0] == '\\' && g_st->app_dir[1] == '\\') {
        appdir_abs = g_st->app_dir;
    } else {
        appdir_abs = abs_win(g_st->app_dir);
    }

    SetEnvironmentVariableA("GX_OPS_DB",  ops_abs.c_str());
    SetEnvironmentVariableA("GX_APP_DB",  appdb_abs.c_str());
    SetEnvironmentVariableA("GX_APP_DIR", appdir_abs.c_str());
    SetEnvironmentVariableA(
        "WSLENV", "GX_OPS_DB/p:GX_APP_DB/p:GX_APP_DIR/p");
    chat_log("env", "GX_OPS_DB=%s",  ops_abs.c_str());
    chat_log("env", "GX_APP_DB=%s",  appdb_abs.c_str());
    chat_log("env", "GX_APP_DIR=%s", appdir_abs.c_str());
    chat_log("env", "WSLENV=GX_OPS_DB/p:GX_APP_DB/p:GX_APP_DIR/p");
#endif

    // Log del argv completo
    {
        std::string joined;
        for (size_t i = 0; i < argv.size(); ++i) {
            if (i) joined += ' ';
            joined += argv[i];
        }
        chat_log("spawn", "argv: %s", joined.c_str());
    }

    // En la primera vuelta inyectamos el contexto del entorno como prefijo
    // del primer mensaje. NO imprimimos los paths Windows tal cual — WSLENV
    // los traducira a /mnt/c/... dentro de WSL antes de que el agente vea
    // las env vars. Lo importante es decirle al agente: usa las env vars
    // GX_OPS_DB / GX_APP_DB / GX_APP_DIR que ya estan seteadas, NO inventes
    // paths.
    std::string prompt = user_text;
    if (!g_st->first_turn_done) {
        std::ostringstream ctx;
        ctx << "[contexto del entorno — no respondas a esto, solo recuerdalo]\n"
            << "Las variables de entorno GX_OPS_DB, GX_APP_DB y GX_APP_DIR\n"
            << "estan seteadas en TU shell (bash). gx-cli las lee\n"
            << "automaticamente, asi que NUNCA pases --db ni configures paths\n"
            << "manualmente. Verifica con: `env | grep GX_`. La BD apuntada\n"
            << "por GX_OPS_DB es la que el usuario VE en el viewport.\n"
            << "Comando estandar: $GX_APP_DIR/gx-cli <subcomando> ...\n\n"
            << "[mensaje del usuario]\n"
            << user_text;
        prompt = ctx.str();
    }

    auto t0 = std::chrono::steady_clock::now();
    int rc = run_claude_streaming(argv, prompt, on_stream_line);
    auto dt_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
        std::chrono::steady_clock::now() - t0).count();
    chat_log("spawn", "subprocess exit rc=%d, duration=%lldms", rc, (long long)dt_ms);
    if (rc != 0) {
        std::lock_guard<std::mutex> lk(g_st->mu);
        ChatMessage m;
        m.kind = ChatMessage::ERROR_MSG;
        m.text = "claude -p exit code " + std::to_string(rc) +
                  " — revisa " + g_log_path;
        g_st->history.push_back(std::move(m));
        g_st->scroll_to_bottom = true;
    }
    g_st->first_turn_done = true;
    g_st->busy.store(false);
}

} // namespace

// ----------------------------------------------------------------------------
// API publica
// ----------------------------------------------------------------------------

bool chat_init(const char* ops_db_path, const char* app_db_path,
               const char* app_dir)
{
    if (!g_st) g_st = new State();
    g_st->ops_db  = ops_db_path  ? ops_db_path  : "";
    g_st->app_db  = app_db_path  ? app_db_path  : "";
    g_st->app_dir = app_dir      ? app_dir      : "";

    // Path del log: junto a app_db (= graph_explorer.db, sibling del exe en
    // Windows). Si app_db esta vacio, fallback a stderr-only.
    if (!g_st->app_db.empty()) {
        std::string p = g_st->app_db;
        // strip ".db" / sustituir extension
        size_t slash = p.find_last_of("/\\");
        size_t dir_end = (slash == std::string::npos) ? 0 : slash + 1;
        g_log_path = p.substr(0, dir_end) + "agent.log";
        // arranca con un separador para distinguir sesiones
        FILE* f = std::fopen(g_log_path.c_str(), "ab");
        if (f) {
            std::fputs("\n========================================\n", f);
            std::fclose(f);
        }
    }
    chat_log("init", "ops_db=%s",  g_st->ops_db.c_str());
    chat_log("init", "app_db=%s",  g_st->app_db.c_str());
    chat_log("init", "app_dir=%s", g_st->app_dir.c_str());
    chat_log("init", "log_path=%s", g_log_path.c_str());

    detect_claude();

    // Genera mcp.json apuntando a gx-cli mcp-server. Solo si claude esta
    // disponible — si no, no merece la pena.
    if (g_st->ready) {
        g_st->mcp_config_path = write_mcp_config();
    }

    return g_st->ready;
}

void chat_set_ops_db(const char* ops_db_path) {
    if (!g_st) return;
    if (!ops_db_path) return;
    if (g_st->ops_db == ops_db_path) return;
    g_st->ops_db = ops_db_path;
    // resetear sesion para que el nuevo grafo se contextualice en turno 1
    {
        std::lock_guard<std::mutex> lk(g_st->mu);
        g_st->session_id.clear();
        g_st->first_turn_done = false;
        g_st->history.push_back({ChatMessage::SYSTEM,
                                  "[ops_db cambiada → nueva sesion]", "", ""});
    }
    // Regenerar mcp.json — el server hereda env del proceso claude que se
    // arranca por turno, asi que con env-vars actualizadas en worker_send
    // el MCP server vera los nuevos paths. El fichero mcp.json en si no
    // contiene los paths, asi que tecnicamente no hace falta reescribir;
    // lo regeneramos por si el path del propio mcp.json cambio.
    if (g_st->ready) {
        g_st->mcp_config_path = write_mcp_config();
    }
}

void chat_send(const char* user_text) {
    if (!g_st || !user_text || !*user_text) return;
    chat_log("send", "user msg, %zu bytes, ready=%d busy=%d",
              std::strlen(user_text), (int)g_st->ready,
              (int)g_st->busy.load());
    if (!g_st->ready) {
        std::lock_guard<std::mutex> lk(g_st->mu);
        g_st->history.push_back({ChatMessage::ERROR_MSG,
            std::string("claude no detectado — revisa ") + g_log_path +
                ". En Windows necesitas Claude Code instalado en WSL.",
            "", ""});
        return;
    }
    if (g_st->busy.exchange(true)) {
        chat_log("send", "ignorado: ya hay una vuelta en curso");
        return;
    }

    {
        std::lock_guard<std::mutex> lk(g_st->mu);
        g_st->history.push_back({ChatMessage::USER, user_text, "", ""});
        g_st->scroll_to_bottom = true;
    }
    if (g_st->worker.joinable()) g_st->worker.join();
    g_st->worker = std::thread(worker_send, std::string(user_text));
}

int chat_mutations_counter() {
    // Lee el mtime del marker file `.mutations.marker` junto a graph_explorer.db.
    // Devuelve mtime en segundos como int — si cambia entre polls, el caller
    // sabe que hay nueva mutacion. Si el fichero no existe, devuelve 0.
    //
    // Ventaja sobre tabla SQLite: stat() funciona cross-filesystem-boundary
    // (NTFS <-> 9p) sin contencion. SQLite WAL no.
    if (!g_st || g_st->app_db.empty()) return 0;
    std::string marker = g_st->app_db;
    size_t slash = marker.find_last_of("/\\");
    if (slash != std::string::npos) {
        marker = marker.substr(0, slash + 1) + ".mutations.marker";
    } else {
        marker = ".mutations.marker";
    }
#ifdef _WIN32
    WIN32_FILE_ATTRIBUTE_DATA fad;
    if (!GetFileAttributesExA(marker.c_str(), GetFileExInfoStandard, &fad)) {
        return 0;
    }
    // FILETIME es 100-nanoseg desde 1601 — suficiente como int monotono.
    return (int)((((uint64_t)fad.ftLastWriteTime.dwHighDateTime << 32)
                  | fad.ftLastWriteTime.dwLowDateTime) / 10000000ULL);
#else
    struct stat st;
    if (::stat(marker.c_str(), &st) != 0) return 0;
    return (int)st.st_mtime;
#endif
}

void chat_render(bool* panel_open) {
    if (!g_st) return;
    if (panel_open && !*panel_open) return;

    if (!ImGui::Begin(TI_MESSAGE_CIRCLE " Agent", panel_open)) {
        ImGui::End();
        return;
    }

    // Status line
    if (!g_st->ready) {
        ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.3f, 1.0f),
            TI_ALERT_TRIANGLE " claude no detectado");
        ImGui::Separator();
    } else {
        ImGui::TextDisabled("ops_db: %s", g_st->ops_db.c_str());
        ImGui::SameLine();
        ImGui::Checkbox("raw", &g_st->show_raw);
        ImGui::Separator();
    }

    // History
    float input_h = 80.0f;
    ImGui::BeginChild("##history", ImVec2(0, -input_h - 4.0f), true);
    {
        std::lock_guard<std::mutex> lk(g_st->mu);
        for (const auto& m : g_st->history) {
            switch (m.kind) {
                case ChatMessage::USER:
                    ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, 1.0f),
                                        TI_USER " You");
                    fn_ui::selectable_text_wrapped_force(m.text.c_str());
                    break;
                case ChatMessage::ASSISTANT:
                    ImGui::TextColored(ImVec4(0.7f, 0.9f, 0.7f, 1.0f),
                                        TI_ROBOT " Agent");
                    fn_ui::selectable_text_wrapped_force(m.text.c_str());
                    break;
                case ChatMessage::TOOL_USE:
                    ImGui::TextColored(ImVec4(0.9f, 0.8f, 0.4f, 1.0f),
                        TI_TOOL " %s", m.tool_name.empty()
                                        ? "(tool)" : m.tool_name.c_str());
                    if (!m.tool_input.empty()) {
                        fn_ui::selectable_text_wrapped_force(m.tool_input.c_str());
                    }
                    break;
                case ChatMessage::TOOL_RESULT:
                    ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f),
                                        TI_CORNER_DOWN_LEFT " result");
                    fn_ui::selectable_text_wrapped_force(m.text.c_str());
                    break;
                case ChatMessage::SYSTEM:
                    ImGui::PushStyleColor(ImGuiCol_Text,
                        ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled));
                    ImGui::TextWrapped("%s", m.text.c_str());
                    ImGui::PopStyleColor();
                    break;
                case ChatMessage::ERROR_MSG:
                    ImGui::PushStyleColor(ImGuiCol_Text,
                                           ImVec4(1.0f, 0.4f, 0.3f, 1.0f));
                    fn_ui::selectable_text_wrapped_force(m.text.c_str());
                    ImGui::PopStyleColor();
                    break;
            }
            ImGui::Spacing();
        }
        if (g_st->show_raw) {
            ImGui::Separator();
            ImGui::TextDisabled("== raw stream-json ==");
            for (const auto& l : g_st->raw_lines) {
                ImGui::TextWrapped("%s", l.c_str());
            }
        }
    }
    if (g_st->scroll_to_bottom) {
        ImGui::SetScrollHereY(1.0f);
        g_st->scroll_to_bottom = false;
    }
    ImGui::EndChild();

    // Spinner
    if (g_st->busy.load()) {
        ImGui::Text(TI_LOADER " thinking...");
    } else {
        ImGui::Dummy(ImVec2(0, ImGui::GetTextLineHeight()));
    }

    // Input
    ImGui::PushItemWidth(-80.0f);
    bool send = ImGui::InputTextMultiline(
        "##chat_input", g_st->input_buf, sizeof(g_st->input_buf),
        ImVec2(0, input_h - 4.0f),
        ImGuiInputTextFlags_EnterReturnsTrue
            | ImGuiInputTextFlags_CtrlEnterForNewLine);
    ImGui::PopItemWidth();
    ImGui::SameLine();
    if (ImGui::Button("Send", ImVec2(72.0f, input_h - 4.0f))) send = true;

    if (send && !g_st->busy.load() && g_st->input_buf[0]) {
        chat_send(g_st->input_buf);
        g_st->input_buf[0] = 0;
    }

    ImGui::End();
}

void chat_shutdown() {
    if (!g_st) return;
    if (g_st->worker.joinable()) {
        // No bloqueamos forzosamente — claude -p termina solo al cerrar stdin
        g_st->worker.join();
    }
    delete g_st;
    g_st = nullptr;
}

} // namespace app_agent
