// main.cpp — Blue: JS-to-C++ compiler entry point
//
//   blue -compile <input.js> [-o out] [-cflags "extra"]
//   blue -init <dir>
//   blue -build <dir> [-o app] [--no-inline-html]

#ifndef _WIN32
#include <unistd.h>
#include <sys/wait.h>
#else
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
/* MSVC/MinGW: alias POSIX names */
#define popen  _popen
#define pclose _pclose
/* getpid() → GetCurrentProcessId() */
inline int getpid() { return static_cast<int>(GetCurrentProcessId()); }
/* pclose() on Windows returns the exit code directly, not POSIX-encoded */
#define WIFEXITED(s)   (true)
#define WEXITSTATUS(s) (s)
#endif
#ifdef __APPLE__
#include <mach-o/dyld.h>
#endif

#include <climits>
#include <cstdio>
#include <cstdlib>
#include <cstring>

#include <chrono>
#include <filesystem>
#include <fstream>
#include <functional>
#include <iomanip>
#include <iostream>
#include <sstream>
#include <stdexcept>
#include <string>
#include <vector>

#include <nlohmann/json.hpp>

#include "ast_node.hpp"
#include "emitter.hpp"
#include "jsc_bundle.hpp"
#include "jsc_embed_generator.hpp"
#include "jsc_npm_bundle.hpp"
#include "module_resolver.hpp"
#include "quickjs.h"

#ifndef PATH_MAX
#define PATH_MAX 4096
#endif

namespace fs = std::filesystem;

/* Directory containing package.json, walking up from entry's directory. */

static std::string readFileToString(const std::string& path) {
    std::ifstream file(path, std::ios::in | std::ios::binary);
    if (!file.is_open())
        throw std::runtime_error("blue: cannot open file: " + path);
    std::ostringstream buf;
    buf << file.rdbuf();
    return buf.str();
}

static bool packageLooksReactProject(const fs::path& packageJsonPath) {
    if (!fs::exists(packageJsonPath))
        return false;
    nlohmann::json pj;
    {
        std::ifstream in(packageJsonPath);
        if (!in)
            return false;
        in >> pj;
    }
    auto hasDep = [&](const char* section, const char* name) -> bool {
        return pj.contains(section) && pj[section].is_object() &&
               pj[section].contains(name);
    };
    bool hasReact = hasDep("dependencies", "react") ||
                    hasDep("devDependencies", "react") ||
                    hasDep("peerDependencies", "react");
    bool hasReactDom = hasDep("dependencies", "react-dom") ||
                       hasDep("devDependencies", "react-dom") ||
                       hasDep("peerDependencies", "react-dom");
    return hasReact || hasReactDom;
}

static void maybeSignExecutable(const fs::path& bin) {
#ifdef __APPLE__
    const char* ident = std::getenv("CODESIGN_IDENTITY");
    if (ident && *ident) {
        std::string cmd = std::string("codesign --force --sign \"") + ident +
                          "\" \"" + bin.string() + "\" 2>&1";
        int rc = std::system(cmd.c_str());
        std::cerr << "blue: codesign exited " << rc << "\n";
    }
#elif defined(_WIN32)
    const char* sg = std::getenv("SIGNTOOL_PATH");
    if (sg && *sg && std::getenv("SIGN_CERT_THUMBPRINT")) {
        std::ostringstream o;
        o << "\"" << sg << "\" sign /sha1 "
          << std::getenv("SIGN_CERT_THUMBPRINT")
          << " /tr http://timestamp.digicert.com /td sha256 /fd "
             "sha256 \""
          << bin.string().c_str() << "\"";
        std::system(o.str().c_str());
    }
#endif
    (void)bin;
}

struct QjsGuard {
    JSRuntime* rt;
    JSContext* ctx;
    QjsGuard() : rt(JS_NewRuntime()), ctx(nullptr) {
        if (rt)
            ctx = JS_NewContext(rt);
    }
    ~QjsGuard() {
        if (ctx)
            JS_FreeContext(ctx);
        if (rt)
            JS_FreeRuntime(rt);
    }
    QjsGuard(const QjsGuard&)            = delete;
    QjsGuard& operator=(const QjsGuard&) = delete;
};

static void qjsThrowIfException(JSContext* ctx, const std::string& what) {
    JSValue       ex = JS_GetException(ctx);
    const char* msg = JS_ToCString(ctx, ex);
    std::string err = what + ": ";
    err += msg ? msg : "exception";
    if (msg)
        JS_FreeCString(ctx, msg);
    JS_FreeValue(ctx, ex);
    throw std::runtime_error(err);
}

static void qjsEvalScript(JSContext* ctx, const std::string& code,
                          const char* filename) {
    JSValue v = JS_Eval(ctx, code.data(), code.size(), filename,
                        JS_EVAL_TYPE_GLOBAL);
    if (JS_IsException(v)) {
        JS_FreeValue(ctx, v);
        qjsThrowIfException(ctx, std::string("blue: ") + filename);
    }
    JS_FreeValue(ctx, v);
}

#ifndef BLUE_VERSION
#define BLUE_VERSION "dev"
#endif

static void printUsage(const char* prog) {
    const char* name = prog;
    if (const char* slash = strrchr(prog, '/')) name = slash + 1;
    // Show "blue" regardless of whether invoked as blue_bin or via the wrapper
    const char* display = (strcmp(name, "blue_bin") == 0) ? "blue" : name;
    std::cerr << "Blue " << BLUE_VERSION << " — JavaScript-to-native compiler\n\n"
              << "Usage:\n"
              << "  " << display << " -compile <input.js> [-o out] [-cflags \"extra\"]\n"
              << "  " << display << " -init [<dir>] [--build]\n"
              << "  " << display << " -build [<dir>] [-o app] [--no-inline-html] [--time]\n"
              << "  " << display << " run [<dir>] [--time] [-- args...]\n"
              << "  " << display << " --print-c <input.js>\n"
              << "  " << display << " --version\n"
              << "  " << display << " --help\n\n"
              << "Tip: run with --print-c to inspect the generated C++ for any input.\n"
              << "Tip: unsupported JS features? Move them to src/island.js (full JS).\n";
}

