#!/usr/bin/env node
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';

const root = new URL('./', import.meta.url);
const record = JSON.parse(await readFile(new URL('bos-proceeds-509-trace.json', root), 'utf8'));
const sources = JSON.parse(await readFile(new URL('sources.json', root), 'utf8'));
const walletFlows = JSON.parse(await readFile(new URL('bos-presale-wallet-flows.json', root), 'utf8'));

function decimal(value, scale = 18) {
  assert.match(value, /^\d+(?:\.\d+)?$/, `invalid decimal: ${value}`);
  const [whole, fraction = ''] = value.split('.');
  assert.ok(fraction.length <= scale, `${value} exceeds scale ${scale}`);
  return BigInt(`${whole}${fraction.padEnd(scale, '0')}`);
}

function format(value, scale = 18, places = scale) {
  const padded = value.toString().padStart(scale + 1, '0');
  const whole = scale ? padded.slice(0, -scale) : padded;
  const fraction = scale ? padded.slice(-scale).slice(0, places).replace(/0+$/, '') : '';
  return `${whole}${fraction ? `.${fraction}` : ''}`;
}

const add = (values, scale = 18) => values.reduce((sum, value) => sum + decimal(value, scale), 0n);
const subtract = (left, right, scale = 18) => decimal(left, scale) - decimal(right, scale);
const roundedPercent = (numerator, denominator, places = 6, scale = 20) => {
  const numeratorScaled = decimal(numerator, scale);
  const denominatorScaled = decimal(denominator, scale);
  const factor = 10n ** BigInt(places);
  const rounded = (numeratorScaled * 100n * factor + denominatorScaled / 2n) / denominatorScaled;
  const padded = rounded.toString().padStart(places + 1, '0');
  return `${padded.slice(0, -places)}.${padded.slice(-places)}`;
};

const ethereum = record.ethereum;
const receipts = ethereum.collectorReceipts;
assert.equal(format(add(receipts.USDT.receipts.map((entry) => entry.amount))), receipts.USDT.total);
assert.equal(format(add(receipts.USDC.receipts.map((entry) => entry.amount))), receipts.USDC.total);
assert.equal(
  format(add([receipts.USDT.total, receipts.USDC.total])),
  receipts.combinedUsdtUsdc.total
);
assert.equal(
  format(subtract(receipts.USDT.total, receipts.USDT.lowestBalanceByNovember30)),
  receipts.USDT.minimumCollectorLotMoved
);

const usdcOtherFunds = add([
  receipts.USDC.preReceiptBalance,
  receipts.USDC.linked60cReceipt,
  receipts.USDC.otherNonSaleFundsAvailable
]);
assert.equal(format(usdcOtherFunds), ethereum.identifiedExchangeRoutes.USDC.maximumOtherFundsAvailable);
assert.equal(
  format(decimal(receipts.USDC.novemberOutflows) - usdcOtherFunds),
  receipts.USDC.minimumCollectorLotMoved
);
assert.equal(
  format(add([receipts.USDT.minimumCollectorLotMoved, receipts.USDC.minimumCollectorLotMoved])),
  receipts.combinedUsdtUsdc.minimumMovedByNovember30
);
assert.equal(
  roundedPercent(receipts.combinedUsdtUsdc.minimumMovedByNovember30, receipts.combinedUsdtUsdc.total),
  receipts.combinedUsdtUsdc.minimumMovedPercent
);

const exchange = ethereum.identifiedExchangeRoutes;
assert.equal(
  format(add(exchange.USDT.routes.map((route) => route.grossInput))),
  exchange.USDT.grossEnteringIdentifiedRoutes
);
assert.equal(
  format(subtract(exchange.USDT.grossEnteringIdentifiedRoutes, exchange.USDT.maximumOtherFundsAvailable)),
  exchange.USDT.minimumCollectorFunded
);
assert.equal(
  format(add(exchange.USDC.routes.map((route) => route.grossInput))),
  exchange.USDC.grossEnteringIdentifiedRoutes
);
assert.equal(
  format(subtract(exchange.USDC.grossEnteringIdentifiedRoutes, exchange.USDC.maximumOtherFundsAvailable)),
  exchange.USDC.minimumCollectorFunded
);
assert.equal(
  format(add([exchange.USDT.minimumCollectorFunded, exchange.USDC.minimumCollectorFunded])),
  exchange.combinedMinimumCollectorFunded
);
const gemini = exchange.USDT.routes.find((route) => route.service === 'Gemini');
assert.ok(gemini);
assert.equal(
  format(subtract(gemini.grossInput, receipts.USDT.preReceiptBalance)),
  gemini.minimumCollectorFunded
);
assert.equal(gemini.transactions.length, 2);

