// onchain-tx.jsx — browser-side tx builders for the run_casino_escrow program
// (Phase 2 milestone 3a). Mirrors onchain/scripts/test-placebet.mjs but takes
// a `payer` adapter (publicKey + signAndSendTransaction) instead of a
// Keypair, so signing routes through Phantom / Solflare.
//
// Loaded from index.html as a babel script AFTER wallet.jsx but BEFORE the
// game files that consume it. Depends on `window.solanaWeb3` (the IIFE
// bundle of @solana/web3.js loaded just above the babel chain in index.html).
//
// The on-chain program lives on devnet at:
//   7iAjLkoHuqECHDY9TFHrCgC22tCAY6JUrrVT8V1cdEuA (§6.11, deployed §6.12)
// GlobalConfig PDA:
//   EYSWyGoKVGwE1YWJ4pTybwAzcPf1gjkNqrh8Jv7t1AYH (§6.12)
// All instruction layouts must match lib.rs byte-for-byte.
//
// Phase 2 ONLY runs in real mode with a connected wallet. Fun mode bypasses
// this entire module — the existing debit/credit path in wallet.jsx stays.

const ONCHAIN_RPC_URL_DEVNET = 'https://api.devnet.solana.com';
const ONCHAIN_PROGRAM_ID_STR = '7iAjLkoHuqECHDY9TFHrCgC22tCAY6JUrrVT8V1cdEuA';
const ONCHAIN_GAME_FLIP = 0;

// Anchor instruction discriminators — first 8 bytes of sha256("global:<name>").
// Stable for the lifetime of the program. Cross-reference: onchain/scripts/
// initialize.mjs prints "afaf6d1f0d989bed" for initialize; test-placebet.mjs
// prints "de3e43dc3fa67e21" for place_bet.
const ONCHAIN_DISC = {
  place_bet:               new Uint8Array([0xde, 0x3e, 0x43, 0xdc, 0x3f, 0xa6, 0x7e, 0x21]),
  settle:                  new Uint8Array([0xaf, 0x2a, 0xb9, 0x57, 0x90, 0x83, 0x66, 0xd4]),
  refund:                  new Uint8Array([0x02, 0x60, 0xb7, 0xfb, 0x3f, 0xd0, 0x2e, 0x2e]),
  withdraw_treasury:       new Uint8Array([0x28, 0x3f, 0x7a, 0x9e, 0x90, 0xd8, 0x53, 0x60]),
  set_authority:           new Uint8Array([0x85, 0xfa, 0x25, 0x15, 0x6e, 0xa3, 0x1a, 0x79]),
  delegate_session:        new Uint8Array([0x52, 0x53, 0x77, 0x77, 0xc4, 0xdb, 0x05, 0xc5]),
  revoke_session:          new Uint8Array([0x56, 0x5c, 0xc6, 0x78, 0x90, 0x02, 0x07, 0xc2]),
  cleanup_expired_session: new Uint8Array([0x06, 0x6c, 0xf3, 0x79, 0x23, 0xe1, 0x30, 0x65]),
  play_flip_session:       new Uint8Array([0x40, 0x52, 0xc4, 0xe8, 0xb0, 0xbb, 0x03, 0x5e]),
};

function onchainAssertSdk() {
  if (typeof window === 'undefined' || !window.solanaWeb3) {
    throw new Error('onchain-tx: @solana/web3.js IIFE not loaded — check index.html script order');
  }
  return window.solanaWeb3;
}

function onchainProgramId() {
  const { PublicKey } = onchainAssertSdk();
  return new PublicKey(ONCHAIN_PROGRAM_ID_STR);
}

// One Connection per page load. Returned by useConnection (below).
let _onchainConnection = null;
function onchainGetConnection() {
  const { Connection } = onchainAssertSdk();
  if (_onchainConnection) return _onchainConnection;
  _onchainConnection = new Connection(ONCHAIN_RPC_URL_DEVNET, 'confirmed');
  return _onchainConnection;
}

