// calogCrypto.c -- calog cryptography library (see calogCrypto.h). Thin, binary-safe
// bindings over OpenSSL's one-shot primitives (EVP_Digest, HMAC, RAND_bytes,
// EVP_EncodeBlock/EVP_DecodeBlock): SHA-256/SHA-1 hashing, HMAC-SHA-256, random bytes,
// base64/hex codecs, and version-4 UUIDs. No shared state: every native is a pure function
// of its arguments.

#include "calogCrypto.h"
#include "calogInternal.h"

#include <limits.h>
#include <stdint.h>
#include <stdlib.h>

#include <openssl/evp.h>
#include <openssl/hmac.h>
#include <openssl/rand.h>

static int32_t cryptoBase64DecodeNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t cryptoBase64EncodeNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t cryptoBytesToHex(CalogValueT *result, const unsigned char *bytes, size_t length);
static int32_t cryptoDigestHex(CalogValueT *args, int32_t argCount, CalogValueT *result, const EVP_MD *md, const char *usage);
static int32_t cryptoHashSha1Native(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t cryptoHashSha256Native(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t cryptoHexDecodeNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t cryptoHexEncodeNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t cryptoHmacSha256Native(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t cryptoRandomBytesNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t cryptoUuidNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);

// The single source of truth for lowercase hex digit rendering, shared by cryptoBytesToHex
// and cryptoUuidNative.
static const char cryptoHexDigits[] = "0123456789abcdef";


int32_t calogCryptoRegister(CalogT *calog) {
    calogRegisterInline(calog, "cryptoHashSha256", cryptoHashSha256Native, NULL);
    calogRegisterInline(calog, "cryptoHashSha1", cryptoHashSha1Native, NULL);
    calogRegisterInline(calog, "cryptoHmacSha256", cryptoHmacSha256Native, NULL);
    calogRegisterInline(calog, "cryptoRandomBytes", cryptoRandomBytesNative, NULL);
    calogRegisterInline(calog, "cryptoBase64Encode", cryptoBase64EncodeNative, NULL);
    calogRegisterInline(calog, "cryptoBase64Decode", cryptoBase64DecodeNative, NULL);
    calogRegisterInline(calog, "cryptoHexEncode", cryptoHexEncodeNative, NULL);
    calogRegisterInline(calog, "cryptoHexDecode", cryptoHexDecodeNative, NULL);
    calogRegisterInline(calog, "cryptoUuid", cryptoUuidNative, NULL);
    return calogOkE;
}


static int32_t cryptoBase64DecodeNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
    const unsigned char *in;
    unsigned char       *out;
    int64_t              length;
    size_t               outCap;
    int32_t              decoded;
    int32_t              pad;
    int32_t              status;

    (void)userData;
    calogValueNil(result);
    if (argCount != 1 || args[0].type != calogStringE) {
        return calogFail(result, calogErrArgE, "cryptoBase64Decode expects (text)");
    }
    length = args[0].as.s.length;
    in     = (const unsigned char *)args[0].as.s.bytes;
    // EVP_DecodeBlock ignores trailing whitespace before decoding; trim it here too so the
    // decoded length and the '=' padding count are computed from the real base64 content
    // (otherwise "YQ==\n" would decode with a spurious trailing NUL).
    while (length > 0) {
        unsigned char c;
        c = in[length - 1];
        if (c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == '\v') {
            length--;
        } else {
            break;
        }
    }
    if (length == 0) {
        return calogValueString(result, "", 0);
    }
    if ((length % 4) != 0 || length > INT_MAX) {
        return calogFail(result, calogErrArgE, "cryptoBase64Decode: invalid base64 length");
    }
    // EVP_DecodeBlock treats '=' as a zero sextet no matter where it appears, so without this
    // check an interior '=' (e.g. "YQ==YQ==") would decode as silent garbage instead of failing.
    // Valid padding is at most the final two characters, and if present must run to the end.
    {
        int64_t firstPad;
        int64_t padIndex;
        firstPad = -1;
        for (padIndex = 0; padIndex < length; padIndex++) {
            if (in[padIndex] == '=') {
                firstPad = padIndex;
                break;
            }
        }
        if (firstPad >= 0) {
            if (firstPad < length - 2) {
                return calogFail(result, calogErrArgE, "cryptoBase64Decode: invalid base64 data");
            }
            for (padIndex = firstPad; padIndex < length; padIndex++) {
                if (in[padIndex] != '=') {
                    return calogFail(result, calogErrArgE, "cryptoBase64Decode: invalid base64 data");
                }
            }
        }
    }
    outCap = ((size_t)length / 4) * 3;
    out    = (unsigned char *)malloc(outCap);
    if (out == NULL) {
        return calogFail(result, calogErrOomE, "cryptoBase64Decode: out of memory");
    }
    // EVP_DecodeBlock always emits a multiple of three bytes and does NOT drop the bytes that
    // correspond to '=' padding, so trim one output byte per trailing '=' ourselves.
    decoded = EVP_DecodeBlock(out, in, (int)length);
    if (decoded < 0) {
        free(out);
        return calogFail(result, calogErrArgE, "cryptoBase64Decode: invalid base64 data");
    }
    pad = 0;
    if (in[length - 1] == '=') {
        pad++;
        if (in[length - 2] == '=') {
            pad++;
        }
    }
    status = calogValueString(result, (const char *)out, (int64_t)(decoded - pad));
    free(out);
    if (status != calogOkE) {
        return calogFail(result, status, "cryptoBase64Decode: out of memory");
    }
    return calogOkE;
}


static int32_t cryptoBase64EncodeNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
    unsigned char *out;
    size_t         inLen;
    size_t         outCap;
    int32_t        written;
    int32_t        status;

    (void)userData;
    calogValueNil(result);
    if (argCount != 1 || args[0].type != calogStringE) {
        return calogFail(result, calogErrArgE, "cryptoBase64Encode expects (data)");
    }
    // EVP_EncodeBlock returns its output length as an int, accumulated in units of 4 bytes per
    // 3 input bytes; inputs beyond (INT_MAX / 4) * 3 bytes would overflow that return value.
    if (args[0].as.s.length > (INT_MAX / 4) * 3) {
        return calogFail(result, calogErrRangeE, "cryptoBase64Encode: input too large");
    }
    inLen = (size_t)args[0].as.s.length;
    if (inLen == 0) {
        return calogValueString(result, "", 0);
    }
    outCap = ((inLen + 2) / 3) * 4 + 1;   // 4 base64 chars per 3 input bytes, plus the NUL EVP writes
    out    = (unsigned char *)malloc(outCap);
    if (out == NULL) {
        return calogFail(result, calogErrOomE, "cryptoBase64Encode: out of memory");
    }
    written = EVP_EncodeBlock(out, (const unsigned char *)args[0].as.s.bytes, (int)inLen);
    status  = calogValueString(result, (const char *)out, (int64_t)written);
    free(out);
    if (status != calogOkE) {
        return calogFail(result, status, "cryptoBase64Encode: out of memory");
    }
    return calogOkE;
}


