/*
 * NOSTR Core Library - NIP-013: Proof of Work
 */

#include "nip013.h"
#include "nip001.h"
#include "utils.h"
#include "../cjson/cJSON.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <stdint.h>
#include "../nostr_core/nostr_common.h"

/**
 * Count leading zero bits in a hash (NIP-13 reference implementation)
 */
static int zero_bits(unsigned char b) {
    int n = 0;
    
    if (b == 0)
        return 8;
    
    while (b >>= 1)
        n++;
    
    return 7-n;
}

/**
 * Find the number of leading zero bits in a hash (NIP-13 reference implementation)
 */
static int count_leading_zero_bits(unsigned char *hash) {
    int bits, total, i;
    for (i = 0, total = 0; i < 32; i++) {
        bits = zero_bits(hash[i]);
        total += bits;
        if (bits != 8)
            break;
    }
    return total;
}

/**
 * Add or update nonce tag with target difficulty
 */
static int update_nonce_tag_with_difficulty(cJSON* tags, uint64_t nonce, int target_difficulty) {
    if (!tags) return -1;
    
    // Look for existing nonce tag and remove it
    cJSON* tag = NULL;
    int index = 0;
    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) {
                // Remove existing nonce tag
                cJSON_DetachItemFromArray(tags, index);
                cJSON_Delete(tag);
                break;
            }
        }
        index++;
    }
    
    // Add new nonce tag with format: ["nonce", "<nonce>", "<target_difficulty>"]
    cJSON* nonce_tag = cJSON_CreateArray();
    if (!nonce_tag) return -1;
    
    char nonce_str[32];
    char difficulty_str[16];
    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;
}

/**
 * Helper function to replace event content with successful PoW result
 */
static void replace_event_content(cJSON* target_event, cJSON* source_event) {
    // Remove old fields
    cJSON_DeleteItemFromObject(target_event, "id");
    cJSON_DeleteItemFromObject(target_event, "sig");
    cJSON_DeleteItemFromObject(target_event, "tags");
    cJSON_DeleteItemFromObject(target_event, "created_at");
    
    // Copy new fields from successful event
    cJSON* id = cJSON_GetObjectItem(source_event, "id");
    cJSON* sig = cJSON_GetObjectItem(source_event, "sig");
    cJSON* tags = cJSON_GetObjectItem(source_event, "tags");
    cJSON* created_at = cJSON_GetObjectItem(source_event, "created_at");
    
    if (id) cJSON_AddItemToObject(target_event, "id", cJSON_Duplicate(id, 1));
    if (sig) cJSON_AddItemToObject(target_event, "sig", cJSON_Duplicate(sig, 1));
    if (tags) cJSON_AddItemToObject(target_event, "tags", cJSON_Duplicate(tags, 1));
    if (created_at) cJSON_AddItemToObject(target_event, "created_at", cJSON_Duplicate(created_at, 1));
}

/**
 * Add NIP-13 Proof of Work to an event
 * 
 * @param event The event to add proof of work to
 * @param private_key The private key for re-signing the event
 * @param target_difficulty Target number of leading zero bits (default: 4 if 0)
 * @param max_attempts Maximum number of mining attempts (default: 10,000,000 if <= 0)
 * @param progress_report_interval How often to call progress callback (default: 10,000 if <= 0)
 * @param timestamp_update_interval How often to update timestamp (default: 10,000 if <= 0)
 * @param progress_callback Optional callback for mining progress
 * @param user_data User data for progress callback
 * @return NOSTR_SUCCESS on success, error code on failure
 */
