/**
 * ═══════════════════════════════════════════════════════════════════════════════
 * LOOPUMAN - AUTO-DEPOSIT BLOCKCHAIN LISTENER
 * Fixed: 9 February 2026
 * ═══════════════════════════════════════════════════════════════════════════════
 *
 * Watches the Celo blockchain for incoming stablecoin transfers to the
 * treasury wallet. Auto-credits users when:
 *   1. Transfer detected to treasury
 *   2. TX hash not already recorded in crypto_deposits table
 *   3. Sender address matches a registered user's wallet_address
 *
 * FIXES APPLIED:
 *  - Bug 10: Added node-fetch fallback for Node < 18
 *  - Bug 11: increment_balance RPC uses field-based signature matching disputes.js
 *  - Bug 15: Block persistence via Supabase (survives restarts)
 *  - Bug 16: Uses ethers.formatUnits() instead of Number() for precision
 *  - Bug 21: Uses ADMIN_BOT_TOKEN (matches .env) not TELEGRAM_ADMIN_BOT_TOKEN
 *
 * STABLECOINS ONLY: USDC, USDT, cUSD on Celo mainnet.
 * NEVER accepts native CELO token.
 *
 * ═══════════════════════════════════════════════════════════════════════════════
 */

'use strict';
const { ethers } = require('ethers');
const { createClient } = require('@supabase/supabase-js');

// Node < 18 fetch polyfill
const fetchFn = typeof globalThis.fetch === 'function'
  ? globalThis.fetch
  : (() => { try { return require('node-fetch'); } catch { return null; } })();

const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_SERVICE_KEY);

// ─── Config ──────────────────────────────────────────────────────────────────

const CELO_RPC = process.env.CELO_RPC_URL || 'https://forno.celo.org';
const TREASURY = (process.env.TREASURY_ADDRESS || '').toLowerCase();
const MIN_DEPOSIT_USD = 6;
const VAE_RATE = 100; // 100 VAE = $1

// Celo mainnet stablecoin contracts (ONLY these — no native CELO)
const TOKENS = {
  USDC: {
    address: '0xcebA9300f2b948710d2653dD7B07f33A8B32118C',
    decimals: 6,
    name: 'USDC',
  },
  USDT: {
    address: '0x48065fbBE25f71C9282ddf5e1cD6D6A887483D5e',
    decimals: 6,
    name: 'USDT',
  },
  cUSD: {
    address: '0x765DE816845861e75A25fCA122bb6898B8B1282a',
    decimals: 18,
    name: 'cUSD',
  },
};

// Reverse lookup: contract address → token symbol
const ADDRESS_TO_TOKEN = {};
for (const [symbol, config] of Object.entries(TOKENS)) {
  ADDRESS_TO_TOKEN[config.address.toLowerCase()] = { symbol, ...config };
}

// ERC-20 Transfer event signature
const TRANSFER_TOPIC = ethers.id('Transfer(address,address,uint256)');

// Treasury address padded to 32 bytes for topic matching
const TREASURY_TOPIC = '0x' + TREASURY.slice(2).padStart(64, '0');

// ─── State ───────────────────────────────────────────────────────────────────

let provider = null;
let lastProcessedBlock = 0;
let isRunning = false;

// ─── Initialise ──────────────────────────────────────────────────────────────

function init() {
  if (!TREASURY) {
    console.error('[DepositListener] ❌ TREASURY_ADDRESS not set');
    return false;
  }
  if (!fetchFn) {
    console.error('[DepositListener] ❌ fetch not available. Install node-fetch: npm i node-fetch');
    return false;
  }
  provider = new ethers.JsonRpcProvider(CELO_RPC);
  console.log(`[DepositListener] ✅ Watching treasury: ${TREASURY}`);
  console.log(`[DepositListener] ✅ Tokens: USDC, USDT, cUSD (Celo mainnet only)`);
  return true;
}

// ─── Block persistence ───────────────────────────────────────────────────────

/**
 * Load last processed block from Supabase so we survive restarts.
 * Falls back to in-memory value if table doesn't exist.
 */
