// calogMain.c -- the `calog` command-line runner.
//
// Links every engine and every library into one binary, then executes the script files named
// on the command line. A file named with a known extension runs on that engine (.lua .js .nut
// .bas .be .scm .wren); a name with no recognized extension is searched for as <name>.<ext> in
// engine-registration priority order (the first existing file wins), matching calogContextLoad.
//
// Each script is launched fire-and-forget on its own context, and the process pumps the host
// thread so scripts can call back into the registered natives (print, the libraries). Because
// calog is event-driven, a script finishing its top level does NOT end the program -- its
// context stays alive to service timer/pubsub/task callbacks -- so there is no way to infer
// "all processing is done". Instead a script calls calogExit([code]) to tear everything down
// and exit; SIGINT/SIGTERM do the same. A script that ERRORS has its context retired (closed);
// once no launched contexts remain live, calog exits on its own. Absent any of these, calog
// runs until killed.
//
// Teardown order matters (see the library headers): the cross-context reference holders
// (export/pubsub/timer/kv) are released while contexts are still alive, THEN calogDestroy joins
// every context thread, THEN the libraries whose registries outlive the runtime (db/net/ssh/
// task) are freed.

#define _POSIX_C_SOURCE 200809L

#include "calog.h"

#include "calogCrypto.h"
#include "calogDb.h"
#include "calogExport.h"
#include "calogFs.h"
#include "calogHttp.h"
#include "calogJson.h"
#include "calogKv.h"
#include "calogNet.h"
#include "calogPubsub.h"
#include "calogSsh.h"
#include "calogTask.h"
#include "calogTime.h"
#include "calogTimer.h"

#include <errno.h>
#include <signal.h>
#include <stdatomic.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <time.h>

// One host-thread pump iteration parks this long between drains (0.5 ms), matching the test
// harness -- long enough not to spin a core, short enough to feel responsive.
#define PUMP_INTERVAL_NS 500000

// The base for a signal-derived exit code (128 + signal number), the shell convention.
#define SIGNAL_EXIT_BASE 128

