const crypto = require('crypto');

const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
const BASE58_MAP = new Map(BASE58_ALPHABET.split('').map((char, index) => [char, BigInt(index)]));

const TRON_CONFIG = {
  apiBase: (process.env.PLAI_TRON_API_BASE || 'https://api.trongrid.io').replace(/\/+$/, ''),
  apiKey: process.env.PLAI_TRON_API_KEY || '',
  requestTimeoutMs: Number(process.env.PLAI_TRON_REQUEST_TIMEOUT_MS || 10000),
  transferScanLimit: Number(process.env.PLAI_TRON_TRANSFER_SCAN_LIMIT || 200),
  transferScanPages: Number(process.env.PLAI_TRON_TRANSFER_SCAN_PAGES || 5)
};

function tronError(message, status, code, details = {}) {
  const err = new Error(message);
  err.status = status;
  err.code = code;
  err.details = details;
  return err;
}

function sha256(buffer) {
  return crypto.createHash('sha256').update(buffer).digest();
}

function decodeBase58(value) {
  const text = String(value || '').trim();
  if (!text) throw tronError('TRON address is required.', 400, 'TRON_ADDRESS_REQUIRED');
  let number = 0n;
  for (const char of text) {
    if (!BASE58_MAP.has(char)) throw tronError('Invalid TRON base58 address.', 400, 'INVALID_TRON_ADDRESS');
    number = number * 58n + BASE58_MAP.get(char);
  }
  let hex = number.toString(16);
  if (hex.length % 2) hex = `0${hex}`;
  let bytes = Buffer.from(hex, 'hex');
  let leadingZeroes = 0;
  for (const char of text) {
    if (char !== '1') break;
    leadingZeroes += 1;
  }
  if (leadingZeroes) bytes = Buffer.concat([Buffer.alloc(leadingZeroes), bytes]);
  return bytes;
}

function base58ToHexAddress(address) {
  const bytes = decodeBase58(address);
  if (bytes.length !== 25) throw tronError('Invalid TRON address length.', 400, 'INVALID_TRON_ADDRESS');
  const body = bytes.subarray(0, 21);
  const checksum = bytes.subarray(21);
  const expected = sha256(sha256(body)).subarray(0, 4);
  if (!checksum.equals(expected)) throw tronError('Invalid TRON address checksum.', 400, 'INVALID_TRON_ADDRESS');
  return body.toString('hex').toUpperCase();
}

async function tronPost(pathname, body = {}) {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), TRON_CONFIG.requestTimeoutMs);
  try {
    const response = await fetch(`${TRON_CONFIG.apiBase}${pathname}`, {
      method: 'POST',
      headers: {
        'content-type': 'application/json',
        ...(TRON_CONFIG.apiKey ? { 'TRON-PRO-API-KEY': TRON_CONFIG.apiKey } : {})
      },
      body: JSON.stringify(body),
      signal: controller.signal
    });
    const data = await response.json().catch(() => ({}));
    if (!response.ok) {
      throw tronError('TRON API request failed.', 502, 'TRON_API_FAILED', { status: response.status, data });
    }
    return data;
  } catch (error) {
    if (error.name === 'AbortError') throw tronError('TRON API request timed out.', 504, 'TRON_API_TIMEOUT');
    if (error.code) throw error;
    throw tronError(error.message || 'TRON API request failed.', 502, 'TRON_API_FAILED');
  } finally {
    clearTimeout(timer);
  }
}

async function tronGet(pathname) {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), TRON_CONFIG.requestTimeoutMs);
  try {
    const response = await fetch(`${TRON_CONFIG.apiBase}${pathname}`, {
      method: 'GET',
      headers: {
        ...(TRON_CONFIG.apiKey ? { 'TRON-PRO-API-KEY': TRON_CONFIG.apiKey } : {})
      },
      signal: controller.signal
    });
    const data = await response.json().catch(() => ({}));
    if (!response.ok) {
      throw tronError('TRON API request failed.', 502, 'TRON_API_FAILED', { status: response.status, data });
    }
    return data;
  } catch (error) {
    if (error.name === 'AbortError') throw tronError('TRON API request timed out.', 504, 'TRON_API_TIMEOUT');
    if (error.code) throw error;
    throw tronError(error.message || 'TRON API request failed.', 502, 'TRON_API_FAILED');
  } finally {
    clearTimeout(timer);
  }
}

async function getNowBlockNumber() {
  const block = await tronPost('/wallet/getnowblock', {});
  return Number(block && block.block_header && block.block_header.raw_data && block.block_header.raw_data.number) || 0;
}

async function getTransaction(txHash) {
  const safeTxHash = String(txHash || '').trim();
  if (!safeTxHash) throw tronError('TRON TX hash is required.', 400, 'TRX_TX_HASH_REQUIRED');
  const transaction = await tronPost('/wallet/gettransactionbyid', { value: safeTxHash });
  if (!transaction || !transaction.txID) throw tronError('TRON transaction was not found.', 404, 'TRX_TX_NOT_FOUND');
  const info = await tronPost('/wallet/gettransactioninfobyid', { value: safeTxHash });
  return { transaction, info };
}