async function loadLastBlock() {
  try {
    const { data } = await supabase
      .from('system_state')
      .select('value')
      .eq('key', 'deposit_listener_last_block')
      .single();
    if (data?.value) {
      lastProcessedBlock = parseInt(data.value, 10);
      console.log(`[DepositListener] Resumed from block ${lastProcessedBlock}`);
    }
  } catch {
    // Table may not exist — that's fine, use in-memory
  }
}

async function saveLastBlock(blockNumber) {
  try {
    await supabase.from('system_state').upsert({
      key: 'deposit_listener_last_block',
      value: String(blockNumber),
      updated_at: new Date().toISOString()
    }, { onConflict: 'key' });
  } catch {
    // Non-critical — worst case we re-scan some blocks (dedup prevents double-credit)
  }
}

// ─── Core: Scan recent blocks for deposits ───────────────────────────────────

/**
 * Scan the last N blocks for stablecoin transfers to treasury.
 * Called on interval by scheduler (every 2 minutes).
 *
 * @param {number} lookbackBlocks — how many blocks to scan (default 60 ≈ 5 min)
 */
async function scanForDeposits(lookbackBlocks = 60) {
  if (isRunning) {
    console.log('[DepositListener] Already scanning, skipping');
    return;
  }
  isRunning = true;

  try {
    if (!provider) {
      if (!init()) return;
    }

    // Load persisted block on first run
    if (lastProcessedBlock === 0) {
      await loadLastBlock();
    }

    const currentBlock = await provider.getBlockNumber();
    const fromBlock = lastProcessedBlock > 0
      ? lastProcessedBlock + 1
      : currentBlock - lookbackBlocks;

    if (fromBlock > currentBlock) {
      isRunning = false;
      return; // Nothing new
    }

    const MAX_BLOCK_RANGE = 500;
    const toBlock = Math.min(currentBlock, fromBlock + MAX_BLOCK_RANGE);
    console.log(`[DepositListener] Scanning blocks ${fromBlock}-${toBlock} (head: ${currentBlock})`);

    // Build filter: Transfer events TO treasury across all 3 token contracts
    const tokenAddresses = Object.values(TOKENS).map(t => t.address);

    const logs = await provider.getLogs({
      fromBlock,
      toBlock,
      address: tokenAddresses,
      topics: [
        TRANSFER_TOPIC,       // event Transfer(address,address,uint256)
        null,                 // from: any sender
        TREASURY_TOPIC,       // to: our treasury
      ],
    });

    if (logs.length > 0) {
      console.log(`[DepositListener] Found ${logs.length} transfer(s) to treasury`);
    }

    for (const log of logs) {
      await processTransferLog(log);
    }

    lastProcessedBlock = toBlock;
    await saveLastBlock(currentBlock);
  } catch (error) {
    console.error('[DepositListener] Scan error:', error.message);
    // Reset provider on RPC errors so it reconnects next time
    if (error.message.includes('network') || error.message.includes('timeout')) {
      provider = null;
    }
  } finally {
    isRunning = false;
  }
}

// ─── Process a single Transfer log ───────────────────────────────────────────

