#include "util.h"
#include "error.h"
#include "eval.h"
#include "eval_python.h"
#include "eval_cpp.h"
#include "log.h"
#include "show.h"
#include "katom.h"
#include "file.h"
#include <algorithm>
#include <optional>
#include <unistd.h>
#include <sys/wait.h>

// Run a shell command and return its STANDARD OUTPUT, which becomes document
// text.  Two things the first version did not do, both of them the same defect
// as a msg() on the wrong stream -- output nobody chose to see, and a failure
// nobody was told about:
//
//   * stderr went straight to the user's terminal, unattributed.  It is not the
//     command's output in any of the three policy categories (CLAUDE.md,
//     "Command output policy"): it belongs to a subprocess a klammer invoked, at
//     a location the Locator can name.  It is captured here and reported at
//     "-v 1" -- a derived value, the sort of thing "-v 1" exists for.
//   * the exit status was discarded, so a command that failed contributed its
//     partial output (or nothing) to the document and said nothing.  A nonzero
//     status is now an error, with the status, the command, and whatever the
//     command said on stderr.  Some commands exit nonzero without failing --
//     "grep" finding no match is the usual one -- so the message names the
//     explicit way to say that was intended, "cmd || true".
//
// tex_to_pdf() in sks/document/document.cpp is the same pattern for xelatex:
// capture, then decide what to report.
std::string shell(State state, std::string command, Locator loc)
{
    command = state.subst(command);

    // popen gives one pipe, so stderr goes to a temporary file.  mkstemp rather
    // than a constructed name: several ktext runs may share /tmp.
    std::string err_path = (fs::temp_directory_path() / "ktext_shell_XXXXXX").string();
    std::vector<char> err_template(err_path.begin(), err_path.end());
    err_template.push_back('\0');
    int err_fd = mkstemp(err_template.data());
    if (err_fd == -1) {
        throw Environment_error(
            "Could not create a temporary file for the command's error output", loc);
    }
    close(err_fd);
    err_path = err_template.data();

    // The braces keep the redirection outside the writer's command, so a
    // command containing its own pipeline or redirection still works.
    std::string wrapped = "{ " + command + " ; } 2>" + q_(err_path);
    FILE* pipe = popen(wrapped.c_str(), "r");
    if (!pipe) {
        fs::remove(err_path);
        throw Environment_error(
            "Could not run command:\n  " + command, loc);
    }
    char buffer[128];
    std::string result = "";
    while (fgets(buffer, sizeof(buffer), pipe) != nullptr) {
        result += buffer;
    }
    int status = pclose(pipe);
    std::string error_output = trim_right(string_from_file(err_path));
    fs::remove(err_path);

    int exit_status = WIFEXITED(status) ? WEXITSTATUS(status) : -1;
    if (exit_status != 0) {
        std::stringstream ss {};
        ss << "The shell command failed";
        if (WIFEXITED(status)) {
            ss << " (exit status " << exit_status << ")";
        } else if (WIFSIGNALED(status)) {
            ss << " (killed by signal " << WTERMSIG(status) << ")";
        }
        ss << ":\n  " << command;
        if (!error_output.empty()) {
            ss << "\nIt reported:\n  " << error_output;
        }
        // Do not echo the command back into the suggestion: appending to an
        // arbitrary command can produce nonsense ("exit 3 || true" cannot
        // work, since exit terminates before || is reached).
        ss << "\nIf a nonzero status is expected -- \"grep\" finding no match, "
              "say -- end the command with \"|| true\" to say so.";
        // "environment", not "parsing": the failure is OUTSIDE Klammertext,
        // in the command the document invoked -- the same class as a missing
        // xelatex or an unset environment variable.
        throw Environment_error(ss.str(), loc);
    }
    if (!error_output.empty()) {
        (void)K::log(1, "shell command wrote to stderr:", command,
                     "\n  " + error_output);
    }
    return result;
}

bool is_haskell_file(const std::string& text)
{
    std::string trimmed = trim(text);
    if (trimmed.size() < 4) return false;
    if (trimmed.find(' ') != std::string::npos) return false;
    if (trimmed.find('\n') != std::string::npos) return false;
    return trimmed.substr(trimmed.size() - 3) == ".hs";
}

