const express = require("express");
const bodyParser = require("body-parser");
const cors = require("cors");
const { ethers } = require("ethers");
const cluster = require('cluster');
const os = require('os');
require("dotenv").config();

// Master process handles API requests and load balancing
if (cluster.isPrimary) {
  const numCPUs = os.cpus().length;
  console.log(`Master process ${process.pid} starting with ${numCPUs} CPUs`);
  
  // Fork worker processes (one per CPU core)
  // Each worker will handle its own batch of transactions
  const numWorkers = Math.min(numCPUs, 8); // Cap at 8 workers
  
  for (let i = 0; i < numWorkers; i++) {
    cluster.fork({ WORKER_ID: i });
  }
  
  // Transaction tracking in master process
  const activeTransactions = new Map(); // requestId -> {workerId, status, txHash, etc}
  
  // Simple shared memory cache for minted addresses (in-memory only)
  const mintedAddresses = new Set();
  let healthMetrics = {
    startTime: Date.now(),
    totalRequests: 0,
    successfulMints: 0,
    failedMints: 0,
    workerStatus: {}
  };
  
  // Track worker status
  for (let i = 0; i < numWorkers; i++) {
    healthMetrics.workerStatus[i] = {
      status: 'starting',
      transactions: 0,
      errors: 0,
      lastActiveTime: Date.now()
    };
  }
  
  // Handle worker messages
  cluster.on('message', (worker, message) => {
    const workerId = worker.id % numWorkers;
    
    if (message.type === 'mint_success') {
      // Record successful mint
      mintedAddresses.add(message.address.toLowerCase());
      healthMetrics.successfulMints++;
      healthMetrics.workerStatus[workerId].transactions++;
      healthMetrics.workerStatus[workerId].status = 'active';
      healthMetrics.workerStatus[workerId].lastActiveTime = Date.now();
      
      // Update transaction tracking
      if (message.requestId) {
        activeTransactions.set(message.requestId, {
          status: 'confirmed',
          txHash: message.txHash,
          blockNumber: message.blockNumber,
          address: message.address,
          workerId: workerId,
          completedAt: Date.now()
        });
        
        // Clean up after 10 minutes
        setTimeout(() => {
          activeTransactions.delete(message.requestId);
        }, 10 * 60 * 1000);
      }
    } else if (message.type === 'mint_failure') {
      // Record failed mint
      healthMetrics.failedMints++;
      healthMetrics.workerStatus[workerId].errors++;
      healthMetrics.workerStatus[workerId].status = 'active';
      healthMetrics.workerStatus[workerId].lastActiveTime = Date.now();
      
      // Update transaction tracking
      if (message.requestId) {
        activeTransactions.set(message.requestId, {
          status: 'failed',
          error: message.error,
          errorCode: message.errorCode,
          address: message.address,
          workerId: workerId,
          completedAt: Date.now()
        });
        
        // Clean up after 10 minutes
        setTimeout(() => {
          activeTransactions.delete(message.requestId);
        }, 10 * 60 * 1000);
      }
    } else if (message.type === 'tx_update') {
      // Update transaction status
      if (message.requestId && activeTransactions.has(message.requestId)) {
        const txData = activeTransactions.get(message.requestId);
        activeTransactions.set(message.requestId, {
          ...txData,
          ...message.data,
          lastUpdated: Date.now()
        });
      } else if (message.requestId) {
        activeTransactions.set(message.requestId, {
          ...message.data,
          workerId: workerId,
          createdAt: Date.now(),
          lastUpdated: Date.now()
        });
      }
    } else if (message.type === 'check_minted') {
      // Check if address has minted in our cache
      const hasMinted = mintedAddresses.has(message.address.toLowerCase());
      worker.send({ type: 'minted_response', requestId: message.requestId, hasMinted });
    } else if (message.type === 'worker_status') {
      // Update worker status
      healthMetrics.workerStatus[workerId] = {
        ...healthMetrics.workerStatus[workerId],
        ...message.status,
        lastActiveTime: Date.now()
      };
    } else if (message.type === 'check_health') {
      // Worker is checking in
      healthMetrics.workerStatus[workerId].status = 'active';
      healthMetrics.workerStatus[workerId].lastActiveTime = Date.now();
    } else if (message.type === 'tx_status_response') {
      // Transaction status response from worker - we'll handle this in the request handler
    }
  });
  
  // Handle worker death and restart
  cluster.on('exit', (worker, code, signal) => {
    const workerId = worker.id % numWorkers;
    console.log(`Worker ${worker.process.pid} (ID: ${workerId}) died. Restarting...`);
    
    // Mark this worker's transactions as abandoned if they were in progress
    for (const [requestId, txData] of activeTransactions.entries()) {
      if (txData.workerId === workerId && 
          ['processing', 'submitted'].includes(txData.status)) {
        activeTransactions.set(requestId, {
          ...txData,
          status: 'worker_died',
          error: 'Worker process terminated unexpectedly',
          lastUpdated: Date.now()
        });
      }
    }
    
    // Start a new worker
    const newWorker = cluster.fork({ WORKER_ID: workerId });
    console.log(`Started new worker ${newWorker.process.pid} (ID: ${workerId})`);
  });
  
  // Set up Express for API server
  const app = express();
  
  // Middleware
  app.use(cors({
    origin: [
      /^https?:\/\/(www\.)?metarelayer\.xyz$/,
      'http://localhost:3000' 
    ],
    methods: ['POST', 'GET', 'OPTIONS'],
    allowedHeaders: ['Content-Type', 'Authorization'],
    credentials: true
  }));
  app.use(bodyParser.json({ limit: '2mb' }));
  app.use(express.json({ limit: '2mb' }));
  
  // Basic request logging
  app.use((req, res, next) => {
    if (req.method === 'POST' || req.path.startsWith('/status/')) {
      console.log(`[API] ${new Date().toISOString()} - ${req.method} ${req.url}`);
    }
    next();
  });
  
  // Load balancing function - distribute work evenly
  function getNextWorker() {
    // Find available workers and sort by least busy
    const workers = Object.values(cluster.workers);
    if (workers.length === 0) return null;
    
    // Get worker with fewest active transactions
    const workerEntries = Object.entries(healthMetrics.workerStatus);
    workerEntries.sort((a, b) => a[1].transactions - b[1].transactions);
    
    // Get the worker ID of the least busy worker
    const workerId = parseInt(workerEntries[0][0]);
    
    return Object.values(cluster.workers).find(w => w.id % numWorkers === workerId);
  }
  
  // Health check endpoint
  app.get("/health", (req, res) => {
    const uptime = Math.floor((Date.now() - healthMetrics.startTime) / 1000);
    
    res.json({
      status: "ok",
      network: "Monad Testnet",
      chainId: 10143,
      uptime: `${Math.floor(uptime / 60)}m ${uptime % 60}s`,
      workers: {
        total: Object.keys(cluster.workers).length,
        active: Object.values(healthMetrics.workerStatus)
          .filter(w => Date.now() - w.lastActiveTime < 10000).length
      },
      metrics: {
        totalRequests: healthMetrics.totalRequests,
        successfulMints: healthMetrics.successfulMints,
        failedMints: healthMetrics.failedMints,
        mintedAddressesCount: mintedAddresses.size,
        activeTransactions: activeTransactions.size,
        workerMetrics: healthMetrics.workerStatus
      }
    });
  });
  
  // Status check endpoint - NEW
  app.get("/status/:requestId", async (req, res) => {
    const { requestId } = req.params;
    
    // Check if we have this transaction in our master tracking
    if (activeTransactions.has(requestId)) {
      const txData = activeTransactions.get(requestId);
      
      // Return status directly from master
      return res.json({
        success: true,
        requestId,
        ...txData,
        cached: true
      });
    }
    
    // If not in master cache, ask all workers
    const workers = Object.values(cluster.workers);
    if (workers.length === 0) {
      return res.status(503).json({
        success: false,
        error: "No workers available"
      });
    }
    
    // Set up a promise to ask all workers
    const statusPromise = new Promise((resolve) => {
      let responsesReceived = 0;
      let transactionFound = false;
      
      // Set timeout for overall request
      const timeout = setTimeout(() => {
        if (!transactionFound) {
          resolve({ 
            status: "unknown",
            error: "Transaction not found or timed out"
          });
        }
      }, 5000);
      
      // Handler for worker responses
      function messageHandler(worker, message) {
        if (message.type === 'tx_status_response' && message.requestId === requestId) {
          responsesReceived++;
          
          // If found, resolve with data
          if (message.found && !transactionFound) {
            transactionFound = true;
            clearTimeout(timeout);
            cluster.removeListener('message', messageHandler);
            resolve(message.data);
            
            // Update master cache
            activeTransactions.set(requestId, {
              ...message.data,
              workerId: worker.id % numWorkers,
              lastUpdated: Date.now()
            });
          } 
          // If all workers have responded and none found it
          else if (responsesReceived === workers.length && !transactionFound) {
            clearTimeout(timeout);
            cluster.removeListener('message', messageHandler);
            resolve({ 
              status: "unknown",
              error: "Transaction not found"
            });
          }
        }
      }
      
      // Set up listener
      cluster.on('message', messageHandler);
      
      // Ask all workers
      workers.forEach(worker => {
        worker.send({
          type: 'tx_status_query',
          requestId
        });
      });
    });
    
    // Wait for response and send to client
    const statusData = await statusPromise;
    return res.json({
      success: true,
      requestId,
      ...statusData
    });
  });
  
  // Check mint status endpoint
  app.get("/hasMinted/:address", async (req, res) => {
    try {
      const { address } = req.params;
      if (!ethers.isAddress(address)) {
        return res.status(400).json({
          success: false,
          error: "Invalid Ethereum address format"
        });
      }
      
      // Check memory cache first
      if (mintedAddresses.has(address.toLowerCase())) {
        return res.json({ success: true, hasMinted: true });
      }
      
      // Check with a worker
      const worker = getNextWorker();
      if (!worker) {
        return res.status(503).json({
          success: false,
          error: "No workers available"
        });
      }
      
      // Generate a unique request ID
      const requestId = `req_${Date.now()}_${Math.floor(Math.random() * 1000000)}`;
      
      // Send request to worker and wait for response
      const mintedCheckPromise = new Promise((resolve, reject) => {
        // Set timeout
        const timeout = setTimeout(() => {
          reject(new Error("Worker response timeout"));
        }, 5000);
        
        // Listen for response
        function messageHandler(message) {
          if (message.type === 'minted_response' && message.requestId === requestId) {
            clearTimeout(timeout);
            worker.removeListener('message', messageHandler);
            resolve(message.hasMinted);
          }
        }
        
        worker.on('message', messageHandler);
        
        // Send request to worker
        worker.send({ type: 'check_minted', address, requestId });
      });
      
      const hasMinted = await mintedCheckPromise;
      return res.json({ success: true, hasMinted });
    } catch (error) {
      console.error("Error checking mint status:", error);
      return res.status(500).json({
        success: false,
        error: `Error checking mint status: ${error.message}`
      });
    }
  });
  
  // Relay endpoint
  app.post("/", (req, res) => {
    // Update metrics
    healthMetrics.totalRequests++;
    
    const { user, tokenId, sigR, sigS, sigV } = req.body;
    
    // Input validation
    if (!user || tokenId === undefined || !sigR || !sigS || sigV === undefined) {
      console.error("Missing required parameters");
      return res.status(400).json({
        success: false,
        error: "Missing required parameters",
        errorType: "INVALID_PARAMETERS",
        received: { user, tokenId, sigR, sigS, sigV }
      });
    }
    
    // Check if user has already minted
    if (mintedAddresses.has(user.toLowerCase())) {
      return res.status(409).json({
        success: false,
        error: "User has already minted an NFT",
        errorType: "ALREADY_MINTED"
      });
    }
    
    // Get least busy worker
    const worker = getNextWorker();
    if (!worker) {
      return res.status(503).json({
        success: false,
        error: "No workers available",
        errorType: "NO_WORKERS"
      });
    }
    
    // Create a requestId for tracking
    const requestId = `req_${Date.now()}_${Math.floor(Math.random() * 1000000)}`;
    
    // Initialize transaction tracking
    activeTransactions.set(requestId, {
      status: 'queued',
      address: user,
      tokenId: tokenId,
      workerId: worker.id % numWorkers,
      createdAt: Date.now()
    });
    
    // Send task to worker
    worker.send({
      type: 'mint_request',
      data: { user, tokenId, sigR, sigS, sigV },
      requestId
    });
    
    // Return success immediately - fully async
    res.status(202).json({
      success: true,
      message: "Transaction submitted for processing",
      requestId
    });
  });
  
  // Start API server
  const PORT = process.env.PORT || 3000;
  app.listen(PORT, () => {
    console.log(`Parallel Monad Relayer API server running on http://localhost:${PORT}`);
    console.log(`Worker count: ${numWorkers}`);
  });

} else {
  // Worker processes handle blockchain transactions
  const workerId = parseInt(process.env.WORKER_ID || 0);
  console.log(`Worker ${process.pid} (ID: ${workerId}) started`);
  
  // Contract configuration
  const RELAYER_PRIVATE_KEY = process.env.RELAYER_PRIVATE_KEY;
  const RPC_URL = process.env.RPC_URL || "https://monad-testnet.g.alchemy.com/v2/nRKjRbGd1vlkIaIxAlLUhd0rbTnE1s69";
  const CONTRACT_ADDRESS = process.env.CONTRACT_ADDRESS || "0x0238C45aDE925d7822B94FFf6D35a9C414532996";
  
  // Verify environment variables
  if (!RELAYER_PRIVATE_KEY || !RPC_URL || !CONTRACT_ADDRESS) {
    console.error("Missing required environment variables!");
    process.exit(1);
  }
  
  // Create unique connections for each worker
  // This is critical for Monad's parallel execution
  const provider = new ethers.JsonRpcProvider(RPC_URL, undefined, {
    staticNetwork: true,
    batchStallTime: 5, // Lower for quicker execution
    pollingInterval: 750, // Slightly faster for worker processes
  });
  
  const relayerWallet = new ethers.Wallet(RELAYER_PRIVATE_KEY, provider);
  
  // Initialize nonce tracking for this worker
  let currentNonce = null;
  let nonceLastRefreshed = 0;
  
  // NFT contract ABI
  const gaslessNFTAbi = [
    {
      inputs: [
        { internalType: "address", name: "user", type: "address" },
        { internalType: "uint256", name: "tokenId", type: "uint256" },
        { internalType: "bytes32", name: "sigR", type: "bytes32" },
        { internalType: "bytes32", name: "sigS", type: "bytes32" },
        { internalType: "uint8", name: "sigV", type: "uint8" },
      ],
      name: "mintNFT",
      outputs: [],
      stateMutability: "nonpayable",
      type: "function",
    },
    {
      inputs: [{ internalType: "address", name: "user", type: "address" }],
      name: "hasMinted",
      outputs: [{ internalType: "bool", name: "", type: "bool" }],
      stateMutability: "view",
      type: "function",
    },
  ];
  
  // Create contract instance
  const gaslessNFTContract = new ethers.Contract(
    CONTRACT_ADDRESS,
    gaslessNFTAbi,
    relayerWallet
  );
  
  // Worker status tracking
  let workerMetrics = {
    pendingTasks: 0,
    completedTasks: 0,
    failedTasks: 0,
    transactions: {} // Track individual transactions
  };
  
  // Update master with status
  function updateStatus() {
    process.send({
      type: 'worker_status',
      workerId,
      status: {
        pendingTasks: workerMetrics.pendingTasks,
        completedTasks: workerMetrics.completedTasks,
        failedTasks: workerMetrics.failedTasks,
        status: 'active',
        transactions: Object.keys(workerMetrics.transactions).length
      }
    });
  }
  
  // Update transaction status in master process
  function updateTransactionStatus(requestId, status, data = {}) {
    if (workerMetrics.transactions[requestId]) {
      // Update local worker state
      workerMetrics.transactions[requestId] = {
        ...workerMetrics.transactions[requestId],
        ...data,
        status
      };
      
      // Update master
      process.send({
        type: 'tx_update',
        requestId,
        data: {
          ...data,
          status
        }
      });
    }
  }
  
  // Refresh nonce
  async function refreshNonce() {
    try {
      const newNonce = await relayerWallet.getNonce();
      if (newNonce !== currentNonce) {
        console.log(`[Worker ${workerId}] Nonce updated: ${currentNonce} -> ${newNonce}`);
        currentNonce = newNonce;
      }
      nonceLastRefreshed = Date.now();
      return currentNonce;
    } catch (error) {
      console.error(`[Worker ${workerId}] Error refreshing nonce:`, error.message);
      throw error;
    }
  }
  
  // Check if a user has minted
  async function checkHasMinted(address) {
    try {
      return await gaslessNFTContract.hasMinted(address);
    } catch (error) {
      console.warn(`[Worker ${workerId}] Error checking if ${address} has minted:`, error.message);
      return false; // Assume not minted, contract will reject if already minted
    }
  }
  
  // Process mint request
  async function processMintRequest(requestData, requestId) {
    const { user, tokenId, sigR, sigS, sigV } = requestData;
    const startTime = Date.now();
    
    // Update worker metrics
    workerMetrics.pendingTasks++;
    workerMetrics.transactions[requestId] = {
      user,
      tokenId,
      startTime,
      status: 'processing'
    };
    updateStatus();
    
    // Update transaction status in master
    updateTransactionStatus(requestId, 'processing', { user, tokenId });
    
    try {
      // Double-check if user has already minted (ask the master process)
      const hasMintedPromise = new Promise((resolve, reject) => {
        const timeout = setTimeout(() => resolve(false), 1000); // Default to false if no response
        
        function messageHandler(message) {
          if (message.type === 'minted_response' && message.requestId === requestId) {
            clearTimeout(timeout);
            process.removeListener('message', messageHandler);
            resolve(message.hasMinted);
          }
        }
        
        process.on('message', messageHandler);
        process.send({ type: 'check_minted', address: user, requestId, workerId });
      });
      
      const hasMinted = await hasMintedPromise;
      if (hasMinted) {
        console.log(`[Worker ${workerId}] User ${user} has already minted`);
        
        // Update metrics
        workerMetrics.pendingTasks--;
        workerMetrics.failedTasks++;
        workerMetrics.transactions[requestId].status = 'already_minted';
        updateStatus();
        
        // Update transaction status
        updateTransactionStatus(requestId, 'already_minted', { 
          error: 'User has already minted an NFT' 
        });
        
        // Notify master
        process.send({
          type: 'mint_failure',
          address: user,
          error: 'ALREADY_MINTED',
          workerId,
          requestId
        });
        
        return;
      }
      
      // Double-check with blockchain (in case master's cache is outdated)
      const onChainHasMinted = await checkHasMinted(user);
      if (onChainHasMinted) {
        console.log(`[Worker ${workerId}] On-chain check: User ${user} has already minted`);
        
        // Update metrics
        workerMetrics.pendingTasks--;
        workerMetrics.failedTasks++;
        workerMetrics.transactions[requestId].status = 'already_minted';
        updateStatus();
        
        // Update transaction status
        updateTransactionStatus(requestId, 'already_minted', { 
          error: 'User has already minted an NFT (blockchain check)' 
        });
        
        // Notify master so it can update its cache
        process.send({
          type: 'mint_success', // Mark as success to update the master's cache
          address: user,
          workerId,
          requestId
        });
        
        return;
      }
      
      // Get fresh nonce if needed
      if (currentNonce === null || Date.now() - nonceLastRefreshed > 2000) {
        await refreshNonce();
      }
      
      console.log(`[Worker ${workerId}] Submitting transaction for ${user} with nonce ${currentNonce}`);
      
      // Submit transaction
      const tx = await gaslessNFTContract.mintNFT(
        user,
        tokenId,
        sigR,
        sigS,
        sigV,
        {
          gasLimit: 150000,
          // Critical: Incrementing nonce each time ensures parallel execution in Monad
          nonce: currentNonce++
        }
      );
      
      // Update transaction tracking
      workerMetrics.transactions[requestId].txHash = tx.hash;
      workerMetrics.transactions[requestId].status = 'submitted';
      updateStatus();
      
      // Update transaction status
      updateTransactionStatus(requestId, 'submitted', { 
        txHash: tx.hash 
      });
      
      console.log(`[Worker ${workerId}] Transaction submitted: ${tx.hash}`);
      
      // Wait for transaction with timeout
      const receipt = await Promise.race([
        tx.wait(1), // Wait for 1 confirmation
        new Promise((_, reject) => setTimeout(() => reject(new Error("Transaction receipt timeout")), 30000))
      ]);
      
      // Transaction confirmed
      const duration = Date.now() - startTime;
      console.log(`[Worker ${workerId}] Transaction confirmed: ${tx.hash} (${duration}ms)`);
      
      // Update metrics
      workerMetrics.pendingTasks--;
      workerMetrics.completedTasks++;
      workerMetrics.transactions[requestId].status = 'confirmed';
      workerMetrics.transactions[requestId].blockNumber = receipt.blockNumber;
      updateStatus();
      
      // Update transaction status
      updateTransactionStatus(requestId, 'confirmed', { 
        txHash: tx.hash,
        blockNumber: receipt.blockNumber,
        duration 
      });
      
      // Notify master process
      process.send({
        type: 'mint_success',
        address: user,
        txHash: tx.hash,
        blockNumber: receipt.blockNumber,
        workerId,
        requestId,
        duration
      });
      
      // Clean up transaction tracking after a while
      setTimeout(() => {
        delete workerMetrics.transactions[requestId];
      }, 60000);
      
    } catch (error) {
      console.error(`[Worker ${workerId}] Error processing mint request:`, error);
      
      // Handle nonce issues
      if (error.message.includes('nonce') || error.code === 'NONCE_EXPIRED') {
        console.log(`[Worker ${workerId}] Nonce issue detected, refreshing...`);
        try {
          await refreshNonce();
        } catch (nonceError) {
          console.error(`[Worker ${workerId}] Failed to refresh nonce:`, nonceError);
        }
      }
      
      // Update metrics
      workerMetrics.pendingTasks--;
      workerMetrics.failedTasks++;
      workerMetrics.transactions[requestId].status = 'failed';
      workerMetrics.transactions[requestId].error = error.message;
      updateStatus();
      
      // Update transaction status
      updateTransactionStatus(requestId, 'failed', { 
        error: error.message,
        errorCode: error.code
      });
      
      // Notify master process
      process.send({
        type: 'mint_failure',
        address: user,
        error: error.message,
        errorCode: error.code,
        workerId,
        requestId
      });
    }
  }
  
  // Initialize worker
  async function initWorker() {
    try {
      // Initialize nonce
      await refreshNonce();
      console.log(`[Worker ${workerId}] Initialized with address ${relayerWallet.address} and nonce ${currentNonce}`);
      
      // Send ready message
      process.send({
        type: 'worker_status',
        workerId,
        status: {
          status: 'ready',
          address: relayerWallet.address,
          nonce: currentNonce
        }
      });
      
      // Set up health check interval
      setInterval(() => {
        process.send({
          type: 'check_health',
          workerId
        });
      }, 5000);
      
    } catch (error) {
      console.error(`[Worker ${workerId}] Initialization error:`, error);
      process.exit(1);
    }
  }
  
  // Process messages from master
  process.on('message', (message) => {
    if (message.type === 'mint_request') {
      processMintRequest(message.data, message.requestId);
    } else if (message.type === 'check_minted') {
      // Check if user has minted directly with contract
      checkHasMinted(message.address).then(hasMinted => {
        process.send({
          type: 'minted_response',
          requestId: message.requestId,
          hasMinted
        });
      });
    } else if (message.type === 'tx_status_query') {
      // New handler for transaction status queries
      const requestId = message.requestId;
      
      // Check if we have this transaction
      if (workerMetrics.transactions[requestId]) {
        const txData = workerMetrics.transactions[requestId];
        
        // Send the transaction data back to the master
        process.send({
          type: 'tx_status_response',
          requestId,
          found: true,
          data: {
            status: txData.status || 'unknown',
            txHash: txData.txHash || null,
            blockNumber: txData.blockNumber || null,
            error: txData.error || null,
            user: txData.user,
            tokenId: txData.tokenId,
            duration: txData.duration
          }
        });
      } else {
        // We don't have this transaction
        process.send({
          type: 'tx_status_response',
          requestId,
          found: false
        });
      }
    }
  });
  
  // Initialize worker
  initWorker().catch(error => {
    console.error(`[Worker ${workerId}] Fatal error during initialization:`, error);
    process.exit(1);
  });
}