import { IDepositScannerPort, IRegisteredAddressPort, DepositScanRequest } from "../providers/ProviderPorts";
import { ReconciliationResult } from "../types";
import { IDepositQueueStore, IReconciliationCursorStore } from "./StateStoreContracts";

export class DepositReconciliationService {
  constructor(
    private readonly addressRegistry: IRegisteredAddressPort,
    private readonly depositScanner: IDepositScannerPort,
    private readonly depositQueueService: IDepositQueueStore,
    private readonly reconciliationStoreService: IReconciliationCursorStore,
    private readonly requestBatchSize: number = 100,
    private readonly lookbackBlocks: number = 5000
  ) {}

  async reconcile(): Promise<ReconciliationResult> {
    if (!this.depositScanner.isEnabled()) {
      return { scanned: 0, missing: 0, duplicates: 0, unsupported: 0, overflow: 0 };
    }

    const trackedAddresses = await this.addressRegistry.getAllTrackedAddresses();
    const chainTypes = Array.from(new Set(trackedAddresses.map((entry) => entry.chainType)));
    const fromBlockByChainType = await this.resolveFromBlocks(chainTypes);

    const requests: DepositScanRequest[] = trackedAddresses.map((entry) => ({
      chainType: entry.chainType,
      address: entry.address,
      fromBlock: fromBlockByChainType.get(entry.chainType) || "0x0",
    }));

    let scanned = 0;
    let missing = 0;
    let duplicates = 0;
    let overflow = 0;
    const maxSeenBlockByChainType = new Map<string, number>();

    for (let index = 0; index < requests.length; index += this.requestBatchSize) {
      const batch = requests.slice(index, index + this.requestBatchSize);
      const results = await this.depositScanner.scanDeposits(batch);
      const depositsToQueue = [];

      for (const request of batch) {
        const key = this.depositScanner.resultKey(request.chainType, request.address);
        const events = results.get(key) || [];

        for (const event of events) {
          scanned += 1;

          const blockNumber = event.block_number || 0;
          const previousMax = maxSeenBlockByChainType.get(request.chainType) || 0;
          if (blockNumber > previousMax) {
            maxSeenBlockByChainType.set(request.chainType, blockNumber);
          }

          if (await this.depositQueueService.hasSeen(event.idempotency_key)) {
            duplicates += 1;
            continue;
          }

          depositsToQueue.push(event);
        }
      }

      const enqueueResult = await this.depositQueueService.enqueue(depositsToQueue);
      missing += enqueueResult.accepted;
      duplicates += enqueueResult.duplicates;
      overflow += Math.max(0, depositsToQueue.length - enqueueResult.accepted - enqueueResult.duplicates);
    }

    for (const [chainType, blockNumber] of maxSeenBlockByChainType.entries()) {
      await this.reconciliationStoreService.upsertCursor(chainType, this.toHexBlock(blockNumber + 1));
    }

    console.info("deposit reconciliation complete", {
      scanned,
      missing,
      duplicates,
      tracked_addresses: requests.length,
    });

    return { scanned, missing, duplicates, unsupported: 0, overflow };
  }

  private async resolveFromBlocks(chainTypes: string[]): Promise<Map<string, string>> {
    const result = new Map<string, string>();

    for (const chainType of chainTypes) {
      const existingCursor = await this.reconciliationStoreService.getCursor(chainType);
      if (existingCursor) {
        result.set(chainType, existingCursor.from_block);
        continue;
      }

      const latestBlock = await this.depositScanner.getBlockNumber(chainType);
      result.set(chainType, this.toHexBlock(latestBlock - this.lookbackBlocks));
    }

    return result;
  }

  private toHexBlock(value: number): string {
    return `0x${Math.max(0, value).toString(16)}`;
  }
}