int nostr_add_proof_of_work(cJSON* event, const unsigned char* private_key, 
                           int target_difficulty, int max_attempts,
                           int progress_report_interval, int timestamp_update_interval,
                           void (*progress_callback)(int current_difficulty, uint64_t nonce, void* user_data),
                           void* user_data) {
    if (!event || !private_key) {
        return NOSTR_ERROR_INVALID_INPUT;
    }
    
    // Set default difficulty if not specified (but allow 0 to disable PoW)
    if (target_difficulty < 0) {
        target_difficulty = 4;
    }
    
    // If target_difficulty is 0, skip proof of work entirely
    if (target_difficulty == 0) {
        return NOSTR_SUCCESS;
    }
    
    // Set default values for parameters
    if (max_attempts <= 0) {
        max_attempts = 10000000;  // 10 million default
    }
    if (progress_report_interval <= 0) {
        progress_report_interval = 10000;  // Every 10,000 attempts
    }
    if (timestamp_update_interval <= 0) {
        timestamp_update_interval = 10000;  // Every 10,000 attempts
    }
    
    // Extract event data for reconstruction
    cJSON* kind_item = cJSON_GetObjectItem(event, "kind");
    cJSON* content_item = cJSON_GetObjectItem(event, "content");
    cJSON* created_at_item = cJSON_GetObjectItem(event, "created_at");
    cJSON* tags_item = cJSON_GetObjectItem(event, "tags");
    
    if (!kind_item || !content_item || !created_at_item || !tags_item) {
        return NOSTR_ERROR_INVALID_INPUT;
    }
    
    int kind = (int)cJSON_GetNumberValue(kind_item);
    const char* content = cJSON_GetStringValue(content_item);
    time_t original_timestamp = (time_t)cJSON_GetNumberValue(created_at_item);
    
    uint64_t nonce = 0;
    int attempts = 0;
    time_t current_timestamp = original_timestamp;
    
    // PoW difficulty tracking variables
    int best_difficulty_this_round = 0;
    int best_difficulty_overall = 0;
    
    // Mining loop
    while (attempts < max_attempts) {
        // Update timestamp based on timestamp_update_interval
        if (attempts % timestamp_update_interval == 0) {
            current_timestamp = time(NULL);
#ifdef ENABLE_DEBUG_LOGGING
            FILE* f = fopen("debug.log", "a"); 
            if (f) { 
                fprintf(f, "PoW mining: %d attempts, best this round: %d, overall best: %d, goal: %d\n", 
                        attempts, best_difficulty_this_round, best_difficulty_overall, target_difficulty); 
                fclose(f); 
            }
#endif
            // Reset best difficulty for the new round
            best_difficulty_this_round = 0;
        }
        
        // Call progress callback at specified intervals
        if (progress_callback && (attempts % progress_report_interval == 0)) {
            progress_callback(best_difficulty_overall, nonce, user_data);
        }
        
        // Create working copy of tags and add nonce
        cJSON* working_tags = cJSON_Duplicate(tags_item, 1);
        if (!working_tags) {
            return NOSTR_ERROR_MEMORY_FAILED;
        }
        
        if (update_nonce_tag_with_difficulty(working_tags, nonce, target_difficulty) != 0) {
            cJSON_Delete(working_tags);
            return NOSTR_ERROR_CRYPTO_FAILED;
        }
        
        // Create and sign new event with current nonce
        cJSON* test_event = nostr_create_and_sign_event(kind, content, working_tags, 
                                                       private_key, current_timestamp);
        cJSON_Delete(working_tags);
        
        if (!test_event) {
            return NOSTR_ERROR_CRYPTO_FAILED;
        }
        
        // Check PoW difficulty
        cJSON* id_item = cJSON_GetObjectItem(test_event, "id");
        if (!id_item || !cJSON_IsString(id_item)) {
            cJSON_Delete(test_event);
            return NOSTR_ERROR_CRYPTO_FAILED;
        }
        
        const char* event_id = cJSON_GetStringValue(id_item);
        unsigned char hash[32];
        if (nostr_hex_to_bytes(event_id, hash, 32) != NOSTR_SUCCESS) {
            cJSON_Delete(test_event);
            return NOSTR_ERROR_CRYPTO_FAILED;
        }
        
        // Count leading zero bits using NIP-13 method
        int current_difficulty = count_leading_zero_bits(hash);
        
        // Update difficulty tracking
        if (current_difficulty > best_difficulty_this_round) {
            best_difficulty_this_round = current_difficulty;
        }
        if (current_difficulty > best_difficulty_overall) {
            best_difficulty_overall = current_difficulty;
        }
        
        // Check if we've reached the target
        if (current_difficulty >= target_difficulty) {
#ifdef ENABLE_DEBUG_LOGGING
            FILE* f = fopen("debug.log", "a"); 
            if (f) { 
                fprintf(f, "PoW SUCCESS: Found difficulty %d (target %d) at nonce %llu after %d attempts\n", 
                        current_difficulty, target_difficulty, (unsigned long long)nonce, attempts + 1);
                
                // Print the final event JSON
                char* event_json = cJSON_Print(test_event);
                if (event_json) {
                    fprintf(f, "Final event: %s\n", event_json);
                    free(event_json);
                }
                fclose(f); 
            }
#endif
            
            // Copy successful result back to input event
            replace_event_content(event, test_event);
            cJSON_Delete(test_event);
            return NOSTR_SUCCESS;
        }
        
        cJSON_Delete(test_event);
        nonce++;
        attempts++;
    }
    
#ifdef ENABLE_DEBUG_LOGGING
    // Debug logging - failure
    FILE* f = fopen("debug.log", "a"); 
    if (f) { 
        fprintf(f, "PoW FAILED: Mining failed after %d attempts\n", max_attempts); 
        fclose(f); 
    }
#endif
    
    // If we reach here, we've exceeded max attempts
    return NOSTR_ERROR_CRYPTO_FAILED;
}

