import { ethers } from 'ethers';
import { assertAggregatorConfig, checkpointAggregatorConfig as cfg } from './config';
import { signBatchAttestation, CheckpointHeaderStruct } from './eip712';
import { publishBatchPayload, BatchPayload } from './ipfs';
import { fetchTransactionsInBlockRange } from './blockscout';
import { enrichLeafFromBlockscoutApi, enrichLeafUsd } from './usdEnrich';
import { loadScanState, saveScanState, type ScanState } from './scanState';
import { sumUsdStrings } from '@dbis/checkpoint-core';
import { effectiveValue, paymentsRoot } from './leafCodec';
import { AdminCheckpointIngress, buildCheckpointContract } from './ingress/adminIngress';
import type { PaymentLeaf } from './ingress/types';
import { dualWriteV1Mirror } from './dualMirror';
import { recordActivityBatch } from './activityRegistry';
import { recordIsoAttestationBatch } from './activityRegistryV2';
import { attachIso20022ToLeaves } from './iso20022Enrich';
import { persistIso20022ForLeaves } from './iso20022LocalStore';
import { surfaceParticipantsOnMainnet, surfaceTopLevelZeroEth } from './participantSurface';
import { attachReceiptMeta, fetchReceiptMeta, receiptsRoot } from './receiptsRoot';

assertAggregatorConfig();

const BLOCK_ORACLE_ABI = ['function setBlockHeader(uint256,bytes32,bytes32) external'];

class CheckpointAggregator {
  private chain138: ethers.JsonRpcProvider;
  private mainnetWallet: ethers.Wallet;
  private checkpoint: ethers.Contract;
  private ingress: AdminCheckpointIngress;
  private blockOracle?: ethers.Contract;
  private buffer: PaymentLeaf[] = [];
  private nextBatchId = 1n;
  private hubImplVersion = 3n;
  private flushTimer?: NodeJS.Timeout;
  private scanning = false;
  private readonly includedTxCache = new Map<string, boolean>();

  constructor() {
    const mainnet = new ethers.JsonRpcProvider(cfg.mainnetRpc);
    this.mainnetWallet = new ethers.Wallet(process.env.PRIVATE_KEY!, mainnet);
    this.checkpoint = buildCheckpointContract(
      this.mainnetWallet,
      cfg.checkpointProxy,
      cfg.submitMode
    );
    this.ingress = new AdminCheckpointIngress(this.checkpoint, cfg.submitMode);
    this.chain138 = new ethers.JsonRpcProvider(cfg.chain138Rpc);
    if (cfg.blockOracleExtension) {
      this.blockOracle = new ethers.Contract(cfg.blockOracleExtension, BLOCK_ORACLE_ABI, this.mainnetWallet);
    }
  }

  async start() {
    await this.syncBatchId();
    const state = loadScanState(cfg.scanStatePath, cfg.scanFromBlock);
    console.log(
      `Checkpoint aggregator mode=${cfg.submitMode} batch=${cfg.batchSize} nextBatchId=${this.nextBatchId} impl=${this.hubImplVersion} blockscout=${cfg.useBlockscout} emitter=${cfg.batchEmitter138 || 'n/a'}`
    );
    this.schedulePartialFlush();
    await this.runScanLoop(state);
  }

  private async syncBatchId() {
    const latest = await this.checkpoint.getLatestBatchId();
    this.nextBatchId = latest > 0n ? BigInt(latest) + 1n : 1n;
    try {
      this.hubImplVersion = await this.checkpoint.IMPLEMENTATION_VERSION();
    } catch {
      this.hubImplVersion = 3n;
    }
  }

  private schedulePartialFlush() {
    if (this.flushTimer) clearTimeout(this.flushTimer);
    this.flushTimer = setTimeout(() => {
      void this.flush(true).catch((e) => console.error('partial flush failed', e));
    }, cfg.maxWaitMs);
  }

  private async runScanLoop(initialState: ScanState) {
    const state: ScanState = { ...initialState };
    for (;;) {
      try {
        await this.scanCatchUp(state);
      } catch (e) {
        console.error('scanCatchUp', e);
      }
      await new Promise((r) => setTimeout(r, cfg.scanPollMs));
    }
  }

