#!/usr/bin/env ts-node

import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
import path from 'path';
import { Contract, JsonRpcProvider, formatUnits } from 'ethers';
import { CANONICAL_TOKENS, getTokenRegistryFamily } from '../src/config/canonical-tokens';
import { getChainConfig } from '../src/config/chains';
import { NETWORKS } from '../src/config/networks';

const ERC20_ABI = [
  'function totalSupply() view returns (uint256)',
  'function decimals() view returns (uint8)',
  'function symbol() view returns (string)',
  'function name() view returns (string)',
];

type SupplyProof = {
  schema: string;
  generatedAt: string;
  network: {
    chainId: number;
    name: string;
    referenceBlock: number;
  };
  token: {
    address: string;
    name: string;
    symbol: string;
    onchainName?: string;
    onchainSymbol?: string;
    decimals: number;
    totalSupplyRaw: string;
    totalSupplyUnits: string;
  };
  circulatingSupplyMethodology: {
    status: string;
    recommendedFormula: string;
    currentConservativeReportableCirculatingSupplyUnits: string;
    notes: string[];
  };
  knownBalances?: unknown;
  trackerSurfaces?: unknown;
  [key: string]: unknown;
};

type SupplyProofCatalog = {
  schema: string;
  generatedAt: string;
  proofs: SupplyProof[];
  proofFailures?: Array<{
    chainId: number;
    symbol: string;
    address: string;
    reason: string;
  }>;
};

function isCandidate(symbol: string, type?: string, registryFamily?: string): boolean {
  return (
    /^c[A-Z0-9]/.test(symbol) &&
    (type === 'base' ||
      type === 'c' ||
      type === 'w' ||
      ['iso4217', 'monetary_unit', 'gas_native', 'commodity'].includes(String(registryFamily || '')))
  );
}

function catalogKey(chainId: number, address: string): string {
  return `${chainId}:${address.toLowerCase()}`;
}

function rpcUrlsForChain(chainId: number, primary?: string): string[] {
  const network = NETWORKS.find((row) => row.chainIdDecimal === chainId);
  return Array.from(new Set([primary, ...(network?.rpcUrls ?? [])].filter(Boolean) as string[]));
}

function loadProofFile(proofPath: string): SupplyProof[] {
  if (!existsSync(proofPath)) return [];
  const parsed = JSON.parse(readFileSync(proofPath, 'utf8')) as Partial<SupplyProofCatalog> | SupplyProof;
  const proofs = Array.isArray((parsed as Partial<SupplyProofCatalog>).proofs)
    ? ((parsed as SupplyProofCatalog).proofs)
    : [parsed as SupplyProof];
  return proofs.filter((proof) => proof?.network?.chainId && proof?.token?.address);
}

function loadExistingCatalog(seedPaths: string[]): Map<string, SupplyProof> {
  const proofs = new Map<string, SupplyProof>();
  for (const seedPath of seedPaths) {
    for (const proof of loadProofFile(seedPath)) {
      proofs.set(catalogKey(proof.network.chainId, proof.token.address), proof);
    }
  }
  return proofs;
}

function shouldPreserveExistingProof(proof: SupplyProof | undefined): boolean {
  if (!proof) return false;
  return (
    proof.knownBalances !== undefined ||
    proof.trackerSurfaces !== undefined ||
    proof.circulatingSupplyMethodology?.status === 'ready_for_tracker_review'
  );
}

async function withTimeout<T>(promise: Promise<T>, timeoutMs: number, label: string): Promise<T> {
  let timeout: NodeJS.Timeout | undefined;
  const timer = new Promise<never>((_, reject) => {
    timeout = setTimeout(() => reject(new Error(`${label} timed out after ${timeoutMs}ms`)), timeoutMs);
  });
  try {
    return await Promise.race([promise, timer]);
  } finally {
    if (timeout) clearTimeout(timeout);
  }
}