// Run a Haskell program with runghc and return its STANDARD OUTPUT, which
// becomes document text.  Same rule as shell() above, and it was broken the
// same way: the command ran with "2>&1", so on a SUCCESSFUL run everything the
// program (or GHC) wrote to stderr was merged into the result and became part
// of the document.  A program printing a progress note or a warning silently
// contributed it to the output.
//
// A compile error was already reported rather than swallowed -- the exit status
// was checked -- so what changes here is the successful case, the error's type,
// and where a failure's detail comes from.
std::string run_haskell(const std::string& hsfile, Locator loc)
{
    std::string err_path = (fs::temp_directory_path() / "ktext_haskell_XXXXXX").string();
    std::vector<char> err_template(err_path.begin(), err_path.end());
    err_template.push_back('\0');
    int err_fd = mkstemp(err_template.data());
    if (err_fd == -1) {
        throw Environment_error(
            "Could not create a temporary file for runghc's error output", loc);
    }
    close(err_fd);
    err_path = err_template.data();

    std::string command = "runghc " + q_(hsfile) + " 2>" + q_(err_path);
    FILE* pipe = popen(command.c_str(), "r");
    if (!pipe) {
        fs::remove(err_path);
        throw Environment_error("Could not run runghc.", loc);
    }
    char buffer[128];
    std::string result = "";
    while (fgets(buffer, sizeof(buffer), pipe) != nullptr) {
        result += buffer;
    }
    int status = pclose(pipe);
    std::string error_output = trim_right(string_from_file(err_path));
    fs::remove(err_path);

    int exit_status = WIFEXITED(status) ? WEXITSTATUS(status) : -1;
    if (exit_status != 0) {
        std::stringstream ss {};
        ss << "The Haskell program failed";
        if (WIFEXITED(status)) {
            ss << " (exit status " << exit_status << ")";
        } else if (WIFSIGNALED(status)) {
            ss << " (killed by signal " << WTERMSIG(status) << ")";
        }
        ss << ".";
        if (!error_output.empty()) {
            ss << "\n\nrunghc reported:\n  " << error_output;
        }
        // A compile error arrives here, and so does a program that ran and then
        // exited nonzero; the message does not guess which, it shows what
        // runghc said.
        throw Environment_error(ss.str(), loc);
    }
    if (!error_output.empty()) {
        // GHC's warnings, and anything the program itself wrote to stderr.
        // Not document text -- reported at "-v 1", like a shell command's.
        (void)K::log(1, "runghc wrote to stderr:\n  " + error_output);
    }
    return result;
}

std::string haskell(State state, std::string code, Locator loc)
{
    if (system("command -v runghc > /dev/null 2>&1") != 0) {
        // "environment", not "parsing": a missing external command is the same
        // class as a missing xelatex, and "parsing error" misdescribes it.
        throw Environment_error(
            "@eval with the :haskell argument requires runghc, which was not found in PATH.\n\n"
            "Install it using GHCup; see https://www.haskell.org/ghcup/install/.",
            loc);
    }
    code = state.subst(code);

    if (is_haskell_file(code)) {
        return run_haskell(trim(code), loc);
    }

    const char* tmpdir = std::getenv("TMPDIR");
    if (!tmpdir) tmpdir = "/tmp";
    std::string tmpl = std::string(tmpdir) + "/klammertext_haskell_XXXXXX";
    std::vector<char> tmppath(tmpl.begin(), tmpl.end());
    tmppath.push_back('\0');
    int fd = mkstemp(tmppath.data());
    if (fd < 0) {
        throw Environment_error(
            "Could not create temporary file for Haskell evaluation.", loc);
    }
    std::string hsfile = std::string(tmppath.data()) + ".hs";
    close(fd);
    rename(tmppath.data(), hsfile.c_str());

    FILE* f = fopen(hsfile.c_str(), "w");
    if (!f) {
        unlink(hsfile.c_str());
        throw Parsing_error("Could not write temporary Haskell file.", loc);
    }
    fprintf(f, "%s\n", code.c_str());
    fclose(f);

    std::string result = run_haskell(hsfile, loc);
    unlink(hsfile.c_str());
    return result;
}

void check_cpp_arguments(katom_list args, Locator loc)
{
    if (args.size() != 4 && args.size() != 5) {
        std::stringstream ss {};
        ss << "Incorrect @eval format for a C++ function. Either:\n"
           << "   @eval :cpp <library-basename> @\n"
           << "or\n"
           << "   @eval :cpp <library-basename> <function-name> @\n"
           << "In the first case, the library basename is used for the function name.";
        throw Argument_error(ss.str(), loc);
    }
}


// Save the process working directory, change to DIR, and restore on
// destruction (exception-safe), so an @eval's :cwd cannot leak into the
// rest of the run.  NOTE: the cwd is process-global state; if input files
// are ever processed in parallel, this needs rethinking.
class Cwd_guard
{
public:
    explicit Cwd_guard(const std::string& dir)
        : m_saved(fs::current_path())
    {
        fs::current_path(dir);
    }
    ~Cwd_guard()
    {
        std::error_code ec;
        fs::current_path(m_saved, ec);   // never throw from a destructor
    }
    Cwd_guard(const Cwd_guard&) = delete;
    Cwd_guard& operator=(const Cwd_guard&) = delete;
private:
    fs::path m_saved;
};


