#include "qjs_crypto.h"
#include <stdint.h>
#include <stdlib.h>
#include <string.h>

#include "monocypher.h"
#include "qjs_blob.h"

#include <stdint.h>
#include <stddef.h>

#if defined(_WIN32)
// ------- Windows: use BCryptGenRandom -------
#include <windows.h>
#include <bcrypt.h>

int randombytes(void *buf, size_t n) {
    NTSTATUS status = BCryptGenRandom(NULL, (PUCHAR)buf, (ULONG)n, BCRYPT_USE_SYSTEM_PREFERRED_RNG);
    return (status == 0) ? 0 : -1;
}

#elif defined(__linux__)
// ------- Linux: try getrandom, fall back to /dev/urandom -------
#include <unistd.h>
#include <sys/syscall.h>
#include <fcntl.h>
#include <errno.h>

// If we have a new enough libc and kernel, getrandom is available.
// Otherwise, we’ll do a /dev/urandom fallback.
#include <sys/stat.h>

static int randombytes_fallback(void *buf, size_t n) {
    int fd = open("/dev/urandom", O_RDONLY);
    if (fd < 0) return -1;
    ssize_t r = read(fd, buf, n);
    close(fd);
    return (r == (ssize_t)n) ? 0 : -1;
}

int randombytes(void *buf, size_t n) {
#ifdef SYS_getrandom
    // Try getrandom(2) if available
    ssize_t ret = syscall(SYS_getrandom, buf, n, 0);
    if (ret < 0) {
        // If getrandom is not supported or fails, fall back
        if (errno == ENOSYS) {
            return randombytes_fallback(buf, n);
        }
        return -1;
    }
    return (ret == (ssize_t)n) ? 0 : -1;
#else
    // getrandom not available, just fallback
    return randombytes_fallback(buf, n);
#endif
}

#else
// ------- Other Unix: read from /dev/urandom -------
#include <fcntl.h>
#include <unistd.h>

int randombytes(void *buf, size_t n) {
    int fd = open("/dev/urandom", O_RDONLY);
    if (fd < 0) return -1;
    ssize_t r = read(fd, buf, n);
    close(fd);
    return (r == (ssize_t)n) ? 0 : -1;
}
#endif

static inline void to_hex(const uint8_t *in, size_t in_len, char *out)
{
    static const char hexchars[] = "0123456789abcdef";
    for (size_t i = 0; i < in_len; i++) {
        out[2*i    ] = hexchars[(in[i] >> 4) & 0x0F];
        out[2*i + 1] = hexchars[ in[i]       & 0x0F];
    }
    out[2 * in_len] = '\0'; // null-terminate
}

static inline int nibble_from_char(char c, uint8_t *nibble)
{
    if (c >= '0' && c <= '9') { *nibble = (uint8_t)(c - '0');      return 0; }
    if (c >= 'a' && c <= 'f') { *nibble = (uint8_t)(c - 'a' + 10); return 0; }
    if (c >= 'A' && c <= 'F') { *nibble = (uint8_t)(c - 'A' + 10); return 0; }
    return -1; // invalid char
}

static inline int from_hex(const char *hex, uint8_t *out, size_t out_len)
{
    for (size_t i = 0; i < out_len; i++) {
        uint8_t hi, lo;
        if (nibble_from_char(hex[2*i],     &hi) < 0) return -1;
        if (nibble_from_char(hex[2*i + 1], &lo) < 0) return -1;
        out[i] = (uint8_t)((hi << 4) | lo);
    }
    return 0;
}

// Convert a JSValue containing a 64-character hex string into a 32-byte array.
static inline void js2crypto(JSContext *js, JSValue v, uint8_t *crypto)
{
    size_t hex_len;
    const char *hex_str = JS_ToCStringLen(js, &hex_len, v);
    if (!hex_str)
        return;

    if (hex_len != 64) {
        JS_FreeCString(js, hex_str);
        JS_ThrowTypeError(js, "js2crypto: expected 64-hex-char string");
        return;
    }

    if (from_hex(hex_str, crypto, 32) < 0) {
        JS_FreeCString(js, hex_str);
        JS_ThrowTypeError(js, "js2crypto: invalid hex encoding");
        return;
    }

    JS_FreeCString(js, hex_str);
}

static inline JSValue crypto2js(JSContext *js, const uint8_t *crypto)
{
    char hex[65];  // 32*2 + 1 for null terminator
    to_hex(crypto, 32, hex);
    return JS_NewString(js, hex);
}

