/*
 * vault1984 CLI — QuickJS bridge to BearSSL
 *
 * Registers native functions into the QuickJS runtime so that
 * crypto/crypto.js and crypto/totp.js can call BearSSL primitives.
 *
 * Native functions exposed to JS:
 *   native_aes_gcm_encrypt(key, plaintext)  → Uint8Array (nonce || ct || tag)
 *   native_aes_gcm_decrypt(key, iv, ct)     → Uint8Array (plaintext)
 *   native_hkdf_sha256(ikm, salt, info, len) → Uint8Array
 *   native_hmac_sha1(key, data)             → Uint8Array
 *   native_base64_encode(bytes)             → string
 *   native_base64_decode(string)            → Uint8Array
 *   native_encode_utf8(string)              → Uint8Array
 *   native_decode_utf8(bytes)               → string
 *   native_random_bytes(n)                  → Uint8Array
 */

#define _POSIX_C_SOURCE 200809L

#include "jsbridge.h"
#include "util.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "quickjs.h"
#include "bearssl.h"

static JSRuntime *rt = NULL;
static JSContext *ctx = NULL;

/* --- helper: JS Uint8Array ↔ C buffer --- */

static int js_get_uint8array(JSContext *ctx, JSValueConst val,
                             uint8_t **out, size_t *out_len) {
    size_t offset, len;
    size_t bpp;
    JSValue buf = JS_GetTypedArrayBuffer(ctx, val, &offset, &len, &bpp);
    if (JS_IsException(buf)) return -1;

    size_t buf_size;
    uint8_t *ptr = JS_GetArrayBuffer(ctx, &buf_size, buf);
    JS_FreeValue(ctx, buf);
    if (!ptr) return -1;

    *out = ptr + offset;
    *out_len = len;
    return 0;
}

static JSValue js_new_uint8array(JSContext *ctx, const uint8_t *data, size_t len) {
    JSValue ab = JS_NewArrayBufferCopy(ctx, data, len);
    if (JS_IsException(ab)) return ab;

    JSValue global = JS_GetGlobalObject(ctx);
    JSValue u8ctor = JS_GetPropertyStr(ctx, global, "Uint8Array");
    JSValue result = JS_CallConstructor(ctx, u8ctor, 1, &ab);
    JS_FreeValue(ctx, u8ctor);
    JS_FreeValue(ctx, global);
    JS_FreeValue(ctx, ab);
    return result;
}

/* --- native_random_bytes(n) --- */

static JSValue js_random_bytes(JSContext *ctx, JSValueConst this_val,
                               int argc, JSValueConst *argv) {
    int n;
    if (JS_ToInt32(ctx, &n, argv[0]) || n <= 0 || n > 4096)
        return JS_ThrowRangeError(ctx, "invalid length");

    uint8_t *buf = malloc((size_t)n);
    br_hmac_drbg_context drbg;
    br_hmac_drbg_init(&drbg, &br_sha256_vtable, "vault1984-seed", 14);

    /* Seed with system RNG */
    uint8_t seed[32];
    br_prng_seeder seeder = br_prng_seeder_system(NULL);
    if (seeder) {
        seeder(&drbg.vtable);
    }
    br_hmac_drbg_generate(&drbg, buf, (size_t)n);

    JSValue result = js_new_uint8array(ctx, buf, (size_t)n);
    free(buf);
    return result;
}

/* --- native_aes_gcm_encrypt(key, plaintext) → nonce(12) || ciphertext || tag(16) --- */