function onchainRandomRoundId() {
  const out = new Uint8Array(16);
  window.crypto.getRandomValues(out);
  return out;
}

function onchainBytesToHex(bytes) {
  return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
}

function onchainHexToBytes(hex) {
  if (typeof hex !== 'string' || !/^[0-9a-fA-F]{32}$/.test(hex)) {
    throw new Error('roundId must be 32 hex chars (16 bytes)');
  }
  const out = new Uint8Array(16);
  for (let i = 0; i < 16; i++) out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
  return out;
}

function onchainBase64ToBytes(b64) {
  const bin = atob(b64);
  const out = new Uint8Array(bin.length);
  for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
  return out;
}

// Little-endian u64 (Anchor's Borsh format for u64 args).
function onchainU64Le(n) {
  const v = (typeof n === 'bigint') ? n : BigInt(n);
  const out = new Uint8Array(8);
  let x = v;
  for (let i = 0; i < 8; i++) { out[i] = Number(x & 0xffn); x >>= 8n; }
  if (x !== 0n) throw new Error('u64 overflow: ' + v);
  return out;
}

function onchainConcat(...arrays) {
  let total = 0;
  for (const a of arrays) total += a.length;
  const out = new Uint8Array(total);
  let off = 0;
  for (const a of arrays) { out.set(a, off); off += a.length; }
  return out;
}

function onchainBuildFlipMessage(roundIdBytes, outcomeWon, payoutLamports) {
  if (roundIdBytes.length !== 16) throw new Error('roundId must be 16 bytes');
  return onchainConcat(
    roundIdBytes,
    new Uint8Array([outcomeWon ? 1 : 0]),
    onchainU64Le(payoutLamports),
  );
}

// PDA derivation — mirrors lib.rs `seeds = [b"config"]` etc.
function onchainConfigPda() {
  const { PublicKey } = onchainAssertSdk();
  return PublicKey.findProgramAddressSync(
    [new TextEncoder().encode('config')],
    onchainProgramId(),
  )[0];
}

function onchainRoundPda(roundIdBytes) {
  const { PublicKey } = onchainAssertSdk();
  return PublicKey.findProgramAddressSync(
    [new TextEncoder().encode('round'), roundIdBytes],
    onchainProgramId(),
  )[0];
}

function onchainTreasuryPda() {
  const { PublicKey } = onchainAssertSdk();
  return PublicKey.findProgramAddressSync(
    [new TextEncoder().encode('treasury')],
    onchainProgramId(),
  )[0];
}