static const CalogEngineT *engineForExtension(const char *ext);
static const char         *extensionOf(const char *arg);
static int32_t             nativeCalogExit(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t             nativeCalogPrint(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static void                onError(uint64_t contextId, const char *message, void *userData);
static void                onSignal(int sig);
static void                printUsage(FILE *stream, const char *program);
static void                printValue(const CalogValueT *value);
static char               *readFile(const char *path);
static bool                resolveArg(const char *arg, const CalogEngineT **outEngine, char **outSource);

// Requested by a script (calogExit), a signal, or the last live context erroring out; the host
// pump loop watches gShutdown and exits with gExitCode.
static _Atomic bool    gShutdown = false;
static _Atomic int32_t gExitCode = 0;

// One entry per script context the runner launched. onError flags `failed` for a context whose
// script errors; the pump loop then closes that context and drops the live count. When the live
// count reaches zero -- every launched context has errored out -- the runner exits.
typedef struct LaunchedT {
    uint64_t       id;
    CalogContextT *context;   // NULL once the context has been closed
    _Atomic bool   failed;
} LaunchedT;

static LaunchedT *gLaunched      = NULL;
static int32_t    gLaunchedCount = 0;

// The engines the runner knows, in load-search priority order (the same order
// calogRegisterBuiltinEngines uses, so an extensionless name resolves identically here and via
// taskLoad). Referencing the vtables directly pulls each engine archive into the link.
static const CalogEngineT *const gEngines[] = {
    &calogLuaEngine,
    &calogJsEngine,
    &calogSquirrelEngine,
    &calogMyBasicEngine,
    &calogBerryEngine,
    &calogS7Engine,
    &calogWrenEngine,
    &calogMrubyEngine
};

// The built-in libraries, in registration order. One source of truth: main() registers every
// entry from this table and printUsage names them from it, so the help text can never drift
// from what is actually registered.
static const struct {
    const char *name;
    int32_t   (*reg)(CalogT *calog);
} gLibraries[] = {
    { "crypto", calogCryptoRegister },
    { "db",     calogDbRegister },
    { "export", calogExportRegister },
    { "fs",     calogFsRegister },
    { "http",   calogHttpRegister },
    { "json",   calogJsonRegister },
    { "kv",     calogKvRegister },
    { "net",    calogNetRegister },
    { "pubsub", calogPubsubRegister },
    { "ssh",    calogSshRegister },
    { "task",   calogTaskRegister },
    { "time",   calogTimeRegister },
    { "timer",  calogTimerRegister }
};

// s7 interns a small, bounded set of "permanent" strings it never reclaims (an s7 trait, not a
// leak); suppress exactly that site so an ASan/LSan build of the runner stays quiet. LSan calls
// this weak hook automatically.
const char *__lsan_default_suppressions(void);
const char *__lsan_default_suppressions(void) {
    return "leak:make_permanent_string\n";
}

// Keep the "Suppressions used" banner off stdout/stderr on a clean run.
const char *__lsan_default_options(void);
const char *__lsan_default_options(void) {
    return "print_suppressions=0";
}


// Find the engine that claims file extension ext (case-insensitive), or NULL if none does.
static const CalogEngineT *engineForExtension(const char *ext) {
    size_t index;

    for (index = 0; index < sizeof(gEngines) / sizeof(gEngines[0]); index++) {
        const CalogEngineT *engine;
        int32_t             which;

        engine = gEngines[index];
        if (engine->extensions == NULL) {
            continue;
        }
        for (which = 0; engine->extensions[which] != NULL; which++) {
            if (strcasecmp(engine->extensions[which], ext) == 0) {
                return engine;
            }
        }
    }
    return NULL;
}


// Return a pointer to the extension of arg (the bytes after the last '.' of the final path
// component), or NULL when there is none -- a name with no dot, or a leading-dot dotfile.
static const char *extensionOf(const char *arg) {
    const char *base;
    const char *slash;
    const char *dot;

    slash = strrchr(arg, '/');
    base  = (slash != NULL) ? slash + 1 : arg;
    dot   = strrchr(base, '.');
    if (dot == NULL || dot == base) {
        return NULL;
    }
    return dot + 1;
}


// calogExit([code]) -- a script asks the runner to tear everything down and exit. Inline: it
// only sets two atomics, which is thread-safe, so it needs no host hop.
static int32_t nativeCalogExit(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
    (void)userData;
    calogValueNil(result);
    if (argCount >= 1) {
        if (args[0].type == calogIntE) {
            atomic_store(&gExitCode, (int32_t)args[0].as.i);
        } else if (args[0].type == calogRealE) {
            atomic_store(&gExitCode, (int32_t)args[0].as.r);
        } else {
            return calogFail(result, calogErrArgE, "calogExit expects an optional integer exit code");
        }
    }
    atomic_store(&gShutdown, true);
    return calogOkE;
}


// calogPrint(...) -- write each argument to stdout, space-separated, with a trailing newline. A
// host native (runs on the host thread), so concurrent scripts' lines do not interleave. Named
// with the calog prefix (like calogExit) so it never collides with an engine's built-in print
// or the my-basic PRINT keyword.
static int32_t nativeCalogPrint(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
    int32_t index;

    (void)userData;
    calogValueNil(result);
    for (index = 0; index < argCount; index++) {
        if (index > 0) {
            fputc(' ', stdout);
        }
        printValue(&args[index]);
    }
    fputc('\n', stdout);
    fflush(stdout);
    return calogOkE;
}


static void onError(uint64_t contextId, const char *message, void *userData) {
    int32_t index;

    (void)userData;
    fprintf(stderr, "calog: script error: %s\n", (message != NULL) ? message : "(unknown)");
    // Flag the failing context so the pump loop retires it (a script error closes its context).
    for (index = 0; index < gLaunchedCount; index++) {
        if (gLaunched[index].id == contextId) {
            atomic_store(&gLaunched[index].failed, true);
            break;
        }
    }
}


// Signal handler for SIGINT/SIGTERM: request the same orderly shutdown as calogExit. Only
// async-signal-safe atomic stores happen here.
static void onSignal(int sig) {
    atomic_store(&gExitCode, (int32_t)(SIGNAL_EXIT_BASE + sig));
    atomic_store(&gShutdown, true);
}


static void printUsage(FILE *stream, const char *program) {
    size_t index;

    fprintf(stream, "usage: %s <script> [script ...]\n", program);
    fprintf(stream, "\n");
    fprintf(stream, "Run each script through the calog multi-language runtime.\n");
    fprintf(stream, "A name with a known extension runs on that engine:\n");
    fprintf(stream, " ");
    // Derived from gEngines so the help text can never drift from the resolver's own truth.
    for (index = 0; index < sizeof(gEngines) / sizeof(gEngines[0]); index++) {
        const CalogEngineT *engine;
        int32_t             which;

        engine = gEngines[index];
        if (engine->extensions == NULL) {
            continue;
        }
        for (which = 0; engine->extensions[which] != NULL; which++) {
            fprintf(stream, "  .%s", engine->extensions[which]);
        }
    }
    fprintf(stream, "\n");
    fprintf(stream, "A name with no recognized extension is searched for as <name>.<ext>\n");
    fprintf(stream, "in that priority order; the first existing file wins.\n");
    fprintf(stream, "\n");
    fprintf(stream, "Scripts share these libraries (plus calogPrint):");
    // Named from the same gLibraries table main() registers, so the help cannot drift.
    for (index = 0; index < sizeof(gLibraries) / sizeof(gLibraries[0]); index++) {
        fprintf(stream, " %s", gLibraries[index].name);
    }
    fprintf(stream, "\n");
    fprintf(stream, "Call calogExit([code]) to tear everything down and exit; otherwise calog\n");
    fprintf(stream, "runs until interrupted.\n");
}


static void printValue(const CalogValueT *value) {
    switch (value->type) {
        case calogNilE:
            fputs("nil", stdout);
            break;
        case calogBoolE:
            fputs(value->as.b ? "true" : "false", stdout);
            break;
        case calogIntE:
            printf("%lld", (long long)value->as.i);
            break;
        case calogRealE:
            printf("%g", value->as.r);
            break;
        case calogStringE:
            fwrite(value->as.s.bytes, 1, (size_t)value->as.s.length, stdout);
            break;
        case calogAggE:
            fputs("[aggregate]", stdout);
            break;
        case calogFnE:
            fputs("[function]", stdout);
            break;
        default:
            break;
    }
}


// Read a whole file into a fresh, NUL-terminated buffer (binary content is preserved up to the
// terminator; scripts are text). Returns NULL on failure, leaving errno set.
static char *readFile(const char *path) {
    FILE   *file;
    char   *buffer;
    long    size;
    size_t  readCount;

    file = fopen(path, "rb");
    if (file == NULL) {
        return NULL;
    }
    if (fseek(file, 0, SEEK_END) != 0 || (size = ftell(file)) < 0) {
        fclose(file);
        return NULL;
    }
    rewind(file);
    buffer = (char *)malloc((size_t)size + 1);
    if (buffer == NULL) {
        fclose(file);
        return NULL;
    }
    readCount = fread(buffer, 1, (size_t)size, file);
    fclose(file);
    buffer[readCount] = '\0';
    return buffer;
}


// Resolve one command-line argument to an engine + its script source (a fresh buffer the caller
// frees). A recognized extension binds the exact file to that engine; otherwise the argument is
// treated as a base name and searched as <arg>.<ext> across the engines in priority order. On
// failure prints a diagnostic and returns false.
static bool resolveArg(const char *arg, const CalogEngineT **outEngine, char **outSource) {
    const char         *ext;
    const CalogEngineT *engine;
    char               *source;
    size_t              index;

    *outEngine = NULL;
    *outSource = NULL;

    ext = extensionOf(arg);
    if (ext != NULL) {
        engine = engineForExtension(ext);
        if (engine != NULL) {
            source = readFile(arg);
            if (source == NULL) {
                fprintf(stderr, "calog: %s: %s\n", arg, strerror(errno));
                return false;
            }
            *outEngine = engine;
            *outSource = source;
            return true;
        }
    }

    for (index = 0; index < sizeof(gEngines) / sizeof(gEngines[0]); index++) {
        int32_t which;

        engine = gEngines[index];
        if (engine->extensions == NULL) {
            continue;
        }
        for (which = 0; engine->extensions[which] != NULL; which++) {
            char  *path;
            size_t pathSize;

            pathSize = strlen(arg) + strlen(engine->extensions[which]) + 2;   // '.' + '\0'
            path     = (char *)malloc(pathSize);
            if (path == NULL) {
                fprintf(stderr, "calog: out of memory\n");
                return false;
            }
            snprintf(path, pathSize, "%s.%s", arg, engine->extensions[which]);
            source = readFile(path);
            if (source == NULL) {
                // Not found here is expected -- keep searching. Any other failure (permission,
                // out of memory) is a hard stop, same as the exact-extension case above.
                if (errno == ENOENT) {
                    free(path);
                    continue;
                }
                fprintf(stderr, "calog: %s: %s\n", path, strerror(errno));
                free(path);
                return false;
            }
            free(path);
            *outEngine = engine;
            *outSource = source;
            return true;
        }
    }

    fprintf(stderr, "calog: %s: no runnable script found\n", arg);
    return false;
}


int main(int argc, char **argv) {
    const CalogEngineT **engines       = NULL;
    char               **sources       = NULL;
    const char          *program;
    CalogT              *calog         = NULL;
    struct timespec      tick = { 0, PUMP_INTERVAL_NS };
    int32_t              scriptCount   = 0;
    int32_t              resolvedCount = 0;
    int32_t              liveCount;
    int32_t              index;
    int32_t              status        = 0;

    program = strrchr(argv[0], '/');
    program = (program != NULL) ? program + 1 : argv[0];

    if (argc < 2) {
        printUsage(stderr, program);
        return 2;
    }
    for (index = 1; index < argc; index++) {
        if (strcmp(argv[index], "-h") == 0 || strcmp(argv[index], "--help") == 0) {
            printUsage(stdout, program);
            return 0;
        }
    }

    scriptCount = argc - 1;
    engines     = (const CalogEngineT **)malloc((size_t)scriptCount * sizeof(*engines));
    sources     = (char **)malloc((size_t)scriptCount * sizeof(*sources));
    if (engines == NULL || sources == NULL) {
        fprintf(stderr, "calog: out of memory\n");
        status = 1;
        goto cleanup;
    }

    // Resolve every argument before running any (fail fast): one bad name runs nothing.
    for (index = 0; index < scriptCount; index++) {
        if (!resolveArg(argv[index + 1], &engines[index], &sources[index])) {
            status = 1;
            goto cleanup;
        }
        resolvedCount++;
    }

    calog = calogCreate();
    if (calog == NULL) {
        fprintf(stderr, "calog: failed to create runtime\n");
        status = 1;
        goto cleanup;
    }
    calogSetErrorHandler(calog, onError, NULL);

    if (calogRegister(calog, "calogPrint", nativeCalogPrint, NULL) != calogOkE ||
        calogRegisterInline(calog, "calogExit", nativeCalogExit, NULL) != calogOkE) {
        fprintf(stderr, "calog: failed to register the runner natives\n");
        status = 1;
        goto cleanup;
    }
    for (index = 0; index < (int32_t)(sizeof(gLibraries) / sizeof(gLibraries[0])); index++) {
        if (gLibraries[index].reg(calog) != calogOkE) {
            fprintf(stderr, "calog: failed to register the %s library\n", gLibraries[index].name);
            status = 1;
            goto cleanup;
        }
    }
    calogRegisterBuiltinEngines(calog);

    signal(SIGINT, onSignal);
    signal(SIGTERM, onSignal);

    // Launch every script fire-and-forget on its own context, recording the live ones so a
    // later error can retire exactly that context. calogContextEval keeps its own copy of the
    // source, so the resolved buffer is freed right after.
    gLaunched = (LaunchedT *)calloc((size_t)scriptCount, sizeof(*gLaunched));
    if (gLaunched == NULL) {
        fprintf(stderr, "calog: out of memory\n");
        status = 1;
        goto cleanup;
    }
    for (index = 0; index < scriptCount; index++) {
        CalogContextT *context;

        context = calogContextOpen(calog, engines[index]);
        if (context == NULL) {
            fprintf(stderr, "calog: %s: failed to open a context\n", argv[index + 1]);
            atomic_store(&gExitCode, 1);   // failed launch: never report success on exit
        } else if (calogContextEval(context, sources[index]) != calogOkE) {
            fprintf(stderr, "calog: %s: failed to start script\n", argv[index + 1]);
            calogContextClose(context);
            atomic_store(&gExitCode, 1);   // failed launch: never report success on exit
        } else {
            gLaunched[gLaunchedCount].id      = calogContextId(context);
            gLaunched[gLaunchedCount].context = context;
            atomic_store(&gLaunched[gLaunchedCount].failed, false);
            gLaunchedCount++;
        }
        free(sources[index]);
    }
    free(engines);
    free(sources);
    engines = NULL;
    sources = NULL;

    liveCount = gLaunchedCount;
    if (liveCount == 0) {
        atomic_store(&gShutdown, true);   // nothing runnable launched
    }

    // Service script->native calls until a script calls calogExit, we are signalled, or every
    // launched context has errored out. A script error retires (closes) its own context; when
    // the last live context is gone, the runner exits.
    while (!atomic_load(&gShutdown)) {
        calogPump(calog);
        calogTaskReap();   // join + free any task that retired itself via taskExit()
        for (index = 0; index < gLaunchedCount; index++) {
            if (gLaunched[index].context != NULL && atomic_load(&gLaunched[index].failed)) {
                int32_t expected;
                calogContextClose(gLaunched[index].context);
                gLaunched[index].context = NULL;
                liveCount--;
                expected = 0;
                atomic_compare_exchange_strong(&gExitCode, &expected, 1);
            }
        }
        if (liveCount == 0) {
            atomic_store(&gShutdown, true);
            break;
        }
        nanosleep(&tick, NULL);
    }
    calogPump(calog);
    calogTaskReap();

    // Release cross-context references while the contexts are still alive, tear the runtime
    // down (joining every context thread), then free the registries that outlive it.
    calogExportShutdown();
    calogPubsubShutdown();
    calogTimerShutdown();
    calogKvShutdown();
    calogDestroy(calog);
    calogDbShutdown();
    calogNetShutdown();
    calogSshShutdown();
    calogTaskShutdown();

    free(gLaunched);
    return (int)atomic_load(&gExitCode);

cleanup:
    // Shared teardown for every pre-run failure path: free what resolveArg produced, the two
    // argv-length arrays, and destroy the runtime if calogCreate got far enough to build one.
    for (index = 0; index < resolvedCount; index++) {
        free(sources[index]);
    }
    free(engines);
    free(sources);
    if (calog != NULL) {
        calogDestroy(calog);
    }
    return status;
}