static JSValue js_aes_gcm_encrypt(JSContext *ctx, JSValueConst this_val,
                                   int argc, JSValueConst *argv) {
    uint8_t *key, *pt;
    size_t key_len, pt_len;

    if (js_get_uint8array(ctx, argv[0], &key, &key_len) != 0)
        return JS_ThrowTypeError(ctx, "key must be Uint8Array");
    if (js_get_uint8array(ctx, argv[1], &pt, &pt_len) != 0)
        return JS_ThrowTypeError(ctx, "plaintext must be Uint8Array");
    if (key_len != 16 && key_len != 32)
        return JS_ThrowRangeError(ctx, "key must be 16 or 32 bytes");

    /* Generate random 12-byte nonce */
    uint8_t nonce[12];
    br_hmac_drbg_context drbg;
    br_hmac_drbg_init(&drbg, &br_sha256_vtable, "vault1984-nonce", 15);
    br_prng_seeder seeder = br_prng_seeder_system(NULL);
    if (seeder) seeder(&drbg.vtable);
    br_hmac_drbg_generate(&drbg, nonce, 12);

    /* AES-GCM encrypt */
    size_t out_len = 12 + pt_len + 16; /* nonce + ciphertext + tag */
    uint8_t *out = malloc(out_len);
    memcpy(out, nonce, 12);
    memcpy(out + 12, pt, pt_len);

    br_aes_ct64_ctr_keys aes_ctx;
    br_aes_ct64_ctr_init(&aes_ctx, key, key_len);

    br_gcm_context gcm;
    br_gcm_init(&gcm, &aes_ctx.vtable, br_ghash_ctmul64);
    br_gcm_reset(&gcm, nonce, 12);
    br_gcm_flip(&gcm);
    br_gcm_run(&gcm, 1, out + 12, pt_len); /* encrypt in place */
    br_gcm_get_tag(&gcm, out + 12 + pt_len); /* append 16-byte tag */

    JSValue result = js_new_uint8array(ctx, out, out_len);
    free(out);
    return result;
}

/* --- native_aes_gcm_decrypt(key, iv, ct_with_tag) → plaintext --- */

static JSValue js_aes_gcm_decrypt(JSContext *ctx, JSValueConst this_val,
                                   int argc, JSValueConst *argv) {
    uint8_t *key, *iv, *ct;
    size_t key_len, iv_len, ct_len;

    if (js_get_uint8array(ctx, argv[0], &key, &key_len) != 0)
        return JS_ThrowTypeError(ctx, "key must be Uint8Array");
    if (js_get_uint8array(ctx, argv[1], &iv, &iv_len) != 0)
        return JS_ThrowTypeError(ctx, "iv must be Uint8Array");
    if (js_get_uint8array(ctx, argv[2], &ct, &ct_len) != 0)
        return JS_ThrowTypeError(ctx, "ciphertext must be Uint8Array");

    if (key_len != 16 && key_len != 32)
        return JS_ThrowRangeError(ctx, "key must be 16 or 32 bytes");
    if (iv_len != 12)
        return JS_ThrowRangeError(ctx, "iv must be 12 bytes");
    if (ct_len < 16)
        return JS_ThrowRangeError(ctx, "ciphertext too short (need tag)");

    size_t pt_len = ct_len - 16;
    uint8_t *buf = malloc(ct_len);
    memcpy(buf, ct, ct_len);

    br_aes_ct64_ctr_keys aes_ctx;
    br_aes_ct64_ctr_init(&aes_ctx, key, key_len);

    br_gcm_context gcm;
    br_gcm_init(&gcm, &aes_ctx.vtable, br_ghash_ctmul64);
    br_gcm_reset(&gcm, iv, 12);
    br_gcm_flip(&gcm);
    br_gcm_run(&gcm, 0, buf, pt_len); /* decrypt in place */

    if (!br_gcm_check_tag(&gcm, buf + pt_len)) {
        free(buf);
        return JS_ThrowInternalError(ctx, "AES-GCM tag verification failed");
    }

    JSValue result = js_new_uint8array(ctx, buf, pt_len);
    free(buf);
    return result;
}

/* --- native_aes_gcm_decrypt_blob(key, nonce_ct_tag) → plaintext --- */