const krakenUsdt = exchange.USDT.routes.find((route) => route.service === 'Kraken');
const krakenUsdc = exchange.USDC.routes.find((route) => route.service === 'Kraken');
assert.ok(krakenUsdt && krakenUsdc);
assert.equal(krakenUsdt.transactions.length, 6);
assert.equal(format(subtract(krakenUsdt.swapInputUSDT, krakenUsdt.swapOutputUSDC)), krakenUsdt.valueLossBeforeEndpoint);
assert.equal(
  format(add([krakenUsdt.swapOutputUSDC, krakenUsdt.intermediatePreexistingUSDC])),
  krakenUsdt.labeledEndpointTransferUSDC
);
assert.equal(
  format(subtract(krakenUsdc.labeledEndpointTransferUSDC, krakenUsdc.relayPreexistingUSDC)),
  krakenUsdc.minimumInputReachingLabeledEndpoint
);
const endpoints = exchange.labeledExchangeEndpointReconciliation;
assert.equal(endpoints.USDT.minimumCollectorFundedEnteringBoundRoutes, exchange.USDT.minimumCollectorFunded);
assert.equal(endpoints.USDT.valueLossBeforeLabeledEndpoint, krakenUsdt.valueLossBeforeEndpoint);
assert.equal(
  format(subtract(endpoints.USDT.minimumCollectorFundedEnteringBoundRoutes, endpoints.USDT.valueLossBeforeLabeledEndpoint)),
  endpoints.USDT.minimumReachingLabeledExchangeAddresses
);
assert.equal(endpoints.USDC.minimumCollectorFundedEnteringBoundRoutes, exchange.USDC.minimumCollectorFunded);
assert.equal(endpoints.USDC.relayPreexistingFunds, krakenUsdc.relayPreexistingUSDC);
assert.equal(
  format(subtract(endpoints.USDC.minimumCollectorFundedEnteringBoundRoutes, endpoints.USDC.relayPreexistingFunds)),
  endpoints.USDC.minimumReachingLabeledExchangeAddresses
);
assert.equal(
  format(add([
    endpoints.USDT.minimumReachingLabeledExchangeAddresses,
    endpoints.USDC.minimumReachingLabeledExchangeAddresses
  ])),
  endpoints.combinedMinimumReachingLabeledExchangeAddresses
);
assert.equal(
  roundedPercent(endpoints.combinedMinimumReachingLabeledExchangeAddresses, receipts.combinedUsdtUsdc.total),
  endpoints.combinedMinimumPercentOfDirectStablecoinReceipts
);

const bnb = record.bnbChain;
assert.equal(
  format(add([bnb.knownCollectorReceipts.USDT, bnb.knownCollectorReceipts.USDC])),
  bnb.knownCollectorReceipts.stablecoinFaceValue
);
assert.equal(
  format(add([bnb.associated60cBranch.USDT, bnb.associated60cBranch.USDC])),
  bnb.associated60cBranch.stablecoinFaceValue
);
assert.equal(bnb.associated60cBranch.BNB, '2.346967086497769');
assert.equal(bnb.conservationAtCutoff.nonzeroUsdtOutflowAfterReceipts, '0');
assert.equal(bnb.conservationAtCutoff.nonzeroUsdcOutflowAfterReceipts, '0');
assert.ok(decimal(bnb.conservationAtCutoff.currentUSDT) >= decimal(bnb.knownCollectorReceipts.USDT));
assert.ok(decimal(bnb.conservationAtCutoff.currentUSDC) >= decimal(bnb.knownCollectorReceipts.USDC));
assert.equal(
  format(add([bnb.binanceRoute.USDT, bnb.binanceRoute.USDC])),
  bnb.binanceRoute.stablecoinFaceValue
);