static const char* devNull() {
#ifdef _WIN32
    return "NUL";
#else
    return "/dev/null";
#endif
}

static std::string binDir(const char* argv0) {
#ifdef __linux__
    char buf[PATH_MAX + 1] = {};
    ssize_t n = ::readlink("/proc/self/exe", buf, sizeof(buf) - 1);
    if (n > 0) {
        buf[n] = '\0';
        return fs::absolute(buf).parent_path().generic_string();
    }
#endif
#ifdef _WIN32
    wchar_t wbuf[32768] = {};
    DWORD n = GetModuleFileNameW(NULL, wbuf, (DWORD)(sizeof(wbuf) / sizeof(wchar_t)) - 1);
    if (n > 0)
        return fs::path(wbuf).parent_path().generic_string();
#endif
    if (argv0 && argv0[0] == '/')
        return fs::path(argv0).parent_path().generic_string();
#ifdef __APPLE__
    char abuf[PATH_MAX + 1];
    uint32_t sz = sizeof(abuf);
    if (_NSGetExecutablePath(abuf, &sz) == 0)
        return fs::absolute(abuf).parent_path().generic_string();
#endif
    char tmp[PATH_MAX];
    std::strncpy(tmp, argv0 ? argv0 : "", sizeof(tmp) - 1);
    tmp[sizeof(tmp) - 1] = '\0';
    return fs::absolute(fs::current_path() / fs::path(tmp))
        .parent_path()
        .generic_string();
}

static std::string readPipe(const char* cmd) {
    FILE* pf = ::popen(cmd, "r");
    if (!pf) {
        std::cerr << "blue: warning: popen failed for: " << cmd << "\n";
        return "";
    }
    std::ostringstream acc;
    char               buf[2048];
    size_t nr;
    while ((nr = fread(buf, 1, sizeof(buf), pf)) > 0)
        acc.write(buf, nr);
    pclose(pf);
    std::string s = acc.str();
    while (!s.empty() && (s.back() == '\n' || s.back() == '\r'))
        s.pop_back();
    return s;
}

/* pkg-config --cflags/-libs emit newlines → sh splits the driver command. */
static std::string flattenFlagsForShell(std::string s) {
    for (char& c : s) {
        if (c == '\n' || c == '\r' || c == '\t')
            c = ' ';
    }
    std::string out;
    out.reserve(s.size());
    bool sp = false;
    for (unsigned char uc : s) {
        char c = (char)uc;
        if (c == ' ') {
            if (!sp) {
                out += ' ';
                sp = true;
            }
        } else {
            out += c;
            sp = false;
        }
    }
    while (!out.empty() && out.front() == ' ')
        out.erase(out.begin());
    while (!out.empty() && out.back() == ' ')
        out.pop_back();
    return out;
}

static bool shellCommandFailed(int st) {
#ifdef _WIN32
    return st != 0;
#else
    if (st == -1)
        return true;
    if (!WIFEXITED(st))
        return true;
    return WEXITSTATUS(st) != 0;
#endif
}

static std::string readPkgCfgLine(const char* cmd) {
    return flattenFlagsForShell(readPipe(cmd));
}

static std::vector<uint8_t> readFileToBytes(const fs::path& p) {
    std::ifstream in(p, std::ios::binary);
    if (!in)
        throw std::runtime_error("cannot read " + p.generic_string());
    return std::vector<uint8_t>((std::istreambuf_iterator<char>(in)),
                                std::istreambuf_iterator<char>());
}

static std::string emitControlPlaneCpp(const std::vector<uint8_t>& bundle) {
    std::ostringstream o;
    o << "/* blue generated: control plane (QuickJS + native I/O) */\n";
    o << "#define JSC_RUNTIME_QUICKJS 1\n";
    o << "#define JSC_RUNTIME_UV 1\n";
    o << "#define JSC_HAVE_UV 1\n";
    o << "#define JSC_RUNTIME_NODE_IO 1\n";
    o << "#define JSC_QJS_CONTROL_PLANE 1\n";
    o << "#include \"runtime/jsc_runtime.h\"\n";
    o << "#include <stddef.h>\n";
    o << "static const unsigned char jsc_control_bundle[] = {";
    for (size_t i = 0; i < bundle.size(); ++i) {
        if (i)
            o << ',';
        if (i % 20 == 0)
            o << '\n';
        o << "0x" << std::hex << std::setw(2) << std::setfill('0')
          << (unsigned)bundle[i];
    }
    /* Trailing NUL: some code paths use strlen() on the buffer; without this,
     * evaluation can read past the script into adjacent rodata ('<'). */
    if (!bundle.empty())
        o << ',';
    o << "0x00\n};\nstatic const size_t jsc_control_bundle_len = " << std::dec
      << bundle.size() << ";\n";
    o << "int main() {\n";
    o << "  jsc_control_plane_run_embedded(jsc_control_bundle, "
         "jsc_control_bundle_len);\n";
    o << "  jsc_uv_run_loop_forever();\n";
    o << "  return 0;\n}\n";
    return o.str();
}

static std::string emitHybridIslandBlob(const std::vector<uint8_t>& bundle) {
    std::ostringstream o;
    o << "/* blue generated: embedded QuickJS npm island (true hybrid) */\n";
    o << "static const unsigned char jsc_hybrid_island_bundle[] = {";
    for (size_t i = 0; i < bundle.size(); ++i) {
        if (i)
            o << ',';
        if (i % 20 == 0)
            o << '\n';
        o << "0x" << std::hex << std::setw(2) << std::setfill('0')
          << (unsigned)bundle[i];
    }
    if (!bundle.empty())
        o << ',';
    o << "0x00\n};\nstatic const unsigned long long jsc_hybrid_island_bundle_len = "
      << std::dec << bundle.size() << "ULL;\n";
    return o.str();
}

