// terminal_panel_linux.cpp — backend PTY para Linux/macOS.
// Compilado solo en plataformas no-Windows.
//
// Implementacion: forkpty() crea el proceso hijo con un PTY maestro/esclavo.
// Un thread de lectura en background lee del fd maestro de forma no-bloqueante
// y llama process_output() para actualizar el scrollback buffer.

#ifndef _WIN32

#include "viz/terminal_panel/terminal_panel.h"
#include "core/logger.h"

#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <fcntl.h>
#include <signal.h>
#include <sys/wait.h>
#include <unistd.h>
#include <pty.h>   // forkpty — requiere -lutil en Linux

namespace fn_term {

namespace {

// Detecta el shell por defecto: $SHELL o /bin/bash como fallback.
std::string default_shell() {
    const char* sh = std::getenv("SHELL");
    return sh ? sh : "/bin/bash";
}

// Thread de lectura: lee del fd maestro del PTY en bloques y
// llama process_output. Termina cuando el proceso hijo cierra el PTY
// (read devuelve 0 o EIO) o cuando reader_running se pone a false.
void reader_thread_fn(TerminalPanel* panel) {
    char buf[4096];
    while (panel->reader_running.load()) {
        ssize_t n = ::read(panel->master_fd, buf, sizeof(buf));
        if (n > 0) {
            process_output(*panel, buf, static_cast<size_t>(n));
        } else if (n == 0) {
            // EOF: el proceso hijo cerró el PTY.
            break;
        } else {
            // EIO ocurre cuando el proceso hijo sale y cierra el esclavo.
            if (errno == EIO || errno == EBADF) break;
            if (errno == EINTR) continue;
            // Otro error transitorio: esperar un poco y reintentar.
            usleep(5000);
        }
    }

    // Recolectar el código de salida del hijo.
    if (panel->child_pid > 0) {
        int status = 0;
        ::waitpid(panel->child_pid, &status, WNOHANG);
        if (WIFEXITED(status))
            panel->exit_code = WEXITSTATUS(status);
        else if (WIFSIGNALED(status))
            panel->exit_code = -WTERMSIG(status);
    }
    panel->process_exited.store(true);
    panel->reader_running.store(false);
}

} // namespace

void open(TerminalPanel& panel) {
    if (panel.is_open()) return;

    std::string sh = panel.shell.empty() ? default_shell() : panel.shell;

    // Construir argv.
    const char* argv[] = {sh.c_str(), nullptr};

    // Construir envp: heredar entorno + extras.
    // Para simplicidad en v1, pasamos nullptr (hereda el entorno completo)
    // y añadimos las variables extra via setenv antes del fork.
    // TODO(0132): construir envp completo en v2.

    struct winsize ws;
    ws.ws_row    = 24;
    ws.ws_col    = 80;
    ws.ws_xpixel = 0;
    ws.ws_ypixel = 0;

    int master_fd = -1;
    pid_t pid = forkpty(&master_fd, nullptr, nullptr, &ws);

    if (pid < 0) {
        fn_log::log_error("terminal_panel: forkpty failed: %s", strerror(errno));
        return;
    }

    if (pid == 0) {
        // Proceso hijo.
        // Aplicar variables de entorno extra.
        for (const auto& kv : panel.env) {
            const auto eq = kv.find('=');
            if (eq != std::string::npos) {
                std::string key = kv.substr(0, eq);
                std::string val = kv.substr(eq + 1);
                ::setenv(key.c_str(), val.c_str(), 1);
            }
        }
        // Cambiar directorio de trabajo si se especificó.
        if (!panel.cwd.empty()) {
            if (::chdir(panel.cwd.c_str()) != 0) {
                // No es fatal — continuar desde el cwd heredado.
            }
        }
        ::execvp(sh.c_str(), const_cast<char* const*>(argv));
        // Si execvp falla, el hijo muere.
        _exit(127);
    }

    // Proceso padre.
    // Poner el fd maestro en modo no-bloqueante.
    int flags = ::fcntl(master_fd, F_GETFL, 0);
    ::fcntl(master_fd, F_SETFL, flags | O_NONBLOCK);

    panel.master_fd = master_fd;
    panel.child_pid = pid;
    panel.process_exited.store(false);
    panel.reader_running.store(true);
    panel.reader_thread = std::thread(reader_thread_fn, &panel);

    fn_log::log_info("terminal_panel: opened shell '%s' pid=%d", sh.c_str(), pid);
}

void send(TerminalPanel& panel, const std::string& text) {
    if (!panel.is_open() || panel.readonly) return;
    if (text.empty()) return;
    const char* p   = text.c_str();
    ssize_t     rem = static_cast<ssize_t>(text.size());
    while (rem > 0) {
        ssize_t n = ::write(panel.master_fd, p, static_cast<size_t>(rem));
        if (n <= 0) {
            if (errno == EINTR) continue;
            fn_log::log_error("terminal_panel: write to pty failed: %s", strerror(errno));
            break;
        }
        p   += n;
        rem -= n;
    }
}

void close(TerminalPanel& panel) {
    // Señalar al reader thread que pare.
    panel.reader_running.store(false);

    // Cerrar el fd maestro del PTY; esto hace que el hijo reciba HUP.
    if (panel.master_fd >= 0) {
        ::close(panel.master_fd);
        panel.master_fd = -1;
    }

    // Matar al hijo si sigue vivo.
    if (panel.child_pid > 0) {
        ::kill(panel.child_pid, SIGTERM);
        int status = 0;
        // Esperar hasta 200 ms; si no terminó, SIGKILL.
        for (int i = 0; i < 20; i++) {
            if (::waitpid(panel.child_pid, &status, WNOHANG) > 0) break;
            usleep(10000);
        }
        ::kill(panel.child_pid, SIGKILL);
        ::waitpid(panel.child_pid, &status, 0);
        panel.child_pid = -1;
    }

    // Esperar al reader thread.
    if (panel.reader_thread.joinable()) panel.reader_thread.join();

    fn_log::log_info("terminal_panel: closed");
}

} // namespace fn_term

#endif // !_WIN32