static int32_t cryptoBytesToHex(CalogValueT *result, const unsigned char *bytes, size_t length) {
    char   *hex;
    size_t  index;
    int32_t status;

    if (length == 0) {
        return calogValueString(result, "", 0);
    }
    hex = (char *)malloc(length * 2);
    if (hex == NULL) {
        return calogErrOomE;
    }
    for (index = 0; index < length; index++) {
        unsigned char byte;
        byte               = bytes[index];
        hex[index * 2]     = cryptoHexDigits[byte >> 4];
        hex[index * 2 + 1] = cryptoHexDigits[byte & 0x0F];
    }
    status = calogValueString(result, hex, (int64_t)(length * 2));
    free(hex);
    return status;
}


static int32_t cryptoDigestHex(CalogValueT *args, int32_t argCount, CalogValueT *result, const EVP_MD *md, const char *usage) {
    unsigned char digest[EVP_MAX_MD_SIZE];
    unsigned int  digestLen;

    calogValueNil(result);
    if (argCount != 1 || args[0].type != calogStringE) {
        return calogFail(result, calogErrArgE, usage);
    }
    digestLen = 0;
    if (EVP_Digest(args[0].as.s.bytes, (size_t)args[0].as.s.length, digest, &digestLen, md, NULL) != 1) {
        return calogFail(result, calogErrUnsupportedE, "digest computation failed");
    }
    return cryptoBytesToHex(result, digest, (size_t)digestLen);
}


static int32_t cryptoHashSha1Native(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
    (void)userData;
    return cryptoDigestHex(args, argCount, result, EVP_sha1(), "cryptoHashSha1 expects (data)");
}


static int32_t cryptoHashSha256Native(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
    (void)userData;
    return cryptoDigestHex(args, argCount, result, EVP_sha256(), "cryptoHashSha256 expects (data)");
}