static void invokeCompiler(const std::string& binDirectory, const std::string& cxx,
                           const fs::path& tmpCc, const std::string& outputBinary,
                           const std::string& cppSource, bool forceWebviewExtra,
                           const fs::path& embedCppGenerated,
                           const std::string& extraCFlags) {
    fs::path    jsIncludeP = fs::path(binDirectory) / "src";
    std::string jsInclude =
        fs::weakly_canonical(jsIncludeP).generic_string();

    fs::path qjsVendorIncl = fs::weakly_canonical(
        fs::path(binDirectory) / "vendor" / "quickjs");
    fs::path qjsObjsDir =
        fs::weakly_canonical(fs::path(binDirectory) / "vendor" / "quickjs" /
                               "obj");

    bool wantsQ =
        cppSource.find("#define JSC_RUNTIME_QUICKJS") != std::string::npos ||
        cppSource.find("jsc_quickjs_") != std::string::npos;
    bool wantsU =
        cppSource.find("#define JSC_RUNTIME_UV") != std::string::npos ||
        cppSource.find("jsc_uv_") != std::string::npos;
    bool wantsIo =
        cppSource.find("JSC_RUNTIME_NODE_IO") != std::string::npos;
    bool wantsCp =
        cppSource.find("JSC_QJS_CONTROL_PLANE") != std::string::npos ||
        cppSource.find("jsc_control_plane_run_embedded") != std::string::npos;
    bool wantsHybrid =
        cppSource.find("JSC_HYBRID") != std::string::npos ||
        cppSource.find("jsc_hybrid_boot_island") != std::string::npos;
    bool wantsW =
        forceWebviewExtra ||
        cppSource.find("#define JSC_RUNTIME_WEBVIEW") != std::string::npos;

    bool haveUvLib =
        (::system((std::string("pkg-config --exists libuv >") + devNull() + " 2>&1").c_str()) == 0);
#ifdef _WIN32
    if (!haveUvLib) {
        const char* uvp = std::getenv("LIBUV_PATH");
        haveUvLib = (uvp && uvp[0]);
    }
#endif

#if defined(__linux__)
    bool haveGtk =
        (::system((std::string("pkg-config --exists gtk+-3.0 >") + devNull() + " 2>&1").c_str()) == 0);
    std::string wkPc =
        (::system((std::string("pkg-config --exists webkit2gtk-4.1 >") + devNull() + " 2>&1").c_str()) == 0)
            ? "webkit2gtk-4.1"
            : "webkit2gtk-4.0";
    bool haveWk =
        (::system(("pkg-config --exists " + wkPc + " >" + devNull() + " 2>&1").c_str()) == 0);
#else
    bool           haveGtk = true;
    bool           haveWk  = true;
    std::string    wkPc    = "";
    (void)haveGtk; (void)haveWk; (void)wkPc;
#endif

    std::string extraSrc;
    std::string extraObjs;

    if (wantsQ) {
        extraSrc += " \"" + jsInclude + "/runtime/jsc_quickjs_fallback.cpp\"";
        static const char* objs[] = {"quickjs.o",    "dtoa.o",       "libregexp.o",
                                     "libunicode.o", "cutils.o",     "quickjs-libc.o"};
        for (const char* fo : objs)
            extraObjs += " \"" + (qjsObjsDir / fo).generic_string() + "\"";
    }
    if (wantsQ || wantsU)
        extraSrc += " \"" + jsInclude + "/runtime/jsc_event_loop.cpp\"";
    extraSrc += " \"" + jsInclude + "/runtime/jsc_node_io.cpp\"";
    if (wantsCp || wantsHybrid) {
        extraSrc += " \"" + jsInclude + "/runtime/jsc_qjs_node_bridge.cpp\"";
        extraSrc += " \"" + jsInclude + "/runtime/jsc_blue_qjs.cpp\"";
    }
    if (wantsHybrid)
        extraSrc += " \"" + jsInclude + "/runtime/jsc_hybrid_runtime.cpp\"";

    fs::path webviewTU = fs::weakly_canonical(
        jsIncludeP / "runtime" / "jsc_webview_stub.cpp");
    fs::path embedTU = fs::weakly_canonical(
        jsIncludeP / "runtime" / "jsc_embed_stub.cpp");

    if (wantsW) {
#if defined(__linux__)
        if (!haveGtk || !haveWk)
            throw std::runtime_error(
                "WebView builds need gtk+-3.0 + webkit2gtk-4.x dev packages.");
        webviewTU =
            fs::weakly_canonical(jsIncludeP / "runtime" / "linux" / "jsc_webview_linux.cpp");
#elif defined(__APPLE__)
        webviewTU =
            fs::weakly_canonical(jsIncludeP / "runtime" / "macos" / "jsc_webview_mac.mm");
#else
        webviewTU =
            fs::weakly_canonical(jsIncludeP / "runtime" / "windows" / "jsc_webview_win.cpp");
#endif
        if (!embedCppGenerated.empty() && fs::exists(embedCppGenerated))
            embedTU = embedCppGenerated;

        extraSrc += " \"" + webviewTU.generic_string() + "\" \"" +
                    embedTU.generic_string() + "\"";
    } else {
        extraSrc += " \"" + webviewTU.generic_string() + "\"";
    }

    extraSrc += " \"" + jsInclude + "/runtime/jsc_blue_native_core.cpp\"";
    if (wantsW) {
#if defined(__linux__)
        extraSrc += " \"" + jsInclude + "/runtime/linux/jsc_blue_native_linux.cpp\"";
#elif defined(_WIN32)
        extraSrc += " \"" + jsInclude + "/runtime/windows/jsc_blue_native_win.cpp\"";
#else
        extraSrc += " \"" + jsInclude + "/runtime/jsc_blue_native_gui_stub.cpp\"";
#endif
    } else {
        extraSrc += " \"" + jsInclude + "/runtime/jsc_blue_native_gui_stub.cpp\"";
    }

    std::string gtkCflags, gtkLibs, wkCflags, wkLibs;
#if defined(__linux__)
    if (wantsW && haveGtk && haveWk) {
        gtkCflags = readPkgCfgLine("pkg-config --cflags gtk+-3.0 2>/dev/null");
        gtkLibs   = readPkgCfgLine("pkg-config --libs gtk+-3.0 2>/dev/null");
        wkCflags  = readPkgCfgLine(("pkg-config --cflags " + wkPc + " 2>/dev/null").c_str());
        wkLibs    = readPkgCfgLine(("pkg-config --libs " + wkPc + " 2>/dev/null").c_str());
    }
#endif

    std::string uvCflags, uvLibs;
    if (wantsU && haveUvLib) {
        uvCflags = readPkgCfgLine("pkg-config --cflags libuv 2>/dev/null");
#if defined(__linux__)
        /* Prefer static libuv so the output binary has no libuv.so runtime dep. */
        {
            static const char* candidates[] = {
                "/usr/lib/x86_64-linux-gnu/libuv.a",
                "/usr/lib/aarch64-linux-gnu/libuv.a",
                "/usr/lib/arm-linux-gnueabihf/libuv.a",
                "/usr/lib/libuv.a",
                "/usr/local/lib/libuv.a",
                nullptr
            };
            for (int ci = 0; candidates[ci]; ++ci) {
                if (fs::exists(candidates[ci])) {
                    uvLibs = std::string("\"") + candidates[ci] + "\" -lpthread -ldl -lrt";
                    break;
                }
            }
        }
        if (uvLibs.empty())
#endif
        uvLibs = readPkgCfgLine("pkg-config --libs libuv 2>/dev/null");
    }
#ifdef _WIN32
    if (wantsU && haveUvLib && uvLibs.empty()) {
        const char* uvp = std::getenv("LIBUV_PATH");
        if (uvp && uvp[0]) {
            std::string p(uvp);
            uvCflags = "-I\"" + p + "/include\"";
            uvLibs   = "-L\"" + p + "/lib\" -luv";
        }
    }
#endif

    bool haveSsl =
        (::system((std::string("pkg-config --exists openssl >") + devNull() + " 2>&1").c_str()) == 0);
#ifdef _WIN32
    if (!haveSsl) {
        const char* sslp = std::getenv("OPENSSL_PATH");
        haveSsl = (sslp && sslp[0]);
    }
#endif
    std::string sslCflags, sslLibs;
    if (wantsIo && haveSsl) {
        sslCflags = readPkgCfgLine("pkg-config --cflags openssl 2>/dev/null");
        sslLibs   = readPkgCfgLine("pkg-config --libs openssl 2>/dev/null");
    }

    std::string compileCmd;
    compileCmd += "\"" + cxx + "\" ";
#if defined(__APPLE__)
    if (wantsW)
        compileCmd += "-ObjC++ ";
#endif
    compileCmd +=
        "-std=c++17 -O2 -Wall -Wextra -I\"" + jsInclude + "\"";

    if (!gtkCflags.empty())
        compileCmd += " " + gtkCflags;
    if (!wkCflags.empty())
        compileCmd += " " + wkCflags;
    if (wantsIo && haveSsl && !sslCflags.empty())
        compileCmd += " " + sslCflags;

    if (wantsQ)
        compileCmd += " -I\"" + qjsVendorIncl.generic_string() + "\"";

    compileCmd += " -o \"" + outputBinary + "\" \"" + tmpCc.generic_string() +
                   "\"" + extraSrc + extraObjs;

    if (wantsU && haveUvLib) {
        if (!uvCflags.empty())
            compileCmd += " " + uvCflags;
        compileCmd += " -DJSC_HAVE_UV=1";
    }
    if (wantsIo) {
        if (!haveUvLib)
            throw std::runtime_error(
                "blue: require('http'|'net'|…) needs libuv — install libuv "
                "development packages (pkg-config libuv).");
        compileCmd += " -DJSC_RUNTIME_NODE_IO=1";
    }
    if (wantsIo && haveSsl) {
        compileCmd += " -DJSC_HAVE_OPENSSL=1";
    }
    if (wantsCp || wantsHybrid)
        compileCmd += " -DJSC_QJS_CONTROL_PLANE=1";
    if (wantsHybrid)
        compileCmd += " -DJSC_HYBRID=1";
    if (wantsQ)
        compileCmd += " -DJSC_RUNTIME_QUICKJS=1";

#ifndef _WIN32
    compileCmd += " -lm -pthread";
#endif
#if defined(__linux__)
    compileCmd += " -ldl";
#endif

#ifdef __APPLE__
    if (wantsW)
        compileCmd += " -framework WebKit -framework Cocoa ";
#endif

#ifdef _WIN32
    {
        bool isMSVC = (cxx.find("cl") != std::string::npos &&
                       cxx.find("clang") == std::string::npos &&
                       cxx.find("g++") == std::string::npos);

        /* GUI / Window APIs */
        if (wantsW) {
            if (isMSVC)
                compileCmd += " user32.lib gdi32.lib ole32.lib shell32.lib"
                              " comdlg32.lib comctl32.lib oleaut32.lib uuid.lib";
            else
                compileCmd += " -luser32 -lgdi32 -lole32 -lshell32"
                              " -lcomdlg32 -lcomctl32 -loleaut32 -luuid";
        }

        /* libuv on Windows requires these system libraries. */
        if (wantsU && haveUvLib) {
            if (isMSVC)
                compileCmd += " ws2_32.lib iphlpapi.lib userenv.lib dbghelp.lib psapi.lib";
            else
                compileCmd += " -lws2_32 -liphlpapi -luserenv -ldbghelp -lpsapi";
        }

        /* WebView2 SDK */
        const char* wv2sdk = std::getenv("WEBVIEW2_SDK_PATH");
        if (wv2sdk && wv2sdk[0] && wantsW) {
            std::string sdk(wv2sdk);
            compileCmd += " -DBLUE_HAVE_WEBVIEW2=1";
            compileCmd += " -I\"" + sdk + "/build/native/include\"";
            if (isMSVC)
                compileCmd += " \"" + sdk + "/build/native/x64/WebView2Loader.dll.lib\"";
            else
                compileCmd += " -L\"" + sdk + "/build/native/x64\" -lWebView2Loader.dll";
        }

        /* MinGW: statically link C++ and pthread runtimes so the binary
         * doesn't depend on libstdc++-6.dll / libwinpthread-1.dll. */
        if (!isMSVC)
            compileCmd += " -static-libgcc -static-libstdc++ -Wl,-Bstatic -lpthread -Wl,-Bdynamic";
    }
#endif

    if (!gtkLibs.empty())
        compileCmd += " " + gtkLibs;
    if (!wkLibs.empty())
        compileCmd += " " + wkLibs;

    compileCmd += extraCFlags.empty()
                       ? ""
                       : (" " + flattenFlagsForShell(extraCFlags));
    if (wantsU && haveUvLib && !uvLibs.empty())
        compileCmd += " " + uvLibs;
    if (wantsIo && haveSsl && !sslLibs.empty())
        compileCmd += " " + sslLibs;

    compileCmd += " 2>&1";

    int rc = std::system(compileCmd.c_str());
    if (shellCommandFailed(rc))
        throw std::runtime_error(
            "blue: C++ compiler failed.\n(hint run with --print-c to inspect)");

    maybeSignExecutable(outputBinary);
}