/**
 * Calculate PoW difficulty (leading zero bits) for an event ID
 *
 * @param event_id_hex Hexadecimal event ID string (64 characters)
 * @return Number of leading zero bits, or negative error code on failure
 */
int nostr_calculate_pow_difficulty(const char* event_id_hex) {
    if (!event_id_hex) {
        return NOSTR_ERROR_INVALID_INPUT;
    }
    
    // Validate hex string length (should be 64 characters for SHA-256)
    size_t hex_len = strlen(event_id_hex);
    if (hex_len != 64) {
        return NOSTR_ERROR_NIP13_CALCULATION;
    }
    
    // Convert hex to bytes
    unsigned char hash[32];
    if (nostr_hex_to_bytes(event_id_hex, hash, 32) != NOSTR_SUCCESS) {
        return NOSTR_ERROR_NIP13_CALCULATION;
    }
    
    // Use existing NIP-13 reference implementation
    return count_leading_zero_bits(hash);
}

/**
 * Extract nonce information from event tags
 *
 * @param event Complete event JSON object
 * @param nonce_out Output pointer for nonce value (can be NULL)
 * @param target_difficulty_out Output pointer for target difficulty (can be NULL)
 * @return NOSTR_SUCCESS if nonce tag found, NOSTR_ERROR_NIP13_NO_NONCE_TAG if not found, other error codes on failure
 */
int nostr_extract_nonce_info(cJSON* event, uint64_t* nonce_out, int* target_difficulty_out) {
    if (!event) {
        return NOSTR_ERROR_INVALID_INPUT;
    }
    
    // Initialize output values
    if (nonce_out) *nonce_out = 0;
    if (target_difficulty_out) *target_difficulty_out = -1;
    
    // Get tags array
    cJSON* tags = cJSON_GetObjectItem(event, "tags");
    if (!tags || !cJSON_IsArray(tags)) {
        return NOSTR_ERROR_NIP13_NO_NONCE_TAG;
    }
    
    // Search for nonce tag
    cJSON* tag = NULL;
    cJSON_ArrayForEach(tag, tags) {
        if (!cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) {
            continue;
        }
        
        // Check if this is a nonce tag
        cJSON* tag_type = cJSON_GetArrayItem(tag, 0);
        if (!tag_type || !cJSON_IsString(tag_type)) {
            continue;
        }
        
        const char* tag_name = cJSON_GetStringValue(tag_type);
        if (!tag_name || strcmp(tag_name, "nonce") != 0) {
            continue;
        }
        
        // Extract nonce value (second element)
        cJSON* nonce_item = cJSON_GetArrayItem(tag, 1);
        if (!nonce_item || !cJSON_IsString(nonce_item)) {
            return NOSTR_ERROR_NIP13_INVALID_NONCE_TAG;
        }
        
        const char* nonce_str = cJSON_GetStringValue(nonce_item);
        if (!nonce_str) {
            return NOSTR_ERROR_NIP13_INVALID_NONCE_TAG;
        }
        
        // Parse nonce value
        char* endptr;
        uint64_t nonce_val = strtoull(nonce_str, &endptr, 10);
        if (*endptr != '\0') {
            return NOSTR_ERROR_NIP13_INVALID_NONCE_TAG;
        }
        
        if (nonce_out) *nonce_out = nonce_val;
        
        // Extract target difficulty (third element, optional)
        if (cJSON_GetArraySize(tag) >= 3) {
            cJSON* target_item = cJSON_GetArrayItem(tag, 2);
            if (target_item && cJSON_IsString(target_item)) {
                const char* target_str = cJSON_GetStringValue(target_item);
                if (target_str) {
                    char* endptr2;
                    long target_val = strtol(target_str, &endptr2, 10);
                    if (*endptr2 == '\0' && target_val >= 0) {
                        if (target_difficulty_out) *target_difficulty_out = (int)target_val;
                    }
                }
            }
        }
        
        return NOSTR_SUCCESS;
    }
    
    // No nonce tag found
    return NOSTR_ERROR_NIP13_NO_NONCE_TAG;
}

