/*
 * miner.c - Multithreaded best-effort NIP-13 Proof-of-Work mining coordinator
 *
 * This module implements a server-safe mining loop that:
 *   - Runs N worker threads, each trying nonces with a unique stride
 *   - Tracks the best event (highest difficulty) across all threads
 *   - Stops when target difficulty is reached OR timeout expires
 *   - Always returns the best result found (best-effort, not failure on timeout)
 *
 * Unlike the standalone event_miner tool, this code:
 *   - Has no global variables (all state in context structs)
 *   - Never calls exit() (returns result codes)
 *   - Has no signal handlers (the server handles signals)
 *   - Uses time-based termination as the primary budget
 */

#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <time.h>
#include <pthread.h>

#include <cJSON.h>
#include <nostr_core/nip001.h>
#include <nostr_core/nip013.h>
#include <nostr_core/utils.h>

/* ---- Constants ---- */

#define MINER_MAX_THREADS    32
#define MINER_SAFETY_TIMEOUT 600  /* 10 minutes safety max when only difficulty is set */
#define MINER_NONCE_STRIDE   0   /* 0 = use thread_count as stride */

/* ---- Result structure ---- */

typedef struct {
    cJSON *best_event;        /* best event found (caller frees) */
    int    achieved_difficulty;
    int    target_difficulty;
    int    target_reached;
    int    elapsed_sec;
    uint64_t total_attempts;
} mine_result_t;

/* ---- Shared state across worker threads ---- */

typedef struct {
    cJSON           *best_event;       /* best event found so far (mutex-protected) */
    int              best_difficulty;  /* difficulty of best_event */
    uint64_t         total_attempts;   /* total attempts across all threads */
    int              target_difficulty;/* 0 = no target, mine full timeout */
    int              target_reached;   /* 1 if target was reached */
    pthread_mutex_t  mutex;            /* protects best_event, best_difficulty, total_attempts */
    volatile int     stop;             /* set when target reached or timeout */
} mine_shared_state_t;

/* ---- Per-worker context ---- */

typedef struct {
    mine_shared_state_t *shared;
    int     kind;
    char    content[65536];     /* copy of event content */
    cJSON  *tags_template;      /* this thread's copy of original tags */
    unsigned char private_key[32];
    int     thread_id;
    uint64_t nonce_start;       /* starting nonce for this thread */
    uint64_t nonce_stride;      /* increment to avoid overlap with other threads */
    time_t  deadline;           /* absolute time to stop */
    uint64_t attempts;          /* this thread's attempt count */
} miner_worker_ctx_t;

/* ---- Helper: update nonce tag in a tags array ---- */

static int miner_update_nonce_tag(cJSON *tags, uint64_t nonce, int target_difficulty) {
    cJSON *tag;
    int index = 0;

    if (!tags) return -1;

    /* Remove existing nonce tag if present */
    cJSON_ArrayForEach(tag, tags) {
        if (cJSON_IsArray(tag) && cJSON_GetArraySize(tag) >= 2) {
            cJSON *tag_type = cJSON_GetArrayItem(tag, 0);
            if (tag_type && cJSON_IsString(tag_type) &&
                strcmp(cJSON_GetStringValue(tag_type), "nonce") == 0) {
                cJSON_DetachItemFromArray(tags, index);
                cJSON_Delete(tag);
                break;
            }
        }
        index++;
    }

    /* Add new nonce tag: ["nonce", "<nonce>", "<target>"] */
    {
        cJSON *nonce_tag = cJSON_CreateArray();
        char nonce_str[32];
        char difficulty_str[16];

        if (!nonce_tag) return -1;

        snprintf(nonce_str, sizeof(nonce_str), "%llu", (unsigned long long)nonce);
        snprintf(difficulty_str, sizeof(difficulty_str), "%d", target_difficulty);

        cJSON_AddItemToArray(nonce_tag, cJSON_CreateString("nonce"));
        cJSON_AddItemToArray(nonce_tag, cJSON_CreateString(nonce_str));
        cJSON_AddItemToArray(nonce_tag, cJSON_CreateString(difficulty_str));

        cJSON_AddItemToArray(tags, nonce_tag);
    }
    return 0;
}