static JSValue js_aes_gcm_decrypt_blob(JSContext *ctx, JSValueConst this_val,
                                        int argc, JSValueConst *argv) {
    uint8_t *key, *data;
    size_t key_len, data_len;

    if (js_get_uint8array(ctx, argv[0], &key, &key_len) != 0)
        return JS_ThrowTypeError(ctx, "key must be Uint8Array");
    if (js_get_uint8array(ctx, argv[1], &data, &data_len) != 0)
        return JS_ThrowTypeError(ctx, "data must be Uint8Array");

    if (key_len != 16 && key_len != 32)
        return JS_ThrowRangeError(ctx, "key must be 16 or 32 bytes");
    if (data_len < 28) /* 12 nonce + 0 pt + 16 tag minimum */
        return JS_ThrowRangeError(ctx, "data too short");

    uint8_t *iv = data;
    uint8_t *ct = data + 12;
    size_t ct_len = data_len - 12;
    size_t pt_len = ct_len - 16;

    uint8_t *buf = malloc(ct_len);
    memcpy(buf, ct, ct_len);

    br_aes_ct64_ctr_keys aes_ctx;
    br_aes_ct64_ctr_init(&aes_ctx, key, key_len);

    br_gcm_context gcm;
    br_gcm_init(&gcm, &aes_ctx.vtable, br_ghash_ctmul64);
    br_gcm_reset(&gcm, iv, 12);
    br_gcm_flip(&gcm);
    br_gcm_run(&gcm, 0, buf, pt_len);

    if (!br_gcm_check_tag(&gcm, buf + pt_len)) {
        free(buf);
        return JS_ThrowInternalError(ctx, "AES-GCM tag verification failed");
    }

    JSValue result = js_new_uint8array(ctx, buf, pt_len);
    free(buf);
    return result;
}

/* --- native_hkdf_sha256(ikm, salt, info, length) → Uint8Array --- */

static JSValue js_hkdf_sha256(JSContext *ctx, JSValueConst this_val,
                               int argc, JSValueConst *argv) {
    uint8_t *ikm, *info;
    size_t ikm_len, info_len;

    if (js_get_uint8array(ctx, argv[0], &ikm, &ikm_len) != 0)
        return JS_ThrowTypeError(ctx, "ikm must be Uint8Array");

    /* salt can be null */
    uint8_t *salt = NULL;
    size_t salt_len = 0;
    if (!JS_IsNull(argv[1]) && !JS_IsUndefined(argv[1])) {
        if (js_get_uint8array(ctx, argv[1], &salt, &salt_len) != 0)
            return JS_ThrowTypeError(ctx, "salt must be Uint8Array or null");
    }

    if (js_get_uint8array(ctx, argv[2], &info, &info_len) != 0)
        return JS_ThrowTypeError(ctx, "info must be Uint8Array");

    int length;
    if (JS_ToInt32(ctx, &length, argv[3]) || length <= 0 || length > 255 * 32)
        return JS_ThrowRangeError(ctx, "invalid output length");

    /* BearSSL HKDF */
    br_hkdf_context hkdf;
    br_hkdf_init(&hkdf, &br_sha256_vtable, salt, salt_len);
    br_hkdf_inject(&hkdf, ikm, ikm_len);
    br_hkdf_flip(&hkdf);

    uint8_t *out = malloc((size_t)length);
    br_hkdf_produce(&hkdf, info, info_len, out, (size_t)length);

    JSValue result = js_new_uint8array(ctx, out, (size_t)length);
    free(out);
    return result;
}

/* --- native_hmac_sha1(key, data) → Uint8Array(20) --- */

static JSValue js_hmac_sha1(JSContext *ctx, JSValueConst this_val,
                             int argc, JSValueConst *argv) {
    uint8_t *key, *data;
    size_t key_len, data_len;

    if (js_get_uint8array(ctx, argv[0], &key, &key_len) != 0)
        return JS_ThrowTypeError(ctx, "key must be Uint8Array");
    if (js_get_uint8array(ctx, argv[1], &data, &data_len) != 0)
        return JS_ThrowTypeError(ctx, "data must be Uint8Array");

    br_hmac_key_context kc;
    br_hmac_key_init(&kc, &br_sha1_vtable, key, key_len);

    br_hmac_context hmac;
    br_hmac_init(&hmac, &kc, 0); /* 0 = full output length (20 for SHA-1) */
    br_hmac_update(&hmac, data, data_len);

    uint8_t out[20];
    br_hmac_out(&hmac, out);

    return js_new_uint8array(ctx, out, 20);
}