JSValue js_crypto_keypair(JSContext *js, JSValue self, int argc, JSValue *argv) {
  JSValue ret = JS_NewObject(js);

  uint8_t public[32];
  uint8_t private[32];

  randombytes(private,32);

  private[0] &= 248;
  private[31] &= 127;
  private[31] |= 64;

  crypto_x25519_public_key(public,private);

  JS_SetPropertyStr(js, ret, "public", crypto2js(js, public));
  JS_SetPropertyStr(js, ret, "private", crypto2js(js,private));
  return ret;
}

JSValue js_crypto_shared(JSContext *js, JSValue self, int argc, JSValue *argv)
{
    if (argc < 1 || !JS_IsObject(argv[0])) {
        return JS_ThrowTypeError(js, "crypto.shared: expected an object argument");
    }

    JSValue obj = argv[0];

    JSValue val_pub = JS_GetPropertyStr(js, obj, "public");
    if (JS_IsException(val_pub)) {
        JS_FreeValue(js, val_pub);
        return JS_EXCEPTION;
    }

    JSValue val_priv = JS_GetPropertyStr(js, obj, "private");
    if (JS_IsException(val_priv)) {
        JS_FreeValue(js, val_pub);
        JS_FreeValue(js, val_priv);
        return JS_EXCEPTION;
    }

    uint8_t pub[32], priv[32];
    js2crypto(js, val_pub,  pub);
    js2crypto(js, val_priv, priv);

    JS_FreeValue(js, val_pub);
    JS_FreeValue(js, val_priv);

    uint8_t shared[32];
    crypto_x25519(shared, priv, pub);

    return crypto2js(js, shared);
}

JSValue js_crypto_random(JSContext *js, JSValue self, int argc, JSValue *argv)
{
  // 1) Pull 64 bits of cryptographically secure randomness
  uint64_t r;
  if (randombytes(&r, sizeof(r)) != 0) {
    // If something fails (extremely rare), throw an error
    return JS_ThrowInternalError(js, "crypto.random: unable to get random bytes");
  }

  // 2) Convert r to a double in the range [0,1). 
  //    We divide by (UINT64_MAX + 1.0) to ensure we never produce exactly 1.0.
  double val = (double)r / ((double)UINT64_MAX + 1.0);

  // 3) Return that as a JavaScript number
  return JS_NewFloat64(js, val);
}

JSValue js_crypto_random_fit(JSContext *js, JSValue self, int argc, JSValue *argv)
{
  int32_t r;
  if (randombytes(&r, sizeof(r)) != 0) {
    return JS_ThrowInternalError(js, "crypto.random: unable to get random bytes");
  }

  return JS_NewInt32(js, r);
}

JSValue js_crypto_hash(JSContext *js, JSValue self, int argc, JSValue *argv)
{
    if (argc < 1)
        return JS_ThrowTypeError(js, "hash requires at least one argument");
    
    // Get input data
    size_t data_len;
    void *data = js_get_blob_data(js, &data_len, argv[0]);
    if (!data)
        return JS_ThrowTypeError(js, "hash: first argument must be an ArrayBuffer");
    
    // Get hash length (default 32)
    int32_t hash_len = 32;
    if (argc > 1) {
        if (JS_ToInt32(js, &hash_len, argv[1]))
            return JS_EXCEPTION;
        if (hash_len < 1 || hash_len > 64)
            return JS_ThrowRangeError(js, "hash length must be between 1 and 64");
    }
    
    // Allocate output buffer
    uint8_t *hash = js_malloc(js, hash_len);
    if (!hash)
        return JS_EXCEPTION;
    
    // Compute BLAKE2b hash
    crypto_blake2b(hash, hash_len, data, data_len);
    
    // Return as blob
    JSValue result = js_new_blob_stoned_copy(js, hash, hash_len);
    js_free(js, hash);
    return result;
}

static const JSCFunctionListEntry js_crypto_funcs[] = {
  JS_CFUNC_DEF("keypair", 0, js_crypto_keypair),
  JS_CFUNC_DEF("shared", 1, js_crypto_shared),
  JS_CFUNC_DEF("random", 0, js_crypto_random),
  JS_CFUNC_DEF("random_fit", 0, js_crypto_random_fit),
  JS_CFUNC_DEF("hash", 2, js_crypto_hash),
};

JSValue js_crypto_use(JSContext *js)
{
  JSValue obj = JS_NewObject(js);
  JS_SetPropertyFunctionList(js, obj, js_crypto_funcs, sizeof(js_crypto_funcs)/sizeof(js_crypto_funcs[0]));
  return obj;
}