/* ---- Worker thread function ---- */

static void *miner_worker(void *arg) {
    miner_worker_ctx_t *ctx = (miner_worker_ctx_t *)arg;
    uint64_t nonce = ctx->nonce_start;
    time_t current_ts = time(NULL);

    while (!ctx->shared->stop && time(NULL) < ctx->deadline) {
        cJSON *working_tags;
        cJSON *signed_event;
        const char *event_id;
        int difficulty;

        /* Build tags with current nonce */
        working_tags = cJSON_Duplicate(ctx->tags_template, 1);
        if (!working_tags) break;

        if (miner_update_nonce_tag(working_tags, nonce, ctx->shared->target_difficulty) != 0) {
            cJSON_Delete(working_tags);
            break;
        }

        /* Update timestamp periodically (every 1000 attempts) to keep it fresh */
        if (ctx->attempts % 1000 == 0) {
            current_ts = time(NULL);
        }

        /* Create and sign the event with this nonce */
        signed_event = nostr_create_and_sign_event(ctx->kind, ctx->content,
                                                    working_tags, ctx->private_key,
                                                    current_ts);
        cJSON_Delete(working_tags);

        if (!signed_event) {
            nonce += ctx->nonce_stride;
            ctx->attempts++;
            continue;
        }

        /* Check difficulty of the resulting event id */
        {
            cJSON *id_item = cJSON_GetObjectItem(signed_event, "id");
            if (!id_item || !cJSON_IsString(id_item)) {
                cJSON_Delete(signed_event);
                nonce += ctx->nonce_stride;
                ctx->attempts++;
                continue;
            }
            event_id = cJSON_GetStringValue(id_item);
        }

        difficulty = nostr_calculate_pow_difficulty(event_id);

        /* Track best under mutex */
        pthread_mutex_lock(&ctx->shared->mutex);
        ctx->shared->total_attempts++;
        if (difficulty > ctx->shared->best_difficulty) {
            if (ctx->shared->best_event) {
                cJSON_Delete(ctx->shared->best_event);
            }
            ctx->shared->best_event = cJSON_Duplicate(signed_event, 1);
            ctx->shared->best_difficulty = difficulty;
        }
        if (ctx->shared->target_difficulty > 0 && difficulty >= ctx->shared->target_difficulty) {
            ctx->shared->target_reached = 1;
            ctx->shared->stop = 1;
        }
        pthread_mutex_unlock(&ctx->shared->mutex);

        cJSON_Delete(signed_event);
        nonce += ctx->nonce_stride;
        ctx->attempts++;
    }

    return NULL;
}

/* ---- Main mining coordinator ---- */

/*
 * Run multithreaded best-effort mining on `event`.
 *
 * Parameters:
 *   event            - cJSON event to mine (must have kind, content, tags, created_at)
 *   private_key      - 32-byte private key for signing
 *   target_difficulty - 0 = no target (mine full timeout); >0 = stop when reached
 *   thread_count     - number of worker threads (1..MINER_MAX_THREADS)
 *   timeout_sec      - time budget in seconds (0 = use MINER_SAFETY_TIMEOUT)
 *   result           - output struct (caller frees result->best_event)
 *
 * Returns 0 on success (result populated), -1 on error.
 */