/**
 * Validate just the nonce tag format (without PoW calculation)
 *
 * @param nonce_tag_array JSON array representing a nonce tag
 * @return NOSTR_SUCCESS if valid format, error code otherwise
 */
int nostr_validate_nonce_tag(cJSON* nonce_tag_array) {
    if (!nonce_tag_array || !cJSON_IsArray(nonce_tag_array)) {
        return NOSTR_ERROR_NIP13_INVALID_NONCE_TAG;
    }
    
    int array_size = cJSON_GetArraySize(nonce_tag_array);
    if (array_size < 2) {
        return NOSTR_ERROR_NIP13_INVALID_NONCE_TAG;
    }
    
    // First element should be "nonce"
    cJSON* tag_type = cJSON_GetArrayItem(nonce_tag_array, 0);
    if (!tag_type || !cJSON_IsString(tag_type)) {
        return NOSTR_ERROR_NIP13_INVALID_NONCE_TAG;
    }
    
    const char* tag_name = cJSON_GetStringValue(tag_type);
    if (!tag_name || strcmp(tag_name, "nonce") != 0) {
        return NOSTR_ERROR_NIP13_INVALID_NONCE_TAG;
    }
    
    // Second element should be a valid nonce (numeric string)
    cJSON* nonce_item = cJSON_GetArrayItem(nonce_tag_array, 1);
    if (!nonce_item || !cJSON_IsString(nonce_item)) {
        return NOSTR_ERROR_NIP13_INVALID_NONCE_TAG;
    }
    
    const char* nonce_str = cJSON_GetStringValue(nonce_item);
    if (!nonce_str) {
        return NOSTR_ERROR_NIP13_INVALID_NONCE_TAG;
    }
    
    // Validate nonce is a valid number
    char* endptr;
    strtoull(nonce_str, &endptr, 10);
    if (*endptr != '\0') {
        return NOSTR_ERROR_NIP13_INVALID_NONCE_TAG;
    }
    
    // Third element (target difficulty) is optional, but if present should be valid
    if (array_size >= 3) {
        cJSON* target_item = cJSON_GetArrayItem(nonce_tag_array, 2);
        if (target_item && cJSON_IsString(target_item)) {
            const char* target_str = cJSON_GetStringValue(target_item);
            if (target_str) {
                char* endptr2;
                strtol(target_str, &endptr2, 10);
                if (*endptr2 != '\0') {
                    return NOSTR_ERROR_NIP13_INVALID_NONCE_TAG;
                }
            }
        }
    }
    
    return NOSTR_SUCCESS;
}

/**
 * Validate Proof of Work for an event according to NIP-13
 *
 * @param event Complete event JSON object to validate
 * @param min_difficulty Minimum difficulty required by the relay (0 = no requirement)
 * @param validation_flags Bitflags for validation options
 * @param result_info Optional output struct for detailed results (can be NULL)
 * @return NOSTR_SUCCESS if PoW is valid and meets requirements, error code otherwise
 */