async function processTransferLog(log) {
  try {
    const txHash = log.transactionHash.toLowerCase();

    // 1. Already recorded?
    const { data: existing } = await supabase
      .from('crypto_deposits')
      .select('id')
      .eq('tx_hash', txHash)
      .single();

    if (existing) return; // Already processed

    // 2. Identify token
    const contractAddr = log.address.toLowerCase();
    const token = ADDRESS_TO_TOKEN[contractAddr];
    if (!token) return; // Unknown token — ignore

    // 3. Decode amount (precision-safe using ethers.formatUnits)
    const rawAmount = ethers.toBigInt(log.data);
    const amountUsd = parseFloat(ethers.formatUnits(rawAmount, token.decimals));

    // 4. Get sender
    const senderAddress = '0x' + log.topics[1].slice(26).toLowerCase();

    console.log(`[DepositListener] Detected: ${amountUsd.toFixed(2)} ${token.symbol} from ${senderAddress}`);

    // 5. Below minimum? Still record but don't credit
    if (amountUsd < MIN_DEPOSIT_USD) {
      console.log(`[DepositListener] Below minimum ($${MIN_DEPOSIT_USD}), recording as unmatched`);
      await recordUnmatchedDeposit(txHash, token.symbol, amountUsd, senderAddress, 'below_minimum');
      return;
    }

    // 6. Find user by wallet address
    const user = await findUserByWallet(senderAddress);

    if (user) {
      await creditUser(user, txHash, token.symbol, amountUsd, senderAddress);
    } else {
      console.log(`[DepositListener] ⚠️ Unmatched deposit: no user with wallet ${senderAddress}`);
      await recordUnmatchedDeposit(txHash, token.symbol, amountUsd, senderAddress, 'no_matching_user');
      await alertAdmin(txHash, token.symbol, amountUsd, senderAddress);
    }
  } catch (error) {
    console.error(`[DepositListener] Process error for ${log.transactionHash}:`, error.message);
  }
}

// ─── Find user by their registered wallet address ────────────────────────────

async function findUserByWallet(walletAddress) {
  const { data } = await supabase
    .from('app_users')
    .select('id, telegram_id, first_name, wallet_address')
    .ilike('wallet_address', walletAddress)
    .single();

  return data || null;
}

// ─── Credit user account ─────────────────────────────────────────────────────

async function creditUser(user, txHash, tokenSymbol, amountUsd, senderAddress) {
  // Apply 20% platform fee (same as Stripe)
  // FIXED: No deposit-time fee (commission taken at task creation)
  const amountVae = Math.floor((amountUsd / 1.20) * VAE_RATE); // 20% platform fee same as Stripe

  // Atomic balance increment via RPC (matches disputes.js increment_balance signature)
  const { error: balError } = await supabase.rpc('increment_balance', {
    p_user_id: user.id,
    p_balance_type: 'available_only',
    p_amount: amountVae,
  });

  if (balError) {
    console.error(`[DepositListener] Balance increment failed for ${user.id}:`, balError);
    await recordUnmatchedDeposit(txHash, tokenSymbol, amountUsd, senderAddress, 'balance_increment_failed');
    return;
  }

  // Record in crypto_deposits (prevents double-processing)
  const { error: insertErr } = await supabase.from('crypto_deposits').insert({
    user_id: user.id,
    tx_hash: txHash,
    token: tokenSymbol,
    amount_usd: amountUsd,
    bonus_usd: 0,
    amount_vae: amountVae,
    status: 'completed',
    sender_address: senderAddress,
    verified_at: new Date().toISOString(),
    source: 'auto_listener',
  });

  if (insertErr) {
    console.error(`[DepositListener] crypto_deposits insert failed:`, insertErr.message);
    // Don't return — balance already credited, just log
  }

  // Record transaction
  await supabase.from('transactions').insert({
    user_id: user.id,
    type: 'crypto_deposit',
    amount: amountVae,
    description: `Auto-detected ${tokenSymbol} deposit`,
    metadata: { tx_hash: txHash, source: 'auto_listener' },
  });

  console.log(`[DepositListener] ✅ Auto-credited ${user.first_name || user.id}: ${amountVae} VAE ($${amountUsd.toFixed(2)} ${tokenSymbol})`);

  // Notify user via Telegram
  await notifyUser(user.telegram_id, amountVae, amountUsd, tokenSymbol, txHash);
}

// ─── Notify user about auto-credited deposit ─────────────────────────────────

async function notifyUser(telegramId, amountVae, amountUsd, token, txHash) {
  if (!telegramId || !fetchFn) return;

  try {
    const botToken = process.env.TELEGRAM_BOT_TOKEN;
    if (!botToken) return;

    const message =
      `✅ <b>Deposit Auto-Detected!</b>\n\n` +
      `We detected your ${token} transfer and credited your account automatically.\n\n` +
      `💰 <b>+${amountVae} VAE ($${amountUsd.toFixed(2)})</b>\n` +
      `🪙 Token: ${token}\n` +
      `🔗 <a href="https://celoscan.io/tx/${txHash}">View on CeloScan</a>`;

    await fetchFn(`https://api.telegram.org/bot${botToken}/sendMessage`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        chat_id: telegramId,
        text: message,
        parse_mode: 'HTML',
        disable_web_page_preview: true,
      }),
    });
  } catch (err) {
    console.error('[DepositListener] Notification error:', err.message);
  }
}