/* --- native_base64_encode(Uint8Array) → string --- */

static JSValue js_base64_encode(JSContext *ctx, JSValueConst this_val,
                                 int argc, JSValueConst *argv) {
    uint8_t *data;
    size_t len;
    if (js_get_uint8array(ctx, argv[0], &data, &len) != 0)
        return JS_ThrowTypeError(ctx, "argument must be Uint8Array");

    size_t out_len = ((len + 2) / 3) * 4 + 1;
    char *out = malloc(out_len);
    base64_encode(data, len, out, out_len);

    JSValue result = JS_NewString(ctx, out);
    free(out);
    return result;
}

/* --- native_base64_decode(string) → Uint8Array --- */

static JSValue js_base64_decode_fn(JSContext *ctx, JSValueConst this_val,
                                    int argc, JSValueConst *argv) {
    const char *str = JS_ToCString(ctx, argv[0]);
    if (!str) return JS_ThrowTypeError(ctx, "argument must be string");

    size_t max_len = strlen(str);
    uint8_t *out = malloc(max_len);
    size_t out_len = 0;

    if (base64_decode(str, out, max_len, &out_len) != 0) {
        JS_FreeCString(ctx, str);
        free(out);
        return JS_ThrowInternalError(ctx, "base64 decode failed");
    }

    JS_FreeCString(ctx, str);
    JSValue result = js_new_uint8array(ctx, out, out_len);
    free(out);
    return result;
}

/* --- native_encode_utf8(string) → Uint8Array --- */

static JSValue js_encode_utf8(JSContext *ctx, JSValueConst this_val,
                               int argc, JSValueConst *argv) {
    size_t len;
    const char *str = JS_ToCStringLen(ctx, &len, argv[0]);
    if (!str) return JS_ThrowTypeError(ctx, "argument must be string");

    JSValue result = js_new_uint8array(ctx, (const uint8_t *)str, len);
    JS_FreeCString(ctx, str);
    return result;
}

/* --- native_decode_utf8(Uint8Array) → string --- */

static JSValue js_decode_utf8(JSContext *ctx, JSValueConst this_val,
                               int argc, JSValueConst *argv) {
    uint8_t *data;
    size_t len;
    if (js_get_uint8array(ctx, argv[0], &data, &len) != 0)
        return JS_ThrowTypeError(ctx, "argument must be Uint8Array");

    return JS_NewStringLen(ctx, (const char *)data, len);
}

/* --- load a JS file from disk --- */

static int load_js_file(JSContext *ctx, const char *filename) {
    FILE *f = fopen(filename, "rb");
    if (!f) {
        /* Try common relative paths */
        static const char *prefixes[] = {
            "../",          /* running from cli/ */
            "./",           /* running from repo root */
            "../../",       /* running from cli/build/ */
            NULL
        };
        char alt[1024];
        for (int i = 0; prefixes[i] && !f; i++) {
            snprintf(alt, sizeof(alt), "%s%s", prefixes[i], filename);
            f = fopen(alt, "rb");
        }
    }
    if (!f) {
        fprintf(stderr, "error: cannot open %s\n", filename);
        return -1;
    }

    fseek(f, 0, SEEK_END);
    long size = ftell(f);
    fseek(f, 0, SEEK_SET);

    char *buf = malloc((size_t)size + 1);
    fread(buf, 1, (size_t)size, f);
    fclose(f);
    buf[size] = '\0';

    JSValue val = JS_Eval(ctx, buf, (size_t)size, filename, JS_EVAL_TYPE_GLOBAL);
    free(buf);

    if (JS_IsException(val)) {
        JSValue exc = JS_GetException(ctx);
        const char *msg = JS_ToCString(ctx, exc);
        fprintf(stderr, "error: JS eval %s: %s\n", filename, msg ? msg : "unknown");
        if (msg) JS_FreeCString(ctx, msg);
        JS_FreeValue(ctx, exc);
        JS_FreeValue(ctx, val);
        return -1;
    }

    JS_FreeValue(ctx, val);
    return 0;
}