static void bootstrapQjs(JSContext* ctx, const std::string& binDirectory) {
    /* Try CWD first (development), then binDirectory (installed .deb). */
    auto readWithFallback = [&](const std::string& name) -> std::string {
        try {
            return readFileToString(name);
        } catch (...) {}
        std::string alt = (fs::path(binDirectory) / name).generic_string();
        try {
            return readFileToString(alt);
        } catch (...) {
            throw std::runtime_error(
                name + " not found in CWD or " + binDirectory +
                " — run make deps.");
        }
    };
    std::string esprimaSrc = readWithFallback("esprima.js");
    qjsEvalScript(ctx, esprimaSrc, "esprima.js");
    qjsEvalScript(ctx,
                  "if (typeof globalThis==='undefined'){var "
                  "globalThis=this;}if(typeof console==='undefined'){var "
                  "console={log:function(){},error:function(){},warn:"
                  "function(){},info:"
                  "function(){},debug:function(){}};} ",
                  "<shim>");
    std::string babelSrc = readWithFallback("babel.min.js");
    qjsEvalScript(ctx, babelSrc, "babel.min.js");
}

static void cmdInit(const fs::path& dir) {
    fs::create_directories(dir / "public");
    fs::create_directories(dir / "src");

    nlohmann::json cfg;
    cfg["entry"]              = "src/main.js";
    cfg["quickjsIsland"]      = "src/island.js";
    cfg["hybrid"]             = true;
    cfg["bundlePlatform"]     = "node";
    cfg["publicDir"]          = "public";
    cfg["bundleDependencies"] = false;
    cfg["controlPlane"]       = false;
    cfg["bundleHtml"] =
        std::vector<std::string>{"inline public/index.html for dev"};
    std::ofstream ac(dir / "blue.config.json");
    ac << cfg.dump(2) << "\n";

    const char* css = "body{font-family:sans-serif;margin:48px;background:#0f172a;"
                      "color:#e2e8f0;}h1{color:#38bdf8;}\n";
    std::ofstream style(dir / "public" / "style.css");
    style << css;

    const char* html =
        "<!DOCTYPE html>\n<html><head><meta charset=\"utf-8\"/>\n"
        "<link rel=\"stylesheet\" href=\"style.css\"/></head><body>"
        "<h1>blue GUI</h1>"
        "<p>Native bridge: <code>fetch('blue://app/__bridge__/'+encodeURIComponent(JSON."
        "stringify({cmd:\"ping\"})))</code> then listen for <code>blue-backend</code>.</p>"
        "<button id=\"b\">Ping backend</button><pre id=\"o\"></pre>"
        "<img alt=\"demo\" src=\"blue://app/app.svg\"/>"
        "<script>\n"
        "var o=document.getElementById('o');function L(m){o.textContent+=m+'\\n';}\n"
        "window.addEventListener('blue-backend',function(e){\n"
        " var d=e.detail||{};var s=d.payloadB64?atob(d.payloadB64):'';\n"
        " L('reply '+JSON.stringify(d)+' '+s);});\n"
        "document.getElementById('b').onclick=function(){\n"
        " var p=encodeURIComponent(JSON.stringify({cmd:'ping'}));\n"
        " fetch('blue://app/__bridge__/'+p).then(function(r){return r.text();})\n"
        " .then(function(t){L('fetch '+t);}).catch(function(e){L(String(e));});};\n"
        "</script></body></html>\n";
    std::ofstream idx(dir / "public" / "index.html");
    idx << html;

    const char* svg = "<svg xmlns=\"http://www.w3.org/2000/svg\" "
                      "width=\"120\" "
                      "height=\"80\"><rect fill=\"#38bdf8\" width=\"120\" "
                      "height=\"80\""
                      "/><text x=\"12\" "
                      "y=\"48\" "
                      "font-size=\"20\" "
                      "fill=\"#0f172a\">SVG</text></svg>\n";
    std::ofstream svgf(dir / "public" / "app.svg");
    svgf << svg;

    const char* mj =
        "\"use strict\";\n"
        "var html = __BLUE_BUNDLE_HTML__;\n"
        "window.open(html, \"Blue App\", 900, 600);\n";
    std::ofstream m(dir / "src" / "main.js");
    m << mj;

    const char* islandJs =
        "\"use strict\";\n"
        "const http = require(\"http\");\n\n"
        "const server = http.createServer((req, res) => {\n"
        "  if (req.url === \"/\") {\n"
        "    res.statusCode = 200;\n"
        "    res.setHeader(\"content-type\", \"text/plain; charset=utf-8\");\n"
        "    res.end(\"hello from blue island\");\n"
        "    return;\n"
        "  }\n"
        "  res.statusCode = 404;\n"
        "  res.end(\"not found\");\n"
        "});\n\n"
        "server.listen(48313, \"127.0.0.1\", () => {\n"
        "  console.log(\"blue island listening on http://127.0.0.1:48313\");\n"
        "});\n";
    std::ofstream is(dir / "src" / "island.js");
    is << islandJs;

    std::cerr << "blue: scaffolded project in "
              << fs::absolute(dir).generic_string() << "\n";
}