int miner_run(cJSON *event, const unsigned char *private_key,
              int target_difficulty, int thread_count, int timeout_sec,
              mine_result_t *result)
{
    mine_shared_state_t shared;
    miner_worker_ctx_t *workers = NULL;
    pthread_t *threads = NULL;
    cJSON *kind_item, *content_item, *tags_item, *created_at_item;
    int kind;
    const char *content;
    time_t start_time;
    int effective_timeout;
    int i;
    int ret = 0;

    if (!event || !private_key || !result) {
        return -1;
    }

    memset(result, 0, sizeof(mine_result_t));

    /* Validate event has required fields */
    kind_item = cJSON_GetObjectItem(event, "kind");
    content_item = cJSON_GetObjectItem(event, "content");
    tags_item = cJSON_GetObjectItem(event, "tags");
    created_at_item = cJSON_GetObjectItem(event, "created_at");

    if (!kind_item || !content_item || !tags_item) {
        return -1;
    }

    /* If created_at is missing, add it with current time */
    if (!created_at_item) {
        cJSON_AddNumberToObject(event, "created_at", (double)time(NULL));
    }

    kind = (int)cJSON_GetNumberValue(kind_item);
    content = cJSON_GetStringValue(content_item);
    if (!content) {
        content = "";
    }

    /* Clamp thread count */
    if (thread_count < 1) thread_count = 1;
    if (thread_count > MINER_MAX_THREADS) thread_count = MINER_MAX_THREADS;

    /* Determine effective timeout */
    if (timeout_sec <= 0) {
        effective_timeout = MINER_SAFETY_TIMEOUT;
    } else {
        effective_timeout = timeout_sec;
    }

    /* Initialize shared state */
    memset(&shared, 0, sizeof(shared));
    shared.best_event = NULL;
    shared.best_difficulty = -1;
    shared.total_attempts = 0;
    shared.target_difficulty = target_difficulty;
    shared.target_reached = 0;
    shared.stop = 0;
    if (pthread_mutex_init(&shared.mutex, NULL) != 0) {
        return -1;
    }

    /* Allocate worker contexts and thread array */
    workers = (miner_worker_ctx_t *)calloc(thread_count, sizeof(miner_worker_ctx_t));
    threads = (pthread_t *)calloc(thread_count, sizeof(pthread_t));
    if (!workers || !threads) {
        free(workers);
        free(threads);
        pthread_mutex_destroy(&shared.mutex);
        return -1;
    }

    start_time = time(NULL);

    /* Set up each worker */
    for (i = 0; i < thread_count; i++) {
        workers[i].shared = &shared;
        workers[i].kind = kind;
        strncpy(workers[i].content, content, sizeof(workers[i].content) - 1);
        workers[i].content[sizeof(workers[i].content) - 1] = '\0';
        workers[i].tags_template = cJSON_Duplicate(tags_item, 1);
        memcpy(workers[i].private_key, private_key, 32);
        workers[i].thread_id = i;
        workers[i].nonce_start = (uint64_t)i;
        workers[i].nonce_stride = (uint64_t)thread_count;
        workers[i].deadline = start_time + effective_timeout;
        workers[i].attempts = 0;

        if (!workers[i].tags_template) {
            /* Allocation failed for this worker's tags — clean up what we have */
            int j;
            for (j = 0; j < i; j++) {
                cJSON_Delete(workers[j].tags_template);
            }
            free(workers);
            free(threads);
            pthread_mutex_destroy(&shared.mutex);
            return -1;
        }
    }

    /* Start threads */
    for (i = 0; i < thread_count; i++) {
        if (pthread_create(&threads[i], NULL, miner_worker, &workers[i]) != 0) {
            /* Failed to create thread i — signal stop and join what we have */
            shared.stop = 1;
            {
                int j;
                for (j = 0; j < i; j++) {
                    pthread_join(threads[j], NULL);
                }
            }
            ret = -1;
            goto cleanup;
        }
    }

    /* Wait for all threads to complete (they stop on target/timeout) */
    for (i = 0; i < thread_count; i++) {
        pthread_join(threads[i], NULL);
    }

cleanup:
    /* Sum up total attempts from all workers */
    {
        uint64_t total = 0;
        for (i = 0; i < thread_count; i++) {
            total += workers[i].attempts;
        }
        /* Use the shared counter (more accurate, includes mutex-protected increments) */
        result->total_attempts = shared.total_attempts > 0 ? shared.total_attempts : total;
    }

    /* Populate result */
    result->best_event = shared.best_event;  /* transfer ownership */
    result->achieved_difficulty = shared.best_difficulty;
    result->target_difficulty = target_difficulty;
    result->target_reached = shared.target_reached;
    result->elapsed_sec = (int)(time(NULL) - start_time);

    /* Clean up worker contexts */
    for (i = 0; i < thread_count; i++) {
        if (workers[i].tags_template) {
            cJSON_Delete(workers[i].tags_template);
        }
        /* Zeroize private key copy */
        memset(workers[i].private_key, 0, 32);
    }

    free(workers);
    free(threads);
    pthread_mutex_destroy(&shared.mutex);

    return ret;
}