/* --- public API --- */

int jsbridge_init(void) {
    rt = JS_NewRuntime();
    if (!rt) {
        fprintf(stderr, "error: cannot create JS runtime\n");
        return -1;
    }

    ctx = JS_NewContext(rt);
    if (!ctx) {
        fprintf(stderr, "error: cannot create JS context\n");
        JS_FreeRuntime(rt);
        rt = NULL;
        return -1;
    }

    /* Register native functions */
    JSValue global = JS_GetGlobalObject(ctx);

    #define REG(name, func, nargs) \
        JS_SetPropertyStr(ctx, global, name, \
            JS_NewCFunction(ctx, func, name, nargs))

    REG("native_aes_gcm_encrypt",      js_aes_gcm_encrypt, 2);
    REG("native_aes_gcm_decrypt",      js_aes_gcm_decrypt, 3);
    REG("native_aes_gcm_decrypt_blob", js_aes_gcm_decrypt_blob, 2);
    REG("native_hkdf_sha256",     js_hkdf_sha256, 4);
    REG("native_hmac_sha1",       js_hmac_sha1, 2);
    REG("native_base64_encode",   js_base64_encode, 1);
    REG("native_base64_decode",   js_base64_decode_fn, 1);
    REG("native_encode_utf8",     js_encode_utf8, 1);
    REG("native_decode_utf8",     js_decode_utf8, 1);
    REG("native_random_bytes",    js_random_bytes, 1);

    #undef REG

    JS_FreeValue(ctx, global);

    /* Load shared crypto modules */
    if (load_js_file(ctx, "crypto/crypto.js") != 0) return -1;
    if (load_js_file(ctx, "crypto/totp.js") != 0) return -1;

    return 0;
}

void jsbridge_cleanup(void) {
    if (ctx) { JS_FreeContext(ctx); ctx = NULL; }
    if (rt)  { JS_FreeRuntime(rt);  rt = NULL;  }
}

char *jsbridge_totp(const char *seed_b32) {
    if (!ctx) return NULL;

    JSValue global = JS_GetGlobalObject(ctx);
    JSValue v84 = JS_GetPropertyStr(ctx, global, "vault1984");
    JSValue totp_obj = JS_GetPropertyStr(ctx, v84, "totp");
    JSValue fn = JS_GetPropertyStr(ctx, totp_obj, "generate_totp");

    JSValue args[1] = { JS_NewString(ctx, seed_b32) };
    JSValue result = JS_Call(ctx, fn, JS_UNDEFINED, 1, args);

    char *out = NULL;
    if (!JS_IsException(result)) {
        const char *str = JS_ToCString(ctx, result);
        if (str) {
            out = strdup(str);
            JS_FreeCString(ctx, str);
        }
    } else {
        JSValue exc = JS_GetException(ctx);
        const char *msg = JS_ToCString(ctx, exc);
        fprintf(stderr, "error: totp: %s\n", msg ? msg : "unknown");
        if (msg) JS_FreeCString(ctx, msg);
        JS_FreeValue(ctx, exc);
    }

    JS_FreeValue(ctx, args[0]);
    JS_FreeValue(ctx, result);
    JS_FreeValue(ctx, fn);
    JS_FreeValue(ctx, totp_obj);
    JS_FreeValue(ctx, v84);
    JS_FreeValue(ctx, global);
    return out;
}

int jsbridge_load(const char *filename) {
    if (!ctx) return -1;
    return load_js_file(ctx, filename);
}