std::string Eval::eval_command(katom_iter begin, katom_iter end)
{
    (void)K::log(3, *begin, *(end - 1));
    //msg() << "in Eval::eval:\n" << ktype << kall << kindex << std::pair(begin, end) << "\n";
    katom_iter first = after_whitespace(begin + 1);
    std::string eval_result = "[unevaluated]";

    // :cwd DIR — run the eval (any mode) with DIR as the working directory,
    // restoring the process cwd afterwards.  The default is the directory
    // ktext was started in (unchanged behavior).  DIR may hold state
    // substitutions (:cwd *K_input_dir* is the document's directory) and,
    // per the filenames-with-spaces convention, whitespace-separated tokens
    // are joined until they name an existing directory.
    std::optional<Cwd_guard> cwd_guard {};
    if (first->m_text == ":cwd") {
        katom_iter tok = after_whitespace(first + 1);
        std::string dir {};
        katom_iter cursor = tok;
        katom_iter resume = tok;
        while (cursor < end - 1 && !cursor->m_text.starts_with(":")) {
            dir = m_machine.m_state.subst(as_string(tok, cursor + 1, true));
            resume = after_whitespace(cursor + 1);
            if (fs::is_directory(dir)) break;
            cursor = resume;
        }
        if (dir.empty() || !fs::is_directory(dir)) {
            throw Argument_error(
                "The :cwd directory does not exist: \"" + dir + "\"",
                begin->m_loc);
        }
        cwd_guard.emplace(dir);
        first = resume;
    }

    std::string first_word = first->m_text;
    int offset = first_word[0] == ':' ? 1 : 0;
    std::string command = as_string(first + offset, end - 1, true);

    if (offset == 0 || first_word == ":python") {  // Default is Python
        Eval_python E_python(m_machine, begin->m_loc);
        eval_result = E_python.eval(command);
    } else if (first_word == ":shell") {
        eval_result = shell(m_machine.m_state, command, begin->m_loc);
    } else if (first_word == ":haskell") {
        eval_result = haskell(m_machine.m_state, command, begin->m_loc);
    } else if (first_word == ":cpp") {
        //msg() << ":cpp: " << first_word << *(begin + 4) << "\n";
        for (auto ki = begin; ki < end; ki++) {
            //msg() << "  " << kall << kindex << *ki << "\n";
        }
        katom_list args = text_katoms(begin, end);
        check_cpp_arguments(args, begin->m_loc);
        //msg() << "args: |" << args << "|\n";
        std::string lib_text = args[2].m_text;
        std::string khome = m_machine.m_state.value("KLAMMERTEXT_HOME", false);
        if (!khome.empty()) {
            lib_text = string_replace(lib_text, "*KLAMMERTEXT_HOME*", khome);
        }
        fs::path libpath(lib_text + ".so");
        // A relative library name not found from the cwd is searched in the
        // directories of the files the Machine has read (same rule as the
        // Python module path: the library lives next to the file using it).
        if (libpath.is_relative() && !fs::exists(fs::absolute(libpath))) {
            for (const auto& dir : m_machine.m_state.m_search_dirs) {
                fs::path candidate = fs::path(dir) / libpath;
                if (fs::exists(candidate)) {
                    libpath = candidate;
                    break;
                }
            }
        }
        libpath = fs::absolute(libpath);
        std::string funcname = args.size() == 4 ? libpath.stem().string() : args[3].m_text;
        Eval_cpp E_cpp(m_machine, begin->m_loc);
        eval_result = E_cpp.eval(libpath, funcname);
    }
    return eval_result;
}

katom_list Eval::eval(katom_iter begin, katom_iter end)
{
    std::string eval_result = eval_command(begin, end);
    katom_list result {};
    Machine M = m_machine;
    M.m_warn_unparsed = false; // Result text is machine-generated (see machine.h)
    size_t before = M.m_katoms.size();
    M.read(eval_result);
    // If the @eval produced more Klammertext -- the read-back result still
    // holds klammers to reduce (a "generator", e.g. a Python klammer returning
    // a @table call with data) -- then its text is writer content: escape it
    // for the target before applying, exactly as typed input is escaped, so a
    // "%" in the data becomes "\%".  If it produced final target markup (no
    // klammers left, a "renderer" such as @table itself emitting \begin{tabular})
    // leave it untouched.  ^'...'^ literal spans are skipped by the escape pass,
    // so a generator can still carry raw target markup.
    bool has_klammer = std::any_of(
        M.m_katoms.begin() + before, M.m_katoms.end(), begin_apply);
    M.apply(M.m_state.value("K_target"), false, has_klammer);
    result = trim(M.m_katoms);
    return result;
}