async function main() {
  const serviceRoot = path.resolve(__dirname, '..');
  const catalogPath = path.resolve(
    process.env.TOKEN_AGGREGATION_SUPPLY_PROOF_CATALOG_JSON ||
      path.join(serviceRoot, 'config/supply-proof-catalog.json')
  );
  const repoRoot = path.resolve(serviceRoot, '..', '..', '..');
  const timeoutMs = Number(process.env.SUPPLY_PROOF_RPC_TIMEOUT_MS || 12000);
  const generatedAt = new Date().toISOString();
  const existing = loadExistingCatalog([
    catalogPath,
    path.join(repoRoot, 'reports/status/mainnet-cwusdc-supply-proof-20260508.json'),
  ]);
  const proofs = new Map(existing);
  const failures: SupplyProofCatalog['proofFailures'] = [];

  for (const spec of CANONICAL_TOKENS) {
    const registryFamily = getTokenRegistryFamily(spec);
    if (!isCandidate(spec.symbol, spec.type, registryFamily)) continue;

    for (const [chainIdText, rawAddress] of Object.entries(spec.addresses)) {
      const chainId = Number(chainIdText);
      const address = String(rawAddress || '').trim();
      if (!address.startsWith('0x')) continue;
      const chain = getChainConfig(chainId);
      if (!chain?.rpcUrl) {
        failures.push({ chainId, symbol: spec.symbol, address, reason: 'missing_rpc_url' });
        continue;
      }

      let proved = false;
      const reasons: string[] = [];
      for (const rpcUrl of rpcUrlsForChain(chainId, chain.rpcUrl)) {
        try {
          const provider = new JsonRpcProvider(rpcUrl, chainId, { staticNetwork: true });
          const contract = new Contract(address, ERC20_ABI, provider);
          const [referenceBlock, totalSupplyRaw, decimals, onchainSymbol, onchainName] = await withTimeout(
            Promise.all([
              provider.getBlockNumber(),
              contract.totalSupply() as Promise<bigint>,
              contract.decimals().catch(() => spec.decimals) as Promise<number>,
              contract.symbol().catch(() => undefined) as Promise<string | undefined>,
              contract.name().catch(() => undefined) as Promise<string | undefined>,
            ]),
            timeoutMs,
            `${chainId}:${spec.symbol}`
          );
          const decimalsNumber = Number(decimals);
          const totalSupplyRawText = totalSupplyRaw.toString();
          const totalSupplyUnits = formatUnits(totalSupplyRaw, decimalsNumber);
          const key = catalogKey(chainId, address);
          if (!shouldPreserveExistingProof(proofs.get(key))) {
            proofs.set(key, {
              schema: 'token-aggregation-supply-proof/v1',
              generatedAt,
              network: {
                chainId,
                name: chain.name,
                referenceBlock,
              },
              token: {
                address,
                name: spec.name,
                symbol: spec.symbol,
                onchainName,
                onchainSymbol,
                decimals: decimalsNumber,
                totalSupplyRaw: totalSupplyRawText,
                totalSupplyUnits,
              },
              circulatingSupplyMethodology: {
                status: 'onchain_total_supply_proved_circulating_review_required',
                recommendedFormula: 'circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances',
                currentConservativeReportableCirculatingSupplyUnits: totalSupplyUnits,
                notes: [
                  'totalSupply was read from the mapped token contract at the reference block.',
                  'circulatingSupply is conservatively set to totalSupply until protocol-controlled non-circulating balances are supplied.',
                  'Tracker acceptance is external and not implied by this API response.',
                ],
              },
            });
          }
          proved = true;
          break;
        } catch (error) {
          reasons.push(`${rpcUrl}: ${error instanceof Error ? error.message : String(error)}`);
        }
      }

      if (!proved) {
        failures.push({
          chainId,
          symbol: spec.symbol,
          address,
          reason: reasons.join(' | '),
        });
      }
    }
  }

  const catalog: SupplyProofCatalog = {
    schema: 'token-aggregation-supply-proof-catalog/v1',
    generatedAt,
    proofs: Array.from(proofs.values()).sort(
      (a, b) => a.network.chainId - b.network.chainId || a.token.symbol.localeCompare(b.token.symbol)
    ),
    proofFailures: failures,
  };

  mkdirSync(path.dirname(catalogPath), { recursive: true });
  writeFileSync(catalogPath, `${JSON.stringify(catalog, null, 2)}\n`);
  console.log(JSON.stringify({
    catalogPath,
    proofs: catalog.proofs.length,
    failures: failures.length,
  }, null, 2));
}

main().catch((error) => {
  console.error(error);
  process.exit(1);
});
