'use strict';

const {
  PublicKey,
  SystemProgram,
  TransactionMessage,
  VersionedTransaction,
} = require('@solana/web3.js');
// bs58 v6 under CommonJS exports only { default } — see lib/wallet.js.
const bs58 = require('bs58').default;

/**
 * Jito Block Engine client — OPT-IN, OFF BY DEFAULT.
 *
 * NFR-15: when ENABLE_JITO_BUNDLES is false (the default), execution behavior
 * must remain provably identical to pre-Jito behavior. Nothing in this file is
 * reached on that path — executor.js does not call into it at all.
 *
 * COST MODEL — state this plainly wherever it appears; do not soften it:
 *   The tip is paid on EVERY ATTEMPT, win or lose. A round-trip trade is two
 *   legs, so it pays the tip TWICE. This is a recurring per-attempt cost, not a
 *   one-time setup fee.
 */

const DEFAULT_BLOCK_ENGINE = 'https://mainnet.block-engine.jito.wtf/api/v1/bundles';

function createJitoClient(env = process.env, { fetchImpl = globalThis.fetch } = {}) {
  const endpoint = (env.JITO_BLOCK_ENGINE_URL || DEFAULT_BLOCK_ENGINE).trim();
  const timeoutMs = Number(env.JITO_TIMEOUT_MS || 10000);

  async function rpc(method, params) {
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), timeoutMs);
    try {
      const res = await fetchImpl(endpoint, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
        body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }),
        signal: controller.signal,
      });
      const text = await res.text();
      if (!res.ok) {
        throw new Error(`Jito ${method} responded ${res.status}: ${text.slice(0, 300)}`);
      }
      const json = JSON.parse(text);
      if (json.error) {
        throw new Error(`Jito ${method} error: ${JSON.stringify(json.error).slice(0, 300)}`);
      }
      return json.result;
    } catch (err) {
      if (err.name === 'AbortError') throw new Error(`Jito ${method} timed out after ${timeoutMs}ms`);
      throw err;
    } finally {
      clearTimeout(timer);
    }
  }

  /**
   * NFR-13: tip accounts are fetched LIVE on every bundle submission — never
   * hardcoded, never cached across attempts.
   *
   * This is Jito's own documented recommendation: picking randomly from the
   * live set reduces contention with other searchers tipping the same account.
   * It also avoids the real risk of a stale or wrong hardcoded payment address —
   * the exact class of mistake this codebase has already had one near-miss with.
   */
  async function getTipAccounts() {
    const accounts = await rpc('getTipAccounts', []);
    if (!Array.isArray(accounts) || accounts.length === 0) {
      throw new Error('Jito getTipAccounts returned no accounts');
    }
    return accounts;
  }

  /** Picks a random live tip account. Called fresh per attempt — never memoized. */
  async function pickTipAccount() {
    const accounts = await getTipAccounts();
    return accounts[Math.floor(Math.random() * accounts.length)];
  }

  /**
   * Builds the tip transaction as a SEPARATE, minimal SOL transfer.
   *
   * NFR-14: the Jupiter-built swap transaction is NEVER modified to add the tip.
   * The tip is always its own transaction in the same bundle. Injecting it into
   * Jupiter's transaction risks corrupting a construction we do not own.
   */
  function buildTipTransaction({ payer, tipAccount, tipLamports, blockhash }) {
    const instruction = SystemProgram.transfer({
      fromPubkey: payer.publicKey,
      toPubkey: new PublicKey(tipAccount),
      lamports: tipLamports,
    });
    const message = new TransactionMessage({
      payerKey: payer.publicKey,
      recentBlockhash: blockhash,
      instructions: [instruction],
    }).compileToV0Message();
    const tx = new VersionedTransaction(message);
    tx.sign([payer]);
    return tx;
  }

  /**
   * Submits a bundle. `transactions` are signed VersionedTransactions, in order.
   * The swap transaction comes first, the tip transaction second.
   */
  async function sendJitoBundle(transactions) {
    const encoded = transactions.map((tx) => bs58.encode(tx.serialize()));
    return rpc('sendBundle', [encoded]);
  }

  async function getBundleStatus(bundleId) {
    const result = await rpc('getBundleStatuses', [[bundleId]]);
    return result?.value?.[0] ?? null;
  }

  return {
    getTipAccounts,
    pickTipAccount,
    buildTipTransaction,
    sendJitoBundle,
    getBundleStatus,
    endpoint,
  };
}

module.exports = { createJitoClient, DEFAULT_BLOCK_ENGINE };