const reconciliation = record.originsCounterReconciliation;
const usdScale = 20;
const branch60c = walletFlows.associatedEthereumCollectorBranches.find((branch) => branch.id === 'origins-linked-60c');
assert.ok(branch60c);
assert.equal(branch60c.exactPurchaseSweepLowerBound.rows, 5);
assert.equal(branch60c.exactPurchaseSweepLowerBound.uniqueDepositAddresses, 4);
assert.equal(
  format(add(Object.values(branch60c.exactPurchaseSweepLowerBound.totals))),
  '205.915601'
);
assert.equal(
  format(add([branch60c.allObservedCanonicalReceipts.USDT, branch60c.allObservedCanonicalReceipts.USDC])),
  branch60c.allObservedCanonicalReceipts.stablecoinFaceValue
);
const conservativeEthereumStablecoins = [
  ...walletFlows.ethereumCollectors.flatMap((collector) => Object.entries(collector.exactPurchaseSweepLowerBound.totals)
    .filter(([symbol]) => symbol !== 'WBTC')
    .map(([, amount]) => amount)),
  ...Object.values(branch60c.exactPurchaseSweepLowerBound.totals)
];
const polygonStablecoin = walletFlows.partialMultichainFootprint.find((entry) => entry.chain === 'Polygon')?.received.USDT;
assert.ok(polygonStablecoin);
assert.equal(
  format(add([...conservativeEthereumStablecoins, polygonStablecoin], usdScale), usdScale, 6),
  reconciliation.conservativeMatchedStablecoinReceipts
);
assert.equal(
  add(Object.values(reconciliation.coverageComponents.conservative), usdScale),
  decimal(reconciliation.conservativeMatchedStablecoinReceipts, usdScale)
);
assert.equal(
  add(Object.values(reconciliation.coverageComponents.expanded), usdScale),
  decimal(reconciliation.expandedCanonicalStablecoinReceipts, usdScale)
);
const conservativeGap = decimal(reconciliation.usdCollected, usdScale)
  - decimal(reconciliation.conservativeMatchedStablecoinReceipts, usdScale);
const expandedGap = decimal(reconciliation.usdCollected, usdScale)
  - decimal(reconciliation.expandedCanonicalStablecoinReceipts, usdScale);
assert.equal(format(conservativeGap, usdScale, 6), reconciliation.conservativePublicCoverageGap);
assert.equal(format(expandedGap, usdScale, 6), reconciliation.expandedPublicCoverageGap);
assert.equal(
  roundedPercent(reconciliation.conservativeMatchedStablecoinReceipts, reconciliation.usdCollected),
  reconciliation.conservativeCoveragePercent
);
assert.equal(
  roundedPercent(reconciliation.expandedCanonicalStablecoinReceipts, reconciliation.usdCollected),
  reconciliation.expandedCoveragePercent
);

const sourceIds = new Set(sources.map((source) => source.id));
const referencedSourceIds = [];
const collectSourceIds = (value) => {
  if (Array.isArray(value)) return value.forEach(collectSourceIds);
  if (!value || typeof value !== 'object') return;
  for (const [key, entry] of Object.entries(value)) {
    if (key === 'sourceIds') referencedSourceIds.push(...entry);
    else collectSourceIds(entry);
  }
};
collectSourceIds(record);
assert.ok(referencedSourceIds.length >= 20);
for (const id of referencedSourceIds) assert.ok(sourceIds.has(id), `missing source ${id}`);
assert.ok(record.recordsAuthoritiesShouldObtain.some((entry) => /Gemini/.test(entry)));
assert.ok(record.recordsAuthoritiesShouldObtain.some((entry) => /Fireblocks/.test(entry)));
assert.ok(record.recordsAuthoritiesShouldObtain.some((entry) => /beneficial owner/i.test(entry)));

process.stdout.write('BOS proceeds / 0x509201 verification PASS\n');
process.stdout.write(`- direct Ethereum USDT/USDC receipts: ${receipts.combinedUsdtUsdc.total}\n`);
process.stdout.write(`- minimum moved by 30 November: ${receipts.combinedUsdtUsdc.minimumMovedByNovember30}\n`);
process.stdout.write(`- minimum entering CEX-bound first hops: ${exchange.combinedMinimumCollectorFunded}\n`);
process.stdout.write(`- minimum reaching publicly labeled CEX addresses: ${endpoints.combinedMinimumReachingLabeledExchangeAddresses}\n`);
process.stdout.write(`- minimum collector-funded Gemini route: ${gemini.minimumCollectorFunded}\n`);
process.stdout.write(`- BNB known-collector receipts conserved at 0x509201: ${bnb.knownCollectorReceipts.stablecoinFaceValue}\n`);
process.stdout.write(`- expanded stablecoin coverage of Origins counter: ${reconciliation.expandedCoveragePercent}%\n`);