  private async scanCatchUp(state: ScanState) {
    if (this.scanning) return;
    this.scanning = true;
    try {
      const head = await this.chain138.getBlockNumber();
      const safeHead = head - cfg.confirmationBlocks;
      let from = state.lastScannedBlock + 1;
      while (from <= safeHead) {
        const to = Math.min(from + cfg.scanChunkBlocks - 1, safeHead);
        let added = 0;
        if (cfg.useBlockscout) {
          added = await this.scanBlockscoutRange(from, to);
        } else {
          for (let b = from; b <= to; b++) added += await this.scanBlockRpc(b);
        }
        state.lastScannedBlock = to;
        saveScanState(cfg.scanStatePath, state);
        console.log(`Scanned blocks ${from}-${to} (+${added} txs) buffer=${this.buffer.length}`);
        from = to + 1;
      }
    } finally {
      this.scanning = false;
    }
  }

  private async scanBlockscoutRange(from: number, to: number): Promise<number> {
    const txs = await fetchTransactionsInBlockRange(cfg.blockscoutApi, from, to);
    let added = 0;
    for (const tx of txs) {
      if (await this.txAlreadyIncluded(tx.hash)) continue;
      const receipt = await this.chain138.getTransactionReceipt(tx.hash);
      const leaf: PaymentLeaf = {
        txHash: tx.hash,
        from: tx.from,
        to: tx.to,
        value: tx.value,
        blockNumber: tx.blockNumber,
        blockTimestamp: tx.blockTimestamp,
        gasUsed: receipt?.gasUsed ?? 0n,
        success: receipt?.status === 1,
      };
      await this.enrichLeafFromBlockscout(leaf);
      if (effectiveValue(leaf) < cfg.minValueWei) continue;
      this.buffer.push(leaf);
      added++;
      if (this.buffer.length >= cfg.batchSize) await this.flush(false);
    }
    return added;
  }

  private async scanBlockRpc(blockNumber: number): Promise<number> {
    const block = await this.chain138.getBlock(blockNumber, true);
    if (!block) return 0;
    let added = 0;
    const entries = block.prefetchedTransactions?.length
      ? block.prefetchedTransactions
      : block.transactions ?? [];
    for (const entry of entries) {
      const tx =
        typeof entry === 'string' ? await this.chain138.getTransaction(entry) : entry;
      if (!tx || typeof tx === 'string') continue;
      const value = tx.value ?? 0n;
      if (await this.txAlreadyIncluded(tx.hash)) continue;
      const receipt = await this.chain138.getTransactionReceipt(tx.hash);
      const leaf: PaymentLeaf = {
        txHash: tx.hash,
        from: tx.from,
        to: tx.to ?? ethers.ZeroAddress,
        value,
        blockNumber,
        blockTimestamp: block.timestamp,
        gasUsed: receipt?.gasUsed ?? 0n,
        success: receipt?.status === 1,
      };
      await this.enrichLeafFromBlockscout(leaf);
      if (effectiveValue(leaf) < cfg.minValueWei) continue;
      this.buffer.push(leaf);
      added++;
      if (this.buffer.length >= cfg.batchSize) await this.flush(false);
    }
    return added;
  }

  private async enrichLeafFromBlockscout(leaf: PaymentLeaf) {
    const { allErc20 } = await enrichLeafFromBlockscoutApi(leaf, cfg.blockscoutApi, cfg.useBlockscout);
    await enrichLeafUsd(
      leaf,
      {
        apiBaseUrl: cfg.tokenAggregationApiUrl,
        chainId: 138,
        enabled: cfg.usdEnrichEnabled,
        requestDelayMs: cfg.usdRequestDelayMs,
        blockscoutApi: cfg.blockscoutApi,
        useBlockscout: cfg.useBlockscout,
      },
      allErc20
    );
  }

  private async txAlreadyIncluded(txHash: string): Promise<boolean> {
    const cached = this.includedTxCache.get(txHash);
    if (cached !== undefined) return cached;
    try {
      const [included] = await this.checkpoint.isTxIncluded(txHash);
      this.includedTxCache.set(txHash, included);
      return included;
    } catch {
      return false;
    }
  }