int main(int argc, char** argv) {
    if (argc < 2) {
        printUsage(argv[0]);
        return EXIT_FAILURE;
    }

    bool        doCompile = false;
    bool        printC    = false;
    bool        doInit    = false;
    bool        doBuild   = false;
    bool        doRun     = false;
    bool        doBenchmark = false;
    bool        initThenBuild = false;
    bool        noInlineHtml = false;
    std::string projDirInit, projDirBuild, inputPath, outputBinary,
        extraCFlags;
    std::vector<std::string> appArgs;

    for (int i = 1; i < argc; ++i) {
        std::string a = argv[i];
        if (a == "--version" || a == "-version") {
            std::cout << "blue " << BLUE_VERSION << "\n";
            return EXIT_SUCCESS;
        }
        if (a == "-h" || a == "--help") {
            printUsage(argv[0]);
            return EXIT_SUCCESS;
        }
        if (a == "-compile")
            doCompile = true;
        else if (a == "--print-c")
            printC = true;
        else if (a == "-init" || (a == "init" && !doBuild)) {
            if (i + 1 < argc && argv[i+1][0] != '-') {
                projDirInit = argv[++i];
                doInit = true;
            } else {
                projDirInit = ".";
                doInit = true;
            }
        }
        else if (a == "-build" || a == "build" || a == "run" || (a == "init" && doBuild)) {
            if (a == "run") doRun = true;
            if (i + 1 < argc && std::string(argv[i + 1]) == "init") {
                if (i + 2 >= argc) {
                    std::cerr << "blue: " << a << " init requires <dir>\n";
                    return EXIT_FAILURE;
                }
                initThenBuild = true;
                projDirBuild  = argv[i + 2];
                i += 2;
            } else if (i + 1 < argc && argv[i+1][0] != '-') {
                projDirBuild = argv[++i];
            } else {
                projDirBuild = ".";
            }
            doBuild = true;
        }
        else if (a == "--time" || a == "--benchmark")
            doBenchmark = true;
        else if (a == "--") {
            for (int j = i + 1; j < argc; ++j)
                appArgs.push_back(argv[j]);
            break;
        }
        else if (a == "--no-inline-html")
            noInlineHtml = true;
        else if (a == "-o" || a == "--output") {
            if (i + 1 >= argc) {
                std::cerr << "blue: -o requires a filename argument\n";
                return EXIT_FAILURE;
            }
            outputBinary = argv[++i];
        }
        else if (a == "-cflags") {
            if (i + 1 >= argc) {
                std::cerr << "blue: -cflags requires a flags argument\n";
                return EXIT_FAILURE;
            }
            extraCFlags = argv[++i];
        }
        else if (a[0] != '-')
            inputPath = a;
        else {
            std::cerr << "blue: unknown flag: " << a << "\n";
            printUsage(argv[0]);
            return EXIT_FAILURE;
        }
    }

    if (doInit) {
        try {
            cmdInit(projDirInit);
        } catch (const std::exception& ex) {
            std::cerr << ex.what() << "\n";
            return EXIT_FAILURE;
        }
        return EXIT_SUCCESS;
    }

    std::string binDirectory = binDir(argv[0]);

    if (doBuild) {
        auto t0 = std::chrono::steady_clock::now();
        try {
            fs::path proj = fs::absolute(projDirBuild);
            fs::path bd   = proj / ".blue-build";
            fs::create_directories(bd);

            if (initThenBuild)
                cmdInit(proj);

            fs::path cfgPath = proj / "blue.config.json";
            if (!fs::exists(cfgPath)) {
                std::ostringstream msg;
                msg << "missing blue.config.json in "
                    << proj.generic_string()
                    << "\n  create a project with:  " << argv[0]
                    << " -init " << proj.generic_string()
                    << "\n  or in one step:        " << argv[0]
                    << " -build init " << proj.generic_string();
                throw std::runtime_error(msg.str());
            }

            std::ifstream jc(cfgPath);
            nlohmann::json cf;
            jc >> cf;
            std::string pubName = cf.value("publicDir", std::string("public"));
            std::string entry   = cf.value("entry", std::string("src/main.js"));
            bool bundleDeps =
                cf.value("bundleDependencies", false);
            std::string bundlePlat =
                cf.value("bundlePlatform", std::string("neutral"));
            bool controlPlane = cf.value("controlPlane", false);
            bool hybrid       = cf.value("hybrid", false);
            std::string quickjsIsland =
                cf.value("quickjsIsland", std::string());

            if (controlPlane && hybrid)
                throw std::runtime_error(
                    "blue: config: \"controlPlane\" and \"hybrid\" "
                    "cannot both be true.");
            if (hybrid && quickjsIsland.empty())
                throw std::runtime_error(
                    "blue: hybrid builds require \"quickjsIsland\" "
                    "(path to island entry .js) in blue.config.json.");

            fs::path publicRoot = proj / pubName;
            fs::path entryJs    = proj / entry;
            if (!fs::exists(entryJs)) {
                throw std::runtime_error("blue: entry file not found: " +
                                         entryJs.generic_string());
            }
            fs::create_directories(publicRoot);

            // React project auto-frontend build:
            // If package.json indicates React and a frontend entry exists,
            // bundle it to public/app.bundle.js using esbuild.
            fs::path pkgJson = proj / "package.json";
            if (packageLooksReactProject(pkgJson)) {
                std::vector<fs::path> candidateEntries = {
                    proj / "src" / "frontend.jsx",
                    proj / "src" / "frontend.tsx",
                    proj / "src" / "frontend.js",
                    proj / "src" / "frontend.ts",
                };
                fs::path frontendEntry;
                for (const auto& c : candidateEntries) {
                    if (fs::exists(c)) {
                        frontendEntry = c;
                        break;
                    }
                }
                if (!frontendEntry.empty()) {
                    fs::path frontendBundle = publicRoot / "app.bundle.js";
                    jsc_npm_bundle::run(
                        binDirectory,
                        fs::absolute(frontendEntry).generic_string(),
                        proj.generic_string(),
                        fs::absolute(frontendBundle).generic_string(),
                        "browser");
                    std::cerr << "blue: built React frontend bundle "
                              << frontendBundle << "\n";
                }
            }

            std::vector<jsc_embed_gen::EmbedFile> assets =
                jsc_embed_gen::listPublicRecursive(publicRoot);

            if (!assets.empty())
                jsc_embed_gen::emitCpp(assets, bd / "jsc_assets.cpp");

            /* Bundle HTML referenced by JS: read main.js, replace sentinel */
            std::string mainTpl = readFileToString(entryJs.generic_string());

            fs::path    idxHtmlPath = publicRoot / "index.html";
            std::string bundledHtml =
                fs::exists(idxHtmlPath) && !noInlineHtml
                    ? jsc_bundle::inlineHtmlDeps(fs::canonical(idxHtmlPath),
                                                 fs::canonical(publicRoot))
                    : std::string("");

            if (!bundledHtml.empty()) {
                std::string esc = "\"";
                for (char c : bundledHtml) {
                    if (c == '\\' || c == '\"') {
                        esc += '\\';
                        esc += c;
                    } else if (c == '\n')
                        esc += "\\n";
                    else if (c == '\r')
                        esc += "\\r";
                    else if (c == '\t')
                        esc += "\\t";
                    else
                        esc += c;
                }
                esc += '\"';
                bool replacedAny = false;
                for (const std::string& ph :
                     {std::string("__BLUE_BUNDLE_HTML__")}) {
                    size_t pos = mainTpl.find(ph);
                    while (pos != std::string::npos) {
                        mainTpl.replace(pos, ph.size(), esc);
                        pos = mainTpl.find(ph, pos + esc.size());
                        replacedAny = true;
                    }
                }
                if (!replacedAny)
                    throw std::runtime_error(
                        "src/main.js must contain placeholder __BLUE_BUNDLE_HTML__");
            }

            fs::path tmpMain = bd / "main.entry.js";
            {
                std::ofstream wf(tmpMain);
                wf << mainTpl;
            }

            fs::path resolveJs = tmpMain;
            fs::path bundledMain;
            if (bundleDeps) {
                bundledMain = bd / "main.bundled.js";
                jsc_npm_bundle::run(binDirectory,
                                    fs::absolute(tmpMain).generic_string(),
                                    proj.generic_string(),
                                    fs::absolute(bundledMain).generic_string(),
                                    bundlePlat);
                resolveJs = bundledMain;
            }

            std::string cpp;
            if (controlPlane) {
                cpp = emitControlPlaneCpp(readFileToBytes(resolveJs));
            } else if (hybrid) {
                fs::path islandEntryPath = fs::weakly_canonical(proj / quickjsIsland);
                if (!fs::exists(islandEntryPath))
                    throw std::runtime_error(
                        "blue: hybrid: quickjsIsland file not found: " +
                        islandEntryPath.generic_string());
                fs::path islandBundled = bd / "island.bundled.js";
                jsc_npm_bundle::run(binDirectory,
                                    fs::absolute(islandEntryPath).generic_string(),
                                    proj.generic_string(),
                                    fs::absolute(islandBundled).generic_string(),
                                    bundlePlat);

                QjsGuard qjs;
                if (!qjs.rt || !qjs.ctx) {
                    std::cerr << "blue: could not allocate QuickJS\n";
                    return EXIT_FAILURE;
                }
                bootstrapQjs(qjs.ctx, binDirectory);

                std::vector<Module> mods =
                    ModuleResolver::resolve(resolveJs.generic_string(), qjs.ctx);
                ModuleResolver::validateHybridAotGraph(mods);

                CEmitter emitter;
                cpp = emitHybridIslandBlob(readFileToBytes(islandBundled)) +
                      emitter.emitModules(mods, true);
            } else {
                QjsGuard qjs;
                if (!qjs.rt || !qjs.ctx) {
                    std::cerr << "blue: could not allocate QuickJS\n";
                    return EXIT_FAILURE;
                }
                bootstrapQjs(qjs.ctx, binDirectory);

                std::vector<Module> mods =
                    ModuleResolver::resolve(resolveJs.generic_string(), qjs.ctx);

                CEmitter emitter;
                cpp = emitter.emitModules(mods);
            }

            fs::path outBin =
                outputBinary.empty()
                    ? bd / fs::path(proj.filename().string() + "_app.bin")
                    : fs::absolute(outputBinary);

            fs::path tmpCc = fs::temp_directory_path() /
                ("blue_build_" + std::to_string(::getpid()) + "_" +
                 std::to_string(std::hash<std::string>{}(outBin.generic_string())) + ".cc");
            {
                std::ofstream oo(tmpCc);
                oo << cpp;
            }

            std::string cxx =
#ifdef _WIN32
                "g++";
#else
                "c++";
#endif
            char* ec = getenv("CXX");
            if (ec && std::strlen(ec))
                cxx = ec;

            fs::path embed =
                fs::exists(bd / "jsc_assets.cpp") ? bd / "jsc_assets.cpp"
                                                     : fs::path();

            if (!bundledHtml.empty() && embed.empty() && !controlPlane && !hybrid)
                throw std::runtime_error(
                    "expected embedded assets after bundling.");

            invokeCompiler(binDirectory, cxx,
                           fs::weakly_canonical(tmpCc),
                           outBin.generic_string(), cpp,
                           !controlPlane && !hybrid,
                           embed.generic_string(),
                           extraCFlags);
            fs::remove(tmpCc);
            auto t1 = std::chrono::steady_clock::now();

            std::cerr << "blue: built " << outBin << "\n";

            if (doBenchmark) {
                auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(t1 - t0).count();
                std::cerr << "blue benchmark (build): " << ms << "ms\n";
            }

            if (doRun) {
                std::string runCmd = outBin.string();
                for (const auto& arg : appArgs) {
                    runCmd += " \"";
                    runCmd += arg;
                    runCmd += "\"";
                }
                runCmd += " 2>&1";
                std::cerr << "blue: running " << outBin << " ...\n";
                auto tr0 = std::chrono::steady_clock::now();
                FILE* pipe = popen(runCmd.c_str(), "r");
                if (!pipe) return EXIT_FAILURE;
                char buf[4096];
                bool measuredStart = false;
                while (fgets(buf, sizeof(buf), pipe)) {
                    if (!measuredStart && doBenchmark) {
                        auto tr1 = std::chrono::steady_clock::now();
                        auto ms2 = std::chrono::duration_cast<std::chrono::milliseconds>(tr1 - tr0).count();
                        std::cerr << "blue benchmark (start): " << ms2 << "ms\n";
                        measuredStart = true;
                    }
                    std::cout << buf << std::flush;
                }
                int status = pclose(pipe);
                return (WIFEXITED(status) && WEXITSTATUS(status) == 0) ? EXIT_SUCCESS : EXIT_FAILURE;
            }
        } catch (const std::exception& ex) {
            std::cerr << ex.what() << "\n";
            return EXIT_FAILURE;
        }
        return EXIT_SUCCESS;
    }

    if (inputPath.empty()) {
        std::cerr << "blue: missing input.\n";
        printUsage(argv[0]);
        return EXIT_FAILURE;
    }

    if (!doCompile)
        printC = true;

    if (doCompile && outputBinary.empty())
        outputBinary = fs::path(inputPath).stem().generic_string() + ".out";

    QjsGuard qjs;
    if (!qjs.rt || !qjs.ctx) {
        std::cerr << "blue: QuickJS unavailable\n";
        return EXIT_FAILURE;
    }

    try {
        bootstrapQjs(qjs.ctx, binDirectory);

        std::string modEntry = fs::absolute(inputPath).generic_string();
        std::vector<Module> mods = ModuleResolver::resolve(modEntry, qjs.ctx);

        CEmitter emitter;
        std::string cpp = emitter.emitModules(mods);

        if (printC)
            std::cout << cpp;

        if (doCompile) {
            fs::path tmpCc = fs::temp_directory_path() /
                ("blue_compile_" + std::to_string(::getpid()) + "_" +
                 std::to_string(std::hash<std::string>{}(outputBinary)) + ".cc");
            {
                std::ofstream oo(tmpCc);
                oo << cpp;
            }

            std::string cxx =
#ifdef _WIN32
                "g++";
#else
                "c++";
#endif
            char* ec = getenv("CXX");
            if (ec && std::strlen(ec))
                cxx = ec;

            auto tc0 = std::chrono::steady_clock::now();
            invokeCompiler(binDirectory, cxx,
                           fs::weakly_canonical(tmpCc),
                           outputBinary,
                           cpp,
                           false,
                           fs::path(),
                           extraCFlags);
            fs::remove(tmpCc);
            std::cerr << "blue: compiled -> " << outputBinary << "\n";
            if (doBenchmark) {
                auto tc1 = std::chrono::steady_clock::now();
                auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(tc1 - tc0).count();
                std::cerr << "blue benchmark (compile): " << ms << "ms\n";
            }
        }

    } catch (const std::exception& ex) {
        std::cerr << ex.what() << "\n";
        std::string msg = ex.what();
        bool isParseOrFeature =
            msg.find("parse error") != std::string::npos ||
            msg.find("Unexpected") != std::string::npos ||
            msg.find("Unimplemented") != std::string::npos ||
            msg.find("SyntaxError") != std::string::npos;
        if (isParseOrFeature) {
            std::cerr << "\nTips:\n"
                      << "  • Run with --print-c to inspect the generated C++.\n"
                      << "  • Move unsupported code to src/island.js for full JS support.\n"
                      << "  • Set BLUE_STRICT_UNSUPPORTED=1 to turn warnings into errors.\n";
        }
        return EXIT_FAILURE;
    }

    return EXIT_SUCCESS;
}