int nostr_validate_pow(cJSON* event, int min_difficulty, int validation_flags,
                      nostr_pow_result_t* result_info) {
    if (!event) {
        return NOSTR_ERROR_INVALID_INPUT;
    }
    
    // Initialize result info if provided
    if (result_info) {
        result_info->actual_difficulty = 0;
        result_info->committed_target = -1;
        result_info->nonce_value = 0;
        result_info->has_nonce_tag = 0;
        result_info->error_detail[0] = '\0';
    }
    
    // Get event ID for PoW calculation
    cJSON* id_item = cJSON_GetObjectItem(event, "id");
    if (!id_item || !cJSON_IsString(id_item)) {
        if (result_info) {
            snprintf(result_info->error_detail, sizeof(result_info->error_detail),
                    "Missing or invalid event ID");
        }
        return NOSTR_ERROR_EVENT_INVALID_ID;
    }
    
    const char* event_id = cJSON_GetStringValue(id_item);
    if (!event_id) {
        if (result_info) {
            snprintf(result_info->error_detail, sizeof(result_info->error_detail),
                    "Event ID is not a string");
        }
        return NOSTR_ERROR_EVENT_INVALID_ID;
    }
    
    // Calculate actual PoW difficulty
    int actual_difficulty = nostr_calculate_pow_difficulty(event_id);
    if (actual_difficulty < 0) {
        if (result_info) {
            snprintf(result_info->error_detail, sizeof(result_info->error_detail),
                    "Failed to calculate PoW difficulty");
        }
        return actual_difficulty; // Return the specific error from calculation
    }
    
    if (result_info) {
        result_info->actual_difficulty = actual_difficulty;
    }
    
    // Check if minimum difficulty requirement is met
    if (min_difficulty > 0 && actual_difficulty < min_difficulty) {
        if (result_info) {
            snprintf(result_info->error_detail, sizeof(result_info->error_detail),
                    "Insufficient difficulty: %d < %d", actual_difficulty, min_difficulty);
        }
        return NOSTR_ERROR_NIP13_INSUFFICIENT;
    }
    
    // Extract nonce information if validation flags require it
    uint64_t nonce_value = 0;
    int committed_target = -1;
    int nonce_extract_result = nostr_extract_nonce_info(event, &nonce_value, &committed_target);
    
    if (result_info) {
        result_info->nonce_value = nonce_value;
        result_info->committed_target = committed_target;
        result_info->has_nonce_tag = (nonce_extract_result == NOSTR_SUCCESS) ? 1 : 0;
    }
    
    // Validate nonce tag presence if required
    if (validation_flags & NOSTR_POW_VALIDATE_NONCE_TAG) {
        if (nonce_extract_result == NOSTR_ERROR_NIP13_NO_NONCE_TAG) {
            if (result_info) {
                snprintf(result_info->error_detail, sizeof(result_info->error_detail),
                        "Missing required nonce tag");
            }
            return NOSTR_ERROR_NIP13_NO_NONCE_TAG;
        } else if (nonce_extract_result != NOSTR_SUCCESS) {
            if (result_info) {
                snprintf(result_info->error_detail, sizeof(result_info->error_detail),
                        "Invalid nonce tag format");
            }
            return nonce_extract_result;
        }
        
        // If strict format validation is enabled, validate the nonce tag structure
        if (validation_flags & NOSTR_POW_STRICT_FORMAT) {
            cJSON* tags = cJSON_GetObjectItem(event, "tags");
            if (tags && cJSON_IsArray(tags)) {
                cJSON* tag = NULL;
                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)) {
                            const char* tag_name = cJSON_GetStringValue(tag_type);
                            if (tag_name && strcmp(tag_name, "nonce") == 0) {
                                int validation_result = nostr_validate_nonce_tag(tag);
                                if (validation_result != NOSTR_SUCCESS) {
                                    if (result_info) {
                                        snprintf(result_info->error_detail, sizeof(result_info->error_detail),
                                                "Nonce tag failed strict format validation");
                                    }
                                    return validation_result;
                                }
                                break;
                            }
                        }
                    }
                }
            }
        }
    }
    
    // Validate committed target difficulty if required
    if (validation_flags & NOSTR_POW_VALIDATE_TARGET_COMMIT) {
        if (nonce_extract_result == NOSTR_SUCCESS && committed_target == -1) {
            if (result_info) {
                snprintf(result_info->error_detail, sizeof(result_info->error_detail),
                        "Missing committed target difficulty in nonce tag");
            }
            return NOSTR_ERROR_NIP13_INVALID_NONCE_TAG;
        }
        
        // Check for target/difficulty mismatch if rejecting lower targets
        if (validation_flags & NOSTR_POW_REJECT_LOWER_TARGET) {
            // According to NIP-13: "if you require 40 bits to reply to your thread and see
            // a committed target of 30, you can safely reject it even if the note has 40 bits difficulty"
            // This means we reject if committed_target < min_difficulty, not actual_difficulty
            if (committed_target != -1 && min_difficulty > 0 && committed_target < min_difficulty) {
                if (result_info) {
                    snprintf(result_info->error_detail, sizeof(result_info->error_detail),
                            "Committed target (%d) is lower than required minimum (%d)",
                            committed_target, min_difficulty);
                }
                return NOSTR_ERROR_NIP13_TARGET_MISMATCH;
            }
        }
    }
    
    // All validations passed
    if (result_info) {
        snprintf(result_info->error_detail, sizeof(result_info->error_detail),
                "PoW validation successful");
    }
    
    return NOSTR_SUCCESS;
}