static int32_t cryptoHexDecodeNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
    const unsigned char *in;
    unsigned char       *out;
    int64_t              length;
    int64_t              outLen;
    int64_t              index;
    int32_t              status;

    (void)userData;
    calogValueNil(result);
    if (argCount != 1 || args[0].type != calogStringE) {
        return calogFail(result, calogErrArgE, "cryptoHexDecode expects (hexText)");
    }
    length = args[0].as.s.length;
    if ((length % 2) != 0) {
        return calogFail(result, calogErrArgE, "cryptoHexDecode: odd-length hex string");
    }
    if (length == 0) {
        return calogValueString(result, "", 0);
    }
    in     = (const unsigned char *)args[0].as.s.bytes;
    outLen = length / 2;
    out    = (unsigned char *)malloc((size_t)outLen);
    if (out == NULL) {
        return calogFail(result, calogErrOomE, "cryptoHexDecode: out of memory");
    }
    for (index = 0; index < outLen; index++) {
        int32_t hi;
        int32_t lo;
        hi = calogHexNibble(in[index * 2]);
        lo = calogHexNibble(in[index * 2 + 1]);
        if (hi < 0 || lo < 0) {
            free(out);
            return calogFail(result, calogErrArgE, "cryptoHexDecode: invalid hex digit");
        }
        out[index] = (unsigned char)((hi << 4) | lo);
    }
    status = calogValueString(result, (const char *)out, outLen);
    free(out);
    if (status != calogOkE) {
        return calogFail(result, status, "cryptoHexDecode: out of memory");
    }
    return calogOkE;
}


static int32_t cryptoHexEncodeNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
    (void)userData;
    calogValueNil(result);
    if (argCount != 1 || args[0].type != calogStringE) {
        return calogFail(result, calogErrArgE, "cryptoHexEncode expects (data)");
    }
    return cryptoBytesToHex(result, (const unsigned char *)args[0].as.s.bytes, (size_t)args[0].as.s.length);
}


static int32_t cryptoHmacSha256Native(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
    unsigned char digest[EVP_MAX_MD_SIZE];
    unsigned int  digestLen;

    (void)userData;
    calogValueNil(result);
    if (argCount != 2 || args[0].type != calogStringE || args[1].type != calogStringE) {
        return calogFail(result, calogErrArgE, "cryptoHmacSha256 expects (key, data)");
    }
    if (args[0].as.s.length > INT_MAX) {
        return calogFail(result, calogErrRangeE, "cryptoHmacSha256: key too large");
    }
    digestLen = 0;
    if (HMAC(EVP_sha256(), args[0].as.s.bytes, (int)args[0].as.s.length, (const unsigned char *)args[1].as.s.bytes, (size_t)args[1].as.s.length, digest, &digestLen) == NULL) {
        return calogFail(result, calogErrUnsupportedE, "cryptoHmacSha256 computation failed");
    }
    return cryptoBytesToHex(result, digest, (size_t)digestLen);
}


static int32_t cryptoRandomBytesNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
    unsigned char *buf;
    int64_t        count;
    int32_t        status;

    (void)userData;
    calogValueNil(result);
    if (argCount != 1 || args[0].type != calogIntE) {
        return calogFail(result, calogErrArgE, "cryptoRandomBytes expects (count)");
    }
    count = args[0].as.i;
    if (count < 0 || count > INT_MAX) {
        return calogFail(result, calogErrRangeE, "cryptoRandomBytes: count out of range");
    }
    if (count == 0) {
        return calogValueString(result, "", 0);
    }
    buf = (unsigned char *)malloc((size_t)count);
    if (buf == NULL) {
        return calogFail(result, calogErrOomE, "cryptoRandomBytes: out of memory");
    }
    if (RAND_bytes(buf, (int)count) != 1) {
        free(buf);
        return calogFail(result, calogErrUnsupportedE, "cryptoRandomBytes: RAND_bytes failed");
    }
    status = calogValueString(result, (const char *)buf, count);
    free(buf);
    if (status != calogOkE) {
        return calogFail(result, status, "cryptoRandomBytes: out of memory");
    }
    return calogOkE;
}


static int32_t cryptoUuidNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
    unsigned char bytes[16];
    char          text[37];
    int32_t       bi;
    int32_t       ti;

    (void)args;
    (void)userData;
    calogValueNil(result);
    if (argCount != 0) {
        return calogFail(result, calogErrArgE, "cryptoUuid expects no arguments");
    }
    if (RAND_bytes(bytes, (int)sizeof(bytes)) != 1) {
        return calogFail(result, calogErrUnsupportedE, "cryptoUuid: RAND_bytes failed");
    }
    bytes[6] = (unsigned char)((bytes[6] & 0x0F) | 0x40);   // version 4
    bytes[8] = (unsigned char)((bytes[8] & 0x3F) | 0x80);   // RFC 4122 variant
    ti = 0;
    for (bi = 0; bi < 16; bi++) {
        if (bi == 4 || bi == 6 || bi == 8 || bi == 10) {
            text[ti++] = '-';
        }
        text[ti++] = cryptoHexDigits[bytes[bi] >> 4];
        text[ti++] = cryptoHexDigits[bytes[bi] & 0x0F];
    }
    text[ti] = '\0';
    return calogValueString(result, text, 36);
}