  private async flush(partial: boolean) {
    if (this.buffer.length === 0) return;
    const leaves = this.buffer.splice(0, cfg.batchSize);

    if (cfg.attachReceiptMeta) {
      await attachReceiptMeta(this.chain138, leaves);
    } else {
      for (const leaf of leaves) {
        const meta = await fetchReceiptMeta(this.chain138, leaf.txHash);
        leaf.logCount = meta.logCount;
        leaf.receiptHash = meta.receiptHash;
      }
    }

    attachIso20022ToLeaves(leaves);

    const root = paymentsRoot(138, leaves);
    const receiptMetas = leaves.map((l) => ({
      logCount: l.logCount ?? 0,
      receiptHash: l.receiptHash ?? ethers.ZeroHash,
    }));
    const receiptsRootHash = receiptsRoot(
      leaves.map((l) => l.txHash),
      receiptMetas
    );
    const checkpointBlock = Math.max(...leaves.map((l) => l.blockNumber));
    const block = await this.chain138.getBlock(checkpointBlock);
    const blockHash = block?.hash ?? ethers.ZeroHash;
    const stateRoot = block?.stateRoot ?? ethers.ZeroHash;

    if (cfg.updateBlockOracle && this.blockOracle) {
      const tx = await this.blockOracle.setBlockHeader(checkpointBlock, blockHash, stateRoot);
      await tx.wait();
    }

    const header: CheckpointHeaderStruct = {
      batchId: this.nextBatchId,
      previousBatchId: this.nextBatchId > 1n ? this.nextBatchId - 1n : 0n,
      chainId: 138n,
      checkpointBlock: BigInt(checkpointBlock),
      startBlock: BigInt(Math.min(...leaves.map((l) => l.blockNumber))),
      endBlock: BigInt(checkpointBlock),
      blockHash,
      stateRoot,
      paymentsRoot: root,
      receiptsRoot: receiptsRootHash,
      txCount: partial && leaves.length < cfg.batchSize ? leaves.length : cfg.batchSize,
      flags: partial && leaves.length < cfg.batchSize ? 1 : 0,
      submittedAt: 0n,
      submitter: ethers.ZeroAddress,
      contentURI: ethers.ZeroHash,
    };

    const payload: BatchPayload = {
      batchId: this.nextBatchId.toString(),
      chainId: 138,
      checkpointBlock,
      paymentsRoot: root,
      leaves: leaves.map((l) => ({
        txHash: l.txHash,
        from: l.from,
        to: l.to,
        value: l.value.toString(),
        nativeValueWei: l.value.toString(),
        onChainValueWei: effectiveValue(l).toString(),
        blockNumber: l.blockNumber,
        blockTimestamp: l.blockTimestamp,
        gasUsed: l.gasUsed.toString(),
        success: l.success,
        ...(l.token
          ? {
              token: l.token,
              tokenSymbol: l.tokenSymbol,
              tokenDecimals: l.tokenDecimals,
              tokenValue: l.tokenValue?.toString(),
              tokenLogIndex: l.tokenLogIndex,
            }
          : {}),
        ...(l.valueUsd ? { valueUsd: l.valueUsd } : {}),
        ...(l.nativeValueUsd ? { nativeValueUsd: l.nativeValueUsd } : {}),
        ...(l.tokenValueUsd ? { tokenValueUsd: l.tokenValueUsd } : {}),
        ...(l.nativePriceUsd != null ? { nativePriceUsd: l.nativePriceUsd } : {}),
        ...(l.tokenPriceUsd != null ? { tokenPriceUsd: l.tokenPriceUsd } : {}),
        ...(l.priceSource ? { priceSource: l.priceSource } : {}),
        ...(l.priceEffectiveTimestamp ? { priceEffectiveTimestamp: l.priceEffectiveTimestamp } : {}),
        ...(l.totalTransfersUsd ? { totalTransfersUsd: l.totalTransfersUsd } : {}),
        ...(l.transfers?.length ? { transfers: l.transfers } : {}),
        ...(l.logCount != null ? { logCount: l.logCount } : {}),
        ...(l.receiptHash ? { receiptHash: l.receiptHash } : {}),
        ...(l.iso20022 ? { iso20022: l.iso20022 } : {}),
      })),
      submittedAt: new Date().toISOString(),
      ...(cfg.isoEnrichEnabled
        ? { iso20022Schema: 'canonical-v1', isoMsgTypeDefault: 'chain138.synthetic' }
        : {}),
      ...(cfg.usdEnrichEnabled
        ? {
            batchTotalUsd:
              sumUsdStrings(leaves.map((l) => l.totalTransfersUsd ?? l.valueUsd)) ?? '0.000000',
            usdEnrichedAt: new Date().toISOString(),
          }
        : {}),
    };
    const contentURI = await publishBatchPayload(this.nextBatchId, payload);
    header.contentURI = contentURI;

    const validatorSignatures = cfg.requireValidatorSigs
      ? await signBatchAttestation(this.mainnetWallet, cfg.checkpointProxy, header)
      : '0x01';

    const txHashes = cfg.submitMode === 'commitment' ? [] : leaves.map((l) => l.txHash);
    const extensionData = AdminCheckpointIngress.extensionDataForMode(cfg.submitMode, leaves);

    const { hash } = await this.ingress.submit({
      header,
      leaves,
      txHashes,
      extensionData,
      contentURI,
      validatorSignatures,
    });

  const batchIdSubmitted = this.nextBatchId;
    console.log(`Submitted batch ${batchIdSubmitted} (${leaves.length} txs) mode=${cfg.submitMode} tx=${hash}`);

    if (cfg.dualWriteV1Mirror) {
      try {
        const mirrorTx = await dualWriteV1Mirror(
          this.mainnetWallet,
          cfg.transactionMirrorMainnet,
          leaves
        );
        if (mirrorTx) console.log(`  v1 TransactionMirror dual-write tx=${mirrorTx}`);
      } catch (e) {
        console.error('  v1 TransactionMirror dual-write failed', e);
      }
    }

    if (cfg.recordAddressActivity && cfg.addressActivityRegistry) {
      try {
        const activityTx = await recordActivityBatch(
          this.mainnetWallet,
          cfg.addressActivityRegistry,
          batchIdSubmitted,
          leaves
        );
        if (activityTx) console.log(`  AddressActivityRegistry tx=${activityTx}`);
      } catch (e) {
        console.error('  AddressActivityRegistry record failed', e);
      }
    }

    if (cfg.recordIsoAttestation && cfg.addressActivityRegistryV2) {
      try {
        const isoTx = await recordIsoAttestationBatch(
          this.mainnetWallet,
          cfg.addressActivityRegistryV2,
          batchIdSubmitted,
          leaves
        );
        if (isoTx) console.log(`  AddressActivityRegistryV2 tx=${isoTx}`);
      } catch (e) {
        console.error('  AddressActivityRegistryV2 record failed', e);
      }
    }

    if (cfg.isoEnrichEnabled) {
      try {
        const stored = persistIso20022ForLeaves(leaves, batchIdSubmitted);
        if (stored.length) console.log(`  ISO20022 OMNL store: ${stored.length} message(s)`);
      } catch (e) {
        console.error('  ISO20022 local store failed', e);
      }
    }

    if (cfg.surfaceParticipants && cfg.participantSurface) {
      try {
        const surfaceTx = await surfaceParticipantsOnMainnet(
          this.mainnetWallet,
          cfg.participantSurface,
          batchIdSubmitted,
          leaves
        );
        if (surfaceTx) console.log(`  Chain138ParticipantSurface tx=${surfaceTx}`);
      } catch (e) {
        console.error('  Chain138ParticipantSurface failed', e);
      }
    }

    if (cfg.surfaceTopLevelZeroEth) {
      try {
        const n = await surfaceTopLevelZeroEth(this.mainnetWallet, leaves, batchIdSubmitted);
        if (n > 0) console.log(`  Top-level 0 ETH surface txs=${n} (Etherscan Transactions tab)`);
      } catch (e) {
        console.error('  Top-level 0 ETH surface failed', e);
      }
    }

    this.nextBatchId += 1n;
    this.schedulePartialFlush();
  }
}

new CheckpointAggregator().start().catch((e) => {
  console.error(e);
  process.exit(1);
});