char *jsbridge_eval(const char *code) {
    if (!ctx) return NULL;
    JSValue val = JS_Eval(ctx, code, strlen(code), "<eval>", JS_EVAL_TYPE_GLOBAL);
    if (JS_IsException(val)) {
        JSValue exc = JS_GetException(ctx);
        const char *msg = JS_ToCString(ctx, exc);
        fprintf(stderr, "JS error: %s\n", msg ? msg : "unknown");
        if (msg) JS_FreeCString(ctx, msg);
        JS_FreeValue(ctx, exc);
        JS_FreeValue(ctx, val);
        return NULL;
    }
    const char *str = JS_ToCString(ctx, val);
    char *out = str ? strdup(str) : NULL;
    if (str) JS_FreeCString(ctx, str);
    JS_FreeValue(ctx, val);
    return out;
}

char *jsbridge_encrypt_field(const unsigned char *key, size_t key_len,
                             const char *field_label, const char *plaintext) {
    if (!ctx) return NULL;

    JSValue global = JS_GetGlobalObject(ctx);
    JSValue v84 = JS_GetPropertyStr(ctx, global, "vault1984");
    JSValue crypto_obj = JS_GetPropertyStr(ctx, v84, "crypto");
    JSValue fn = JS_GetPropertyStr(ctx, crypto_obj, "encrypt_field");

    JSValue key_arr = js_new_uint8array(ctx, key, key_len);
    JSValue args[3] = {
        key_arr,
        JS_NewString(ctx, field_label),
        JS_NewString(ctx, plaintext)
    };

    JSValue result = JS_Call(ctx, fn, JS_UNDEFINED, 3, args);

    char *out = NULL;
    if (!JS_IsException(result)) {
        const char *str = JS_ToCString(ctx, result);
        if (str) { out = strdup(str); JS_FreeCString(ctx, str); }
    } else {
        JSValue exc = JS_GetException(ctx);
        const char *msg = JS_ToCString(ctx, exc);
        fprintf(stderr, "error: encrypt_field: %s\n", msg ? msg : "unknown");
        if (msg) JS_FreeCString(ctx, msg);
        JS_FreeValue(ctx, exc);
    }

    for (int i = 0; i < 3; i++) JS_FreeValue(ctx, args[i]);
    JS_FreeValue(ctx, result);
    JS_FreeValue(ctx, fn);
    JS_FreeValue(ctx, crypto_obj);
    JS_FreeValue(ctx, v84);
    JS_FreeValue(ctx, global);
    return out;
}

char *jsbridge_decrypt_field(const unsigned char *key, size_t key_len,
                             const char *field_label, const char *ciphertext_b64) {
    if (!ctx) return NULL;

    JSValue global = JS_GetGlobalObject(ctx);
    JSValue v84 = JS_GetPropertyStr(ctx, global, "vault1984");
    JSValue crypto_obj = JS_GetPropertyStr(ctx, v84, "crypto");
    JSValue fn = JS_GetPropertyStr(ctx, crypto_obj, "decrypt_field");

    JSValue key_arr = js_new_uint8array(ctx, key, key_len);
    JSValue args[3] = {
        key_arr,
        JS_NewString(ctx, field_label),
        JS_NewString(ctx, ciphertext_b64)
    };

    JSValue result = JS_Call(ctx, fn, JS_UNDEFINED, 3, args);

    char *out = NULL;
    if (!JS_IsException(result)) {
        const char *str = JS_ToCString(ctx, result);
        if (str) { out = strdup(str); JS_FreeCString(ctx, str); }
    } else {
        JSValue exc = JS_GetException(ctx);
        const char *msg = JS_ToCString(ctx, exc);
        fprintf(stderr, "error: decrypt_field: %s\n", msg ? msg : "unknown");
        if (msg) JS_FreeCString(ctx, msg);
        JS_FreeValue(ctx, exc);
    }

    for (int i = 0; i < 3; i++) JS_FreeValue(ctx, args[i]);
    JS_FreeValue(ctx, result);
    JS_FreeValue(ctx, fn);
    JS_FreeValue(ctx, crypto_obj);
    JS_FreeValue(ctx, v84);
    JS_FreeValue(ctx, global);
    return out;
}
