#include "HardwareRNG.h"

#include <algorithm>
#include <cstring>
#include <random>

#include "configuration.h"

#if HAS_RADIO
#include "RadioLibInterface.h"
#endif

#if defined(ARCH_NRF52)
#include <Adafruit_nRFCrypto.h>
extern Adafruit_nRFCrypto nRFCrypto;
#elif defined(ARCH_ESP32)
#include <esp_system.h>
#elif defined(ARCH_RP2040)
#include <Arduino.h>
#elif defined(ARCH_PORTDUINO)
#include <random>
#include <unistd.h>
#ifdef __linux__
#include <sys/random.h> // getrandom()
#elif defined(_WIN32)
// Order is load-bearing, hence the blank line: bcrypt.h uses LONG/ULONG from
// windows.h and does not include it itself.
#include <windows.h>

#include <bcrypt.h> // BCryptGenRandom()
#else
#include <stdlib.h> // arc4random_buf() on Darwin/BSD
#endif
#endif

namespace HardwareRNG
{

namespace
{
void fillWithRandomDevice(uint8_t *buffer, size_t length)
{
    std::random_device rd;
    size_t offset = 0;
    while (offset < length) {
        uint32_t value = rd();
        size_t toCopy = std::min(length - offset, sizeof(value));
        memcpy(buffer + offset, &value, toCopy);
        offset += toCopy;
    }
}

#if HAS_RADIO
bool mixWithLoRaEntropy(uint8_t *buffer, size_t length)
{
    // Only attempt to pull entropy from the modem if it is initialized and exposes the helper.
    // When the radio stack is disabled or has not yet been configured, we simply skip this step
    // and return false so callers know no extra mixing occurred.
    RadioLibInterface *radio = RadioLibInterface::instance;
    if (!radio) {
        // This path can run during portduinoSetup() before the console is initialized,
        // both for unit-test binaries and the simulator's meshtasticd; LOG_* dereferences `console`.
        if (console) {
            LOG_ERROR("No radio instance available to provide entropy");
        }
        return false;
    }

    constexpr size_t chunkSize = 16;
    uint8_t scratch[chunkSize];
    size_t offset = 0;
    bool mixed = false;

    while (offset < length) {
        size_t toCopy = std::min(length - offset, chunkSize);

        // randomBytes() returns false if the modem does not support it or is not ready
        // (for instance, when the radio is powered down). We break immediately to avoid
        // blocking or returning partially-filled entropy and simply report failure.
        if (!radio->randomBytes(scratch, toCopy)) {
            break;
        }

        for (size_t i = 0; i < toCopy; ++i) {
            buffer[offset + i] ^= scratch[i];
        }

        mixed = true;
        offset += toCopy;
    }

    // Avoid leaving the modem-sourced bytes sitting on the stack longer than needed.
    if (mixed) {
        memset(scratch, 0, sizeof(scratch));
    }

    return mixed;
}
#endif
} // namespace

bool fill(uint8_t *buffer, size_t length, bool useRadioEntropy)
{
    if (!buffer || length == 0) {
        return false;
    }

    bool filled = false;

#if defined(ARCH_NRF52)
    // The Nordic SDK RNG provides cryptographic-quality randomness backed by hardware.
    nRFCrypto.begin();
    auto result = nRFCrypto.Random.generate(buffer, length);
    nRFCrypto.end();
    filled = result;
#elif defined(ARCH_ESP32)
    // ESP32 exposes a true RNG via esp_fill_random().
    esp_fill_random(buffer, length);
    filled = true;
#elif defined(ARCH_RP2040)
    // RP2040 has a hardware random number generator accessible through the Arduino core.
    size_t offset = 0;
    while (offset < length) {
        uint32_t value = rp2040.hwrand32();
        size_t toCopy = std::min(length - offset, sizeof(value));
        memcpy(buffer + offset, &value, toCopy);
        offset += toCopy;
    }
    filled = true;
#elif defined(ARCH_PORTDUINO)
    // Prefer the host OS RNG first when running under Portduino.
#ifdef __linux__
    ssize_t generated = ::getrandom(buffer, length, 0);
    if (generated == static_cast<ssize_t>(length)) {
        filled = true;
    }
#elif defined(_WIN32)
    // No getrandom/arc4random on Windows; BCryptGenRandom is the documented CSPRNG.
    // Preferred over std::random_device, whose libstdc++ Windows backend reports entropy() == 0.
    if (BCryptGenRandom(NULL, buffer, static_cast<ULONG>(length), BCRYPT_USE_SYSTEM_PREFERRED_RNG) == 0) { // STATUS_SUCCESS
        filled = true;
    }
#elif defined(__EMSCRIPTEN__)
    // Browser/wasm: no getrandom/arc4random - fall through to std::random_device,
    // which emscripten backs with crypto.getRandomValues().
#else
    // arc4random_buf is available on Darwin/BSD and cannot fail.
    ::arc4random_buf(buffer, length);
    filled = true;
#endif

    if (!filled) {
        fillWithRandomDevice(buffer, length);
        filled = true;
    }
#endif

    if (!filled) {
        // As a last resort, fall back to std::random_device. This should only be reached
        // if a platform-specific source was unavailable.
        fillWithRandomDevice(buffer, length);
        filled = true;
    }

#if HAS_RADIO
    if (useRadioEntropy) {
        // Best-effort: if the radio is active and can provide modem entropy, XOR it over the
        // buffer to improve overall quality. We consider the filling a success if either a
        // good platform RNG or the modem RNG provided data, so we return true as long as at
        // least one of those steps succeeded.
        filled = mixWithLoRaEntropy(buffer, length) || filled;
    }
#endif

    return filled;
}

bool seed(uint32_t &seedOut)
{
    uint32_t candidate = 0;
    if (!fill(reinterpret_cast<uint8_t *>(&candidate), sizeof(candidate), true)) {
        return false;
    }
    seedOut = candidate;
    return true;
}

} // namespace HardwareRNG