// ── place_bet(round_id: [u8;16], amount: u64, game_id: u8) ─────────────────
// Accounts (order must match lib.rs::PlaceBet):
//   0. round           PDA [b"round", round_id]    writable, init
//   1. user            payer                         writable, signer
//   2. system_program                                readonly
async function onchainPlaceBet({ payer, roundIdBytes, amountLamports, gameId = ONCHAIN_GAME_FLIP }) {
  const sdk = onchainAssertSdk();
  const { PublicKey, SystemProgram, Transaction, TransactionInstruction } = sdk;

  if (!payer || !payer.publicKey || !payer.signAndSendTransaction) {
    throw new Error('onchainPlaceBet: payer must have publicKey + signAndSendTransaction');
  }
  if (!(roundIdBytes instanceof Uint8Array) || roundIdBytes.length !== 16) {
    throw new Error('roundIdBytes must be a 16-byte Uint8Array');
  }

  const connection = onchainGetConnection();
  const roundPda = onchainRoundPda(roundIdBytes);

  const data = onchainConcat(
    ONCHAIN_DISC.place_bet,
    roundIdBytes,
    onchainU64Le(amountLamports),
    new Uint8Array([gameId]),
  );

  const ix = new TransactionInstruction({
    programId: onchainProgramId(),
    keys: [
      { pubkey: roundPda,                isSigner: false, isWritable: true  },
      { pubkey: payer.publicKey,         isSigner: true,  isWritable: true  },
      { pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
    ],
    data,
  });

  const tx = new Transaction().add(ix);
  tx.feePayer = payer.publicKey;
  const { blockhash } = await connection.getLatestBlockhash('confirmed');
  tx.recentBlockhash = blockhash;

  const sig = await payer.signAndSendTransaction(tx);
  await connection.confirmTransaction(sig, 'confirmed');
  return { signature: sig, roundPda: roundPda.toBase58(), roundIdHex: onchainBytesToHex(roundIdBytes) };
}

// ── settle(round_id: [u8;16], outcome_won: bool, payout: u64) ──────────────
// Tx must contain TWO instructions in this exact order:
//   [0] Ed25519Program.createInstructionWithPublicKey({ publicKey: workerPubkey,
//        message: roundId(16)|outcome_won(1)|payout_le_u64(8), signature: workerSig })
//   [1] settle program ix
// The on-chain `verify_worker_signature` reads instruction [0] of the same tx
// and asserts pubkey/message match.
//
// Accounts (order must match lib.rs::Settle):
//   0. round                PDA [b"round", round_id]      writable (close = user)
//   1. config               PDA [b"config"]               readonly
//   2. treasury             PDA [b"treasury"]             writable
//   3. user                 payer                         writable, signer
//   4. instructions_sysvar  Sysvar1nstructions1...        readonly
async function onchainSettle({ payer, roundIdBytes, outcomeWon, payoutLamports, workerSigB64, workerPubkeyHex }) {
  const sdk = onchainAssertSdk();
  const { PublicKey, SystemProgram, Transaction, TransactionInstruction, Ed25519Program, SYSVAR_INSTRUCTIONS_PUBKEY } = sdk;

  if (!payer || !payer.publicKey || !payer.signAndSendTransaction) {
    throw new Error('onchainSettle: payer must have publicKey + signAndSendTransaction');
  }

  const connection = onchainGetConnection();
  const roundPda = onchainRoundPda(roundIdBytes);
  const configPda = onchainConfigPda();
  const treasuryPda = onchainTreasuryPda();

  const message = onchainBuildFlipMessage(roundIdBytes, outcomeWon, payoutLamports);
  const signature = onchainBase64ToBytes(workerSigB64);
  if (signature.length !== 64) throw new Error('worker signature must be 64 bytes');
  const workerPubkey = new Uint8Array(workerPubkeyHex.match(/.{2}/g).map(h => parseInt(h, 16)));
  if (workerPubkey.length !== 32) throw new Error('worker pubkey must be 32 bytes');

  const verifyIx = Ed25519Program.createInstructionWithPublicKey({
    publicKey: workerPubkey,
    message,
    signature,
  });

  const settleData = onchainConcat(
    ONCHAIN_DISC.settle,
    roundIdBytes,
    new Uint8Array([outcomeWon ? 1 : 0]),
    onchainU64Le(payoutLamports),
  );

  const settleIx = new TransactionInstruction({
    programId: onchainProgramId(),
    keys: [
      { pubkey: roundPda,                       isSigner: false, isWritable: true  },
      { pubkey: configPda,                      isSigner: false, isWritable: false },
      { pubkey: treasuryPda,                    isSigner: false, isWritable: true  },
      { pubkey: payer.publicKey,                isSigner: true,  isWritable: true  },
      { pubkey: SYSVAR_INSTRUCTIONS_PUBKEY,     isSigner: false, isWritable: false },
      { pubkey: SystemProgram.programId,        isSigner: false, isWritable: false },
    ],
    data: settleData,
  });

  const tx = new Transaction().add(verifyIx).add(settleIx);
  tx.feePayer = payer.publicKey;
  const { blockhash } = await connection.getLatestBlockhash('confirmed');
  tx.recentBlockhash = blockhash;

  const sig = await payer.signAndSendTransaction(tx);
  await connection.confirmTransaction(sig, 'confirmed');
  return { signature: sig };
}

// ── refund(round_id: [u8;16]) ──────────────────────────────────────────────
// Anyone can call after the 1-hour timeout (ROUND_REFUND_TIMEOUT_SECS = 3600).
// Accounts (order must match lib.rs::Refund):
//   0. round    PDA [b"round", round_id]    writable (close = user)
//   1. user     address-constrained to round.user, writable (lamports recipient)
//   2. caller   any signer (typically the user themselves, but doesn't have to be)
async function onchainRefund({ caller, roundIdBytes, userPubkey }) {
  const sdk = onchainAssertSdk();
  const { PublicKey, Transaction, TransactionInstruction } = sdk;

  if (!caller || !caller.publicKey || !caller.signAndSendTransaction) {
    throw new Error('onchainRefund: caller must have publicKey + signAndSendTransaction');
  }
  const userKey = (userPubkey instanceof PublicKey) ? userPubkey : new PublicKey(userPubkey);

  const connection = onchainGetConnection();
  const roundPda = onchainRoundPda(roundIdBytes);

  const data = onchainConcat(ONCHAIN_DISC.refund, roundIdBytes);

  const ix = new TransactionInstruction({
    programId: onchainProgramId(),
    keys: [
      { pubkey: roundPda,         isSigner: false, isWritable: true },
      { pubkey: userKey,          isSigner: false, isWritable: true },
      { pubkey: caller.publicKey, isSigner: true,  isWritable: false },
    ],
    data,
  });

  const tx = new Transaction().add(ix);
  tx.feePayer = caller.publicKey;
  const { blockhash } = await connection.getLatestBlockhash('confirmed');
  tx.recentBlockhash = blockhash;

  const sig = await caller.signAndSendTransaction(tx);
  await connection.confirmTransaction(sig, 'confirmed');
  return { signature: sig };
}

// ── playFlip — bundled tx (§6.16): place_bet ‖ ed25519_verify ‖ settle ─────
// Single transaction containing all three instructions, signed once by the
// user. The Round PDA is created and closed within the same tx — never
// persists, no stranding possible.
//
// The ORDER inside the tx is load-bearing: `settle::verify_worker_signature`
// reads `current_idx - 1`, so the ed25519 verify ix must immediately precede
// settle. place_bet sits at index 0; verify at index 1; settle at index 2.
//
// Caller calls the Worker FIRST to get {outcomeWon, payoutLamports, workerSig,
// workerPubkeyHex}, then hands them to this builder. The user sees the
// outcome in the tx data before approving — accepted as Phase 1 cherry-pick
// risk per §6.16; closed by session keys in §6.17.
async function onchainPlayFlip({ payer, roundIdBytes, amountLamports, gameId = ONCHAIN_GAME_FLIP, outcomeWon, payoutLamports, workerSigB64, workerPubkeyHex }) {
  const sdk = onchainAssertSdk();
  const { PublicKey, SystemProgram, Transaction, TransactionInstruction, Ed25519Program, SYSVAR_INSTRUCTIONS_PUBKEY } = sdk;

  if (!payer || !payer.publicKey || !payer.signAndSendTransaction) {
    throw new Error('onchainPlayFlip: payer must have publicKey + signAndSendTransaction');
  }
  if (!(roundIdBytes instanceof Uint8Array) || roundIdBytes.length !== 16) {
    throw new Error('roundIdBytes must be a 16-byte Uint8Array');
  }

  const connection = onchainGetConnection();
  const roundPda    = onchainRoundPda(roundIdBytes);
  const configPda   = onchainConfigPda();
  const treasuryPda = onchainTreasuryPda();

  // [0] place_bet
  const placeBetData = onchainConcat(
    ONCHAIN_DISC.place_bet,
    roundIdBytes,
    onchainU64Le(amountLamports),
    new Uint8Array([gameId]),
  );
  const placeBetIx = new TransactionInstruction({
    programId: onchainProgramId(),
    keys: [
      { pubkey: roundPda,                isSigner: false, isWritable: true  },
      { pubkey: payer.publicKey,         isSigner: true,  isWritable: true  },
      { pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
    ],
    data: placeBetData,
  });

  // [1] ed25519 verify
  const message = onchainBuildFlipMessage(roundIdBytes, outcomeWon, payoutLamports);
  const signature = onchainBase64ToBytes(workerSigB64);
  if (signature.length !== 64) throw new Error('worker signature must be 64 bytes');
  const workerPubkey = new Uint8Array(workerPubkeyHex.match(/.{2}/g).map(h => parseInt(h, 16)));
  if (workerPubkey.length !== 32) throw new Error('worker pubkey must be 32 bytes');
  const verifyIx = Ed25519Program.createInstructionWithPublicKey({
    publicKey: workerPubkey,
    message,
    signature,
  });

  // [2] settle
  const settleData = onchainConcat(
    ONCHAIN_DISC.settle,
    roundIdBytes,
    new Uint8Array([outcomeWon ? 1 : 0]),
    onchainU64Le(payoutLamports),
  );
  const settleIx = new TransactionInstruction({
    programId: onchainProgramId(),
    keys: [
      { pubkey: roundPda,                       isSigner: false, isWritable: true  },
      { pubkey: configPda,                      isSigner: false, isWritable: false },
      { pubkey: treasuryPda,                    isSigner: false, isWritable: true  },
      { pubkey: payer.publicKey,                isSigner: true,  isWritable: true  },
      { pubkey: SYSVAR_INSTRUCTIONS_PUBKEY,     isSigner: false, isWritable: false },
      { pubkey: SystemProgram.programId,        isSigner: false, isWritable: false },
    ],
    data: settleData,
  });

  const tx = new Transaction().add(placeBetIx).add(verifyIx).add(settleIx);
  tx.feePayer = payer.publicKey;
  const { blockhash } = await connection.getLatestBlockhash('confirmed');
  tx.recentBlockhash = blockhash;

  const sig = await payer.signAndSendTransaction(tx);
  await connection.confirmTransaction(sig, 'confirmed');
  return { signature: sig };
}

// ── withdraw_treasury — admin-only treasury drain ─────────────────────────
// Caller must be the current on-chain `config.authority` (set at initialize,
// rotatable via set_authority). Program rejects with Unauthorized otherwise.
//
// Accounts (must match lib.rs::WithdrawTreasury order):
//   0. config           PDA [b"config"]    writable
//   1. treasury         PDA [b"treasury"]  writable
//   2. authority        signer + recipient
//   3. system_program
async function onchainWithdrawTreasury({ payer, amountLamports }) {
  const sdk = onchainAssertSdk();
  const { SystemProgram, Transaction, TransactionInstruction } = sdk;

  if (!payer || !payer.publicKey || !payer.signAndSendTransaction) {
    throw new Error('onchainWithdrawTreasury: payer must have publicKey + signAndSendTransaction');
  }
  if (typeof amountLamports !== 'bigint' || amountLamports <= 0n) {
    throw new Error('amountLamports must be a positive bigint');
  }

  const connection = onchainGetConnection();
  const data = onchainConcat(ONCHAIN_DISC.withdraw_treasury, onchainU64Le(amountLamports));

  const ix = new TransactionInstruction({
    programId: onchainProgramId(),
    keys: [
      { pubkey: onchainConfigPda(),      isSigner: false, isWritable: true  },
      { pubkey: onchainTreasuryPda(),    isSigner: false, isWritable: true  },
      { pubkey: payer.publicKey,         isSigner: true,  isWritable: true  },
      { pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
    ],
    data,
  });

  const tx = new Transaction().add(ix);
  tx.feePayer = payer.publicKey;
  const { blockhash } = await connection.getLatestBlockhash('confirmed');
  tx.recentBlockhash = blockhash;
  const sig = await payer.signAndSendTransaction(tx);
  await connection.confirmTransaction(sig, 'confirmed');
  return { signature: sig };
}

// ── Session-key path (§6.20 / §6.21 / §6.22) ────────────────────────────
// PDAs:
//   Delegation = [b"delegation", sessionPubkey]
//   UserVault  = [b"vault", user]

function onchainDelegationPda(sessionPubkey) {
  const { PublicKey } = onchainAssertSdk();
  return PublicKey.findProgramAddressSync(
    [new TextEncoder().encode('delegation'), sessionPubkey.toBuffer()],
    onchainProgramId(),
  )[0];
}

function onchainVaultPda(userPubkey) {
  const { PublicKey } = onchainAssertSdk();
  return PublicKey.findProgramAddressSync(
    [new TextEncoder().encode('vault'), userPubkey.toBuffer()],
    onchainProgramId(),
  )[0];
}

function onchainI64Le(n) {
  const v = (typeof n === 'bigint') ? n : BigInt(n);
  const out = new Uint8Array(8);
  let x = v;
  for (let i = 0; i < 8; i++) { out[i] = Number(x & 0xffn); x >>= 8n; }
  return out;
}

// delegate_session — user signs via Phantom. Inits Delegation + UserVault,
// transfers deposit_amount → vault, transfers fee_buffer → session_account.
// One Phantom popup. Args bincode-encoded: 32 + 8 + 8 + 8 + 8 = 64 bytes.
async function onchainDelegateSession({ payer, sessionPubkey, maxTotalBetLamports, durationSecs, depositLamports, feeBufferLamports }) {
  const sdk = onchainAssertSdk();
  const { PublicKey, SystemProgram, Transaction, TransactionInstruction } = sdk;
  if (!payer || !payer.publicKey || !payer.signAndSendTransaction) {
    throw new Error('onchainDelegateSession: payer must have publicKey + signAndSendTransaction');
  }
  if (!(sessionPubkey instanceof PublicKey)) throw new Error('sessionPubkey must be a PublicKey');

  const connection = onchainGetConnection();
  const delegation = onchainDelegationPda(sessionPubkey);
  const vault      = onchainVaultPda(payer.publicKey);

  const data = onchainConcat(
    ONCHAIN_DISC.delegate_session,
    sessionPubkey.toBytes(),
    onchainU64Le(maxTotalBetLamports),
    onchainI64Le(durationSecs),
    onchainU64Le(depositLamports),
    onchainU64Le(feeBufferLamports),
  );

  const ix = new TransactionInstruction({
    programId: onchainProgramId(),
    keys: [
      { pubkey: delegation,              isSigner: false, isWritable: true  },
      { pubkey: vault,                   isSigner: false, isWritable: true  },
      { pubkey: sessionPubkey,           isSigner: false, isWritable: true  },
      { pubkey: payer.publicKey,         isSigner: true,  isWritable: true  },
      { pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
    ],
    data,
  });

  const tx = new Transaction().add(ix);
  tx.feePayer = payer.publicKey;
  const { blockhash } = await connection.getLatestBlockhash('confirmed');
  tx.recentBlockhash = blockhash;
  const sig = await payer.signAndSendTransaction(tx);
  await connection.confirmTransaction(sig, 'confirmed');
  return { signature: sig, delegationPda: delegation.toBase58(), vaultPda: vault.toBase58() };
}

// revoke_session — user signs via Phantom. Closes Delegation + UserVault,
// sweeps rent + remaining vault balance back to wallet via close=user.
async function onchainRevokeSession({ payer, sessionPubkey }) {
  const sdk = onchainAssertSdk();
  const { PublicKey, Transaction, TransactionInstruction } = sdk;
  if (!payer || !payer.publicKey || !payer.signAndSendTransaction) {
    throw new Error('onchainRevokeSession: payer must have publicKey + signAndSendTransaction');
  }

  const connection = onchainGetConnection();
  const delegation = onchainDelegationPda(sessionPubkey);
  const vault      = onchainVaultPda(payer.publicKey);

  const ix = new TransactionInstruction({
    programId: onchainProgramId(),
    keys: [
      { pubkey: delegation,        isSigner: false, isWritable: true  },
      { pubkey: vault,             isSigner: false, isWritable: true  },
      { pubkey: sessionPubkey,     isSigner: false, isWritable: false },
      { pubkey: payer.publicKey,   isSigner: true,  isWritable: true  },
    ],
    data: ONCHAIN_DISC.revoke_session,
  });

  const tx = new Transaction().add(ix);
  tx.feePayer = payer.publicKey;
  const { blockhash } = await connection.getLatestBlockhash('confirmed');
  tx.recentBlockhash = blockhash;
  const sig = await payer.signAndSendTransaction(tx);
  await connection.confirmTransaction(sig, 'confirmed');
  return { signature: sig };
}

// play_flip_session — session keypair signs LOCALLY (no Phantom). The session
// key is the fee payer + the program's signer. Tx contains the ed25519 verify
// ix first, then play_flip_session second.
//
// Args layout (bincode): u64 amount | u8 outcome_won | u64 payout | [u8; 16] round_id
// = 8 + 1 + 8 + 16 = 33 bytes ; total ix data = 8 (disc) + 33 = 41
async function onchainPlayFlipSession({ sessionKp, userPubkey, roundIdBytes, amountLamports, outcomeWon, payoutLamports, workerSigB64, workerPubkeyHex }) {
  const sdk = onchainAssertSdk();
  const { PublicKey, SystemProgram, Transaction, TransactionInstruction, Ed25519Program, SYSVAR_INSTRUCTIONS_PUBKEY } = sdk;
  if (!sessionKp || !sessionKp.publicKey || !sessionKp.secretKey) {
    throw new Error('onchainPlayFlipSession: sessionKp must be a Keypair-like {publicKey, secretKey}');
  }
  if (!(roundIdBytes instanceof Uint8Array) || roundIdBytes.length !== 16) {
    throw new Error('roundIdBytes must be a 16-byte Uint8Array');
  }

  const connection = onchainGetConnection();
  const delegation = onchainDelegationPda(sessionKp.publicKey);
  const configPda  = onchainConfigPda();
  const vault      = onchainVaultPda(userPubkey);
  const treasury   = onchainTreasuryPda();

  // [0] ed25519 verify
  const message = onchainBuildFlipMessage(roundIdBytes, outcomeWon, payoutLamports);
  const signature = onchainBase64ToBytes(workerSigB64);
  if (signature.length !== 64) throw new Error('worker signature must be 64 bytes');
  const workerPubkey = new Uint8Array(workerPubkeyHex.match(/.{2}/g).map(h => parseInt(h, 16)));
  if (workerPubkey.length !== 32) throw new Error('worker pubkey must be 32 bytes');
  const verifyIx = Ed25519Program.createInstructionWithPublicKey({ publicKey: workerPubkey, message, signature });

  // [1] play_flip_session
  const playData = onchainConcat(
    ONCHAIN_DISC.play_flip_session,
    onchainU64Le(amountLamports),
    new Uint8Array([outcomeWon ? 1 : 0]),
    onchainU64Le(payoutLamports),
    roundIdBytes,
  );

  const playIx = new TransactionInstruction({
    programId: onchainProgramId(),
    keys: [
      { pubkey: delegation,                  isSigner: false, isWritable: true  },
      { pubkey: configPda,                   isSigner: false, isWritable: false },
      { pubkey: vault,                       isSigner: false, isWritable: true  },
      { pubkey: treasury,                    isSigner: false, isWritable: true  },
      { pubkey: sessionKp.publicKey,         isSigner: true,  isWritable: true  },
      { pubkey: SYSVAR_INSTRUCTIONS_PUBKEY,  isSigner: false, isWritable: false },
      { pubkey: SystemProgram.programId,     isSigner: false, isWritable: false },
    ],
    data: playData,
  });

  const tx = new Transaction().add(verifyIx).add(playIx);
  tx.feePayer = sessionKp.publicKey;
  const { blockhash } = await connection.getLatestBlockhash('confirmed');
  tx.recentBlockhash = blockhash;
  // Sign LOCALLY with the session keypair (no Phantom).
  tx.sign(sessionKp);
  const sig = await connection.sendRawTransaction(tx.serialize(), { skipPreflight: false, preflightCommitment: 'confirmed' });
  await connection.confirmTransaction(sig, 'confirmed');
  return { signature: sig };
}

// Read the Delegation account if it exists. Returns null when there's no
// active session for `sessionPubkey`. Used by wallet.jsx to recover state
// after a page reload + to read total_bet_used for the session-status pill.
async function onchainReadDelegation(sessionPubkey) {
  const sdk = onchainAssertSdk();
  const { PublicKey } = sdk;
  const connection = onchainGetConnection();
  const info = await connection.getAccountInfo(onchainDelegationPda(sessionPubkey));
  if (!info) return null;
  // Anchor: [0..8] disc | [8..40] user | [40..48] max | [48..56] used | [56..64] expires_at | [64] bump
  const buf = info.data;
  if (buf.length < 65) return null;
  const dv = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
  return {
    user: new PublicKey(buf.slice(8, 40)).toBase58(),
    maxTotalBetLamports: dv.getBigUint64(40, true),
    totalBetUsedLamports: dv.getBigUint64(48, true),
    expiresAt: Number(dv.getBigInt64(56, true)),
    bump: buf[64],
  };
}

// Read the GlobalConfig PDA + current treasury balance. Used by the admin
// UI to show "Treasury: X SOL · Authority: Y" without round-tripping every
// piece of state separately.
async function onchainReadConfig() {
  const sdk = onchainAssertSdk();
  const { PublicKey } = sdk;
  const connection = onchainGetConnection();
  const configPda = onchainConfigPda();
  const treasuryPda = onchainTreasuryPda();
  const [cfg, treasury] = await Promise.all([
    connection.getAccountInfo(configPda),
    connection.getBalance(treasuryPda),
  ]);
  if (!cfg) return null;
  // GlobalConfig layout: [0..8] disc | [8..40] worker_pubkey | [40..72] authority | [72] bump
  return {
    workerPubkeyHex: Array.from(cfg.data.slice(8, 40)).map(b => b.toString(16).padStart(2, '0')).join(''),
    authority: new PublicKey(cfg.data.slice(40, 72)).toBase58(),
    treasuryLamports: treasury,
    treasurySol: treasury / 1e9,
  };
}

// Read the on-chain Round account if it exists. Used by the stranded-round
// detector (milestone 3c) and as a sanity check after place_bet.
async function onchainReadRound(roundIdBytes) {
  const sdk = onchainAssertSdk();
  const { PublicKey } = sdk;

  const connection = onchainGetConnection();
  const roundPda = onchainRoundPda(roundIdBytes);
  const info = await connection.getAccountInfo(roundPda);
  if (!info) return null;
  // Anchor layout: [0..8] disc | [8..40] user | [40..48] amount LE | [48] game_id
  // | [49] status | [50..58] placed_at LE i64 | [58..74] round_id | [74] bump
  const buf = info.data;
  if (buf.length < 75) return null;

  const dv = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
  const user = new PublicKey(buf.slice(8, 40)).toBase58();
  const amount = dv.getBigUint64(40, true);
  const gameId = buf[48];
  const status = buf[49];
  const placedAt = Number(dv.getBigInt64(50, true));
  const roundIdHex = onchainBytesToHex(buf.slice(58, 74));
  const bump = buf[74];

  return {
    address: roundPda.toBase58(),
    user, amount, gameId, status, placedAt, roundIdHex, bump,
    lamports: info.lamports,
  };
}

Object.assign(window, {
  onchainPlayFlip,
  onchainPlaceBet,
  onchainSettle,
  onchainRefund,
  onchainWithdrawTreasury,
  onchainDelegateSession,
  onchainRevokeSession,
  onchainPlayFlipSession,
  onchainReadDelegation,
  onchainReadConfig,
  onchainReadRound,
  onchainGetConnection,
  onchainRandomRoundId,
  onchainBytesToHex,
  onchainHexToBytes,
  onchainProgramId,
  onchainConfigPda,
  onchainRoundPda,
  onchainTreasuryPda,
  onchainDelegationPda,
  onchainVaultPda,
  ONCHAIN_GAME_FLIP,
  ONCHAIN_PROGRAM_ID_STR,
  ONCHAIN_RPC_URL_DEVNET,
});