function readTransferContract(transaction) {
  const contract = transaction
    && transaction.raw_data
    && Array.isArray(transaction.raw_data.contract)
    && transaction.raw_data.contract[0];
  if (!contract || contract.type !== 'TransferContract') {
    throw tronError('TRON transaction is not a native TRX transfer.', 400, 'TRX_TRANSFER_REQUIRED');
  }
  const value = contract.parameter && contract.parameter.value;
  if (!value) throw tronError('TRON transfer data is missing.', 400, 'TRX_TRANSFER_REQUIRED');
  return value;
}

function isSameTrxAmount(left, right) {
  const a = Math.round(Number(left || 0) * 1_000_000) / 1_000_000;
  const b = Math.round(Number(right || 0) * 1_000_000) / 1_000_000;
  return a > 0 && b > 0 && Math.abs(a - b) <= 0.000001;
}

async function verifyTrxTransfer({ txHash, fromAddress, toAddress, minAmount = 0 } = {}) {
  const expectedFromHex = base58ToHexAddress(fromAddress);
  const expectedToHex = base58ToHexAddress(toAddress);
  const { transaction, info } = await getTransaction(txHash);
  const transfer = readTransferContract(transaction);
  const amountSun = Number(transfer.amount || 0);
  const amountTrx = Math.round((amountSun / 1_000_000) * 1_000_000) / 1_000_000;
  const actualFromHex = String(transfer.owner_address || '').toUpperCase();
  const actualToHex = String(transfer.to_address || '').toUpperCase();

  if (actualFromHex !== expectedFromHex) {
    throw tronError('TRON transfer sender address does not match.', 400, 'TRX_SENDER_MISMATCH');
  }
  if (actualToHex !== expectedToHex) {
    throw tronError('TRON transfer target address does not match treasury.', 400, 'TRX_TARGET_MISMATCH');
  }
  const expectedAmount = Number(minAmount || 0);
  if (expectedAmount && !isSameTrxAmount(amountTrx, expectedAmount)) {
    throw tronError('TRON transfer amount does not match requested amount.', 400, 'TRX_AMOUNT_MISMATCH', {
      expectedAmount,
      actualAmount: amountTrx
    });
  }

  const receiptResult = info && info.receipt && info.receipt.result ? info.receipt.result : 'SUCCESS';
  if (receiptResult !== 'SUCCESS') {
    throw tronError('TRON transfer receipt is not successful.', 400, 'TRX_RECEIPT_FAILED', { receiptResult });
  }

  const blockNumber = Number(info && info.blockNumber) || 0;
  const nowBlockNumber = blockNumber ? await getNowBlockNumber() : 0;
  const confirmations = blockNumber && nowBlockNumber ? Math.max(0, nowBlockNumber - blockNumber + 1) : 0;

  return {
    ok: true,
    txHash: transaction.txID,
    fromAddress,
    toAddress,
    fromHex: actualFromHex,
    toHex: actualToHex,
    amountSun,
    amountTrx,
    blockNumber,
    nowBlockNumber,
    confirmations,
    receiptResult
  };
}

async function findIncomingTrxTransfer({ fromAddress, toAddress, minAmount = 0, ignoredTxHashes = [] } = {}) {
  const expectedFromHex = base58ToHexAddress(fromAddress);
  const expectedToHex = base58ToHexAddress(toAddress);
  const expectedAmount = Number(minAmount || 0);
  const ignored = new Set((ignoredTxHashes || []).map(value => String(value || '').trim()).filter(Boolean));
  const limit = Math.min(200, Math.max(1, Number(TRON_CONFIG.transferScanLimit || 200)));
  const maxPages = Math.min(10, Math.max(1, Number(TRON_CONFIG.transferScanPages || 5)));
  let pagePath = `/v1/accounts/${encodeURIComponent(toAddress)}/transactions?only_to=true&limit=${limit}&order_by=block_timestamp,desc`;

  for (let page = 0; page < maxPages && pagePath; page += 1) {
    const data = await tronGet(pagePath);
    const transactions = Array.isArray(data.data) ? data.data : [];
    for (const transaction of transactions) {
      const txHash = transaction.txID || transaction.tx_id || transaction.hash || '';
      if (!txHash || ignored.has(txHash)) continue;
      const contract = transaction.raw_data
        && Array.isArray(transaction.raw_data.contract)
        && transaction.raw_data.contract[0];
      const value = contract && contract.parameter && contract.parameter.value;
      if (!value || contract.type !== 'TransferContract') continue;
      const actualFromHex = String(value.owner_address || '').toUpperCase();
      const actualToHex = String(value.to_address || '').toUpperCase();
      const amountTrx = Math.round((Number(value.amount || 0) / 1_000_000) * 1_000_000) / 1_000_000;
      if (actualFromHex !== expectedFromHex) continue;
      if (actualToHex !== expectedToHex) continue;
      if (expectedAmount && !isSameTrxAmount(amountTrx, expectedAmount)) continue;
      return verifyTrxTransfer({
        txHash,
        fromAddress,
        toAddress,
        minAmount
      });
    }
    const next = data && data.meta && data.meta.links && data.meta.links.next ? String(data.meta.links.next) : '';
    pagePath = next
      ? next.replace(TRON_CONFIG.apiBase, '')
      : '';
  }

  throw tronError('No matching TRX deposit was found.', 404, 'TRX_DEPOSIT_NOT_FOUND');
}

module.exports = {
  TRON_CONFIG,
  base58ToHexAddress,
  verifyTrxTransfer,
  findIncomingTrxTransfer
};
