import { ethers, type Log } from 'ethers';
import { checkpointIndexerConfig as cfg } from './config';
import { decodePaymentAttestedLogs, PAYMENT_ATTESTED_TOPIC0 } from './iso20022LogDecode';
import {
  decodeChain138ActivityNotifiedLogs,
  CHAIN138_ACTIVITY_NOTIFIED_TOPIC0,
} from './participantSurfaceLogDecode';

const TRANSACTION_MIRRORED_TOPIC0 =
  '0xc25ce5062c7e42c68fa21fe088d21e609cc0c61b8bec3180681363bb5cf02a9e';
const PARTICIPANT_CREDITED_TOPIC0 =
  '0x853ebad0824f583553bcb4360a0df947d39654e86aba8d7c0625499cbf09f0d5';
const PARTICIPANT_DEBITED_TOPIC0 =
  '0xf122cad7c8ada37ddd6b6ddca0982b9f7be689cde15eedfd767cc207fa7dc2c8';
const CHECKPOINT_SUBMITTED_TOPIC0 = ethers.id(
  'CheckpointSubmitted(uint64,uint256,bytes32,uint16,uint32,uint64,address,bytes32)'
);

export type MainnetAttestationLink = {
  layer: string;
  label: string;
  mainnetTxHash: string;
  mainnetExplorerUrl: string;
  contractAddress?: string;
  logIndex?: number;
  meta?: Record<string, string>;
};

type BatchMainnetMeta = {
  hubTx?: string;
  mirrorTx?: string;
  activityRegistryV1Tx?: string;
  activityRegistryV2Tx?: string;
  participantSurfaceTx?: string;
  participantTopLevelTxs?: string[];
};

const META_LAYER_MAP: Array<{ key: keyof BatchMainnetMeta; layer: string; label: string }> = [
  { key: 'hubTx', layer: 'checkpointHub', label: 'Checkpoint batch (hub)' },
  { key: 'mirrorTx', layer: 'transactionMirror', label: 'Transaction mirror' },
  { key: 'activityRegistryV1Tx', layer: 'activityRegistryV1', label: 'Activity registry V1' },
  { key: 'activityRegistryV2Tx', layer: 'activityRegistryV2', label: 'ISO-20022 PaymentAttested (V2)' },
  { key: 'participantSurfaceTx', layer: 'participantSurface', label: 'Participant surface' },
];

export function linksFromBatchMeta(
  raw: { mainnetAttestation?: BatchMainnetMeta } | null
): MainnetAttestationLink[] {
  const meta = raw?.mainnetAttestation;
  if (!meta) return [];
  const links: MainnetAttestationLink[] = [];
  for (const row of META_LAYER_MAP) {
    const hash = meta[row.key];
    if (typeof hash === 'string' && /^0x[a-fA-F0-9]{64}$/.test(hash)) {
      links.push({
        layer: row.layer,
        label: row.label,
        mainnetTxHash: hash,
        mainnetExplorerUrl: etherscanTx(hash),
      });
    }
  }
  if (Array.isArray(meta.participantTopLevelTxs)) {
    for (const hash of meta.participantTopLevelTxs) {
      if (!/^0x[a-fA-F0-9]{64}$/.test(hash)) continue;
      links.push({
        layer: 'participantWallet',
        label: 'Participant wallet (0 ETH attestation tx)',
        mainnetTxHash: hash,
        mainnetExplorerUrl: etherscanTx(hash),
      });
    }
  }
  return links;
}

function etherscanTx(hash: string): string {
  return `https://etherscan.io/tx/${hash}`;
}

function logRows(logs: Log[]) {
  return logs.map((log) => ({
    topics: log.topics as string[],
    transactionHash: log.transactionHash,
    blockNumber: log.blockNumber.toString(),
    logIndex: log.index.toString(),
    data: log.data,
  }));
}

function uniqLinks(links: MainnetAttestationLink[]): MainnetAttestationLink[] {
  const seen = new Set<string>();
  return links.filter((l) => {
    const k = `${l.layer}:${l.mainnetTxHash}:${l.logIndex ?? ''}`;
    if (seen.has(k)) return false;
    seen.add(k);
    return true;
  });
}

const LOG_LOOKBACK_BLOCKS = parseInt(process.env.CHECKPOINT_MAINNET_LOGS_LOOKBACK || '120000', 10);

async function resolveFromBlock(
  provider: ethers.JsonRpcProvider,
  batchId?: string
): Promise<number> {
  const head = await provider.getBlockNumber();
  const fallback = Math.max(0, head - LOG_LOOKBACK_BLOCKS);
  if (!batchId || batchId === '0' || !cfg.checkpointProxy) return fallback;
  try {
    const batchTopic = ethers.zeroPadValue(ethers.toBeHex(BigInt(batchId)), 32);
    const hubLogs = await provider.getLogs({
      address: cfg.checkpointProxy,
      fromBlock: fallback,
      toBlock: 'latest',
      topics: [CHECKPOINT_SUBMITTED_TOPIC0, batchTopic],
    });
    if (hubLogs.length) {
      return Math.max(0, hubLogs[0].blockNumber - 10);
    }
  } catch {
    /* use fallback */
  }
  return fallback;
}