// ─── Record unmatched deposit for admin review ───────────────────────────────

async function recordUnmatchedDeposit(txHash, token, amountUsd, senderAddress, reason) {
  try {
    await supabase.from('unmatched_deposits').insert({
      tx_hash: txHash,
      token,
      amount_usd: amountUsd,
      sender_address: senderAddress,
      reason,
      status: 'pending',
      detected_at: new Date().toISOString(),
    });
  } catch (err) {
    // Table might not exist yet — log but don't crash
    console.error('[DepositListener] Unmatched deposit record error:', err.message);
  }
}

// ─── Alert admin about unmatched deposit ─────────────────────────────────────

async function alertAdmin(txHash, token, amountUsd, senderAddress) {
  try {
    // Record system alert (non-critical if table missing)
    try {
      await supabase.from('system_alerts').insert({
        service: 'deposit_listener',
        level: 'warning',
        message: `Unmatched deposit: $${amountUsd.toFixed(2)} ${token} from ${senderAddress}`,
        metadata: { tx_hash: txHash, token, amount_usd: amountUsd, sender: senderAddress },
      });
    } catch {
      // system_alerts table may not exist — non-critical
    }

    // Bug 21 fix: use ADMIN_BOT_TOKEN (matches .env) not TELEGRAM_ADMIN_BOT_TOKEN
    const adminBotToken = process.env.ADMIN_BOT_TOKEN || process.env.TELEGRAM_ADMIN_BOT_TOKEN;
    const adminChatId = process.env.ADMIN_CHAT_ID;
    if (!adminBotToken || !adminChatId || !fetchFn) return;

    const message =
      `⚠️ <b>Unmatched Crypto Deposit</b>\n\n` +
      `$${amountUsd.toFixed(2)} ${token} received but cannot identify sender.\n\n` +
      `Sender: <code>${senderAddress}</code>\n` +
      `TX: <a href="https://celoscan.io/tx/${txHash}">CeloScan</a>\n\n` +
      `Check /unmatched to resolve.`;

    await fetchFn(`https://api.telegram.org/bot${adminBotToken}/sendMessage`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        chat_id: adminChatId,
        text: message,
        parse_mode: 'HTML',
        disable_web_page_preview: true,
      }),
    });
  } catch (err) {
    console.error('[DepositListener] Admin alert error:', err.message);
  }
}

// ─── Manual resolution: credit unmatched deposit to a user ───────────────────

async function resolveUnmatchedDeposit(unmatchedId, userId) {
  const { data: deposit } = await supabase
    .from('unmatched_deposits')
    .select('*')
    .eq('id', unmatchedId)
    .eq('status', 'pending')
    .single();

  if (!deposit) {
    return { success: false, error: 'Deposit not found or already resolved' };
  }

  const { data: user } = await supabase
    .from('app_users')
    .select('id, telegram_id, first_name')
    .eq('id', userId)
    .single();

  if (!user) {
    return { success: false, error: 'User not found' };
  }

  // Credit user
  await creditUser(user, deposit.tx_hash, deposit.token, deposit.amount_usd, deposit.sender_address);

  // Mark as resolved
  await supabase
    .from('unmatched_deposits')
    .update({
      status: 'resolved',
      resolved_user_id: userId,
      resolved_at: new Date().toISOString(),
    })
    .eq('id', unmatchedId);

  return { success: true, amountVae: Math.floor(deposit.amount_usd * VAE_RATE) };
}

// ─── Exports ─────────────────────────────────────────────────────────────────

module.exports = {
  init,
  scanForDeposits,
  resolveUnmatchedDeposit,
  TOKENS,
};