export async function resolveTxMainnetLinks(
  provider: ethers.JsonRpcProvider,
  txHash: string,
  batchId?: string
): Promise<MainnetAttestationLink[]> {
  const links: MainnetAttestationLink[] = [];
  const want = txHash.toLowerCase();
  const txTopic = ethers.zeroPadValue(txHash, 32);
  const fromBlock = await resolveFromBlock(provider, batchId);

  if (batchId && batchId !== '0' && cfg.checkpointProxy) {
    try {
      const batchTopic = ethers.zeroPadValue(ethers.toBeHex(BigInt(batchId)), 32);
      const hubLogs = await provider.getLogs({
        address: cfg.checkpointProxy,
        fromBlock,
        toBlock: 'latest',
        topics: [CHECKPOINT_SUBMITTED_TOPIC0, batchTopic],
      });
      for (const log of hubLogs) {
        links.push({
          layer: 'checkpointHub',
          label: `Checkpoint batch #${batchId}`,
          mainnetTxHash: log.transactionHash,
          mainnetExplorerUrl: etherscanTx(log.transactionHash),
          contractAddress: cfg.checkpointProxy,
          logIndex: log.index,
        });
      }
    } catch {
      /* non-fatal — RPC may prune old logs */
    }
  }

  try {
    const mirrorLogs = await provider.getLogs({
      address: cfg.legacyMirror,
      fromBlock,
      toBlock: 'latest',
      topics: [TRANSACTION_MIRRORED_TOPIC0, txTopic],
    });
    for (const log of mirrorLogs) {
      links.push({
        layer: 'transactionMirror',
        label: 'Transaction mirror',
        mainnetTxHash: log.transactionHash,
        mainnetExplorerUrl: etherscanTx(log.transactionHash),
        contractAddress: cfg.legacyMirror,
        logIndex: log.index,
      });
    }
  } catch {
    /* non-fatal */
  }

  if (cfg.addressActivityRegistry) {
    for (const [topic0, role] of [
      [PARTICIPANT_CREDITED_TOPIC0, 'credited'],
      [PARTICIPANT_DEBITED_TOPIC0, 'debited'],
    ] as const) {
      try {
        const regLogs = await provider.getLogs({
          address: cfg.addressActivityRegistry,
          fromBlock,
          toBlock: 'latest',
          topics: [topic0, txTopic],
        });
        for (const log of regLogs) {
          links.push({
            layer: 'activityRegistryV1',
            label: `Activity registry V1 (${role})`,
            mainnetTxHash: log.transactionHash,
            mainnetExplorerUrl: etherscanTx(log.transactionHash),
            contractAddress: cfg.addressActivityRegistry,
            logIndex: log.index,
            meta: { role },
          });
        }
      } catch {
        /* non-fatal */
      }
    }
  }

  if (cfg.addressActivityRegistryV2) {
    try {
      const v2Logs = await provider.getLogs({
        address: cfg.addressActivityRegistryV2,
        fromBlock,
        toBlock: 'latest',
        topics: [PAYMENT_ATTESTED_TOPIC0, txTopic],
      });
      const decoded = decodePaymentAttestedLogs({ status: '1', result: logRows(v2Logs) });
      for (const row of decoded) {
        if (row.chain138TxHash.toLowerCase() !== want) continue;
        links.push({
          layer: 'activityRegistryV2',
          label: 'ISO-20022 PaymentAttested (V2)',
          mainnetTxHash: row.mainnetTxHash,
          mainnetExplorerUrl: row.mainnetExplorerUrl,
          contractAddress: cfg.addressActivityRegistryV2,
          logIndex: row.logIndex ? parseInt(row.logIndex, 10) : undefined,
          meta: { instructionId: row.instructionId, uetr: row.uetr },
        });
      }
    } catch {
      /* non-fatal */
    }
  }

  if (cfg.participantSurface) {
    try {
      const surfaceLogs = await provider.getLogs({
        address: cfg.participantSurface,
        fromBlock,
        toBlock: 'latest',
        topics: [CHAIN138_ACTIVITY_NOTIFIED_TOPIC0, null, txTopic],
      });
      const decoded = decodeChain138ActivityNotifiedLogs({
        status: '1',
        result: logRows(surfaceLogs),
      });
      for (const row of decoded) {
        if (row.chain138TxHash.toLowerCase() !== want) continue;
        links.push({
          layer: 'participantSurface',
          label: 'Participant surface notification',
          mainnetTxHash: row.mainnetTxHash,
          mainnetExplorerUrl: row.mainnetExplorerUrl,
          contractAddress: cfg.participantSurface,
          logIndex: row.logIndex ? parseInt(row.logIndex, 10) : undefined,
          meta: {
            participant: row.participant,
            direction: String(row.direction),
          },
        });
      }
    } catch {
      /* non-fatal */
    }
  }

  return uniqLinks(links);
}
