/**
 * Indexes custom CCIP relay lane messages for Chain 138 ↔ Monad (143).
 * Correlates CrossChainTransferInitiated (source) with CrossChainTransferCompleted (destination).
 */

import { ethers } from 'ethers';
import { resolveChain138RpcUrl } from '../config/chain138-rpc';
import {
  loadMonadRelayLaneConfig,
  MonadRelayLaneConfig,
  MonadRelaySeedMessage,
  resolveMonadRpcUrl,
} from '../config/monad-relay-lane';
import { logger } from '../utils/logger';

const CCIP_TRANSFER_ABI = [
  'event CrossChainTransferInitiated(bytes32 indexed messageId, address indexed sender, uint64 indexed destinationChainSelector, address recipient, uint256 amount, uint256 nonce)',
  'event CrossChainTransferCompleted(bytes32 indexed messageId, uint64 indexed sourceChainSelector, address indexed recipient, uint256 amount)',
  'function processedTransfers(bytes32 messageId) view returns (bool)',
];

const CROSS_CHAIN_QUERY_FALLBACK_BLOCK_SPAN = Math.max(
  1,
  Number(process.env.CROSS_CHAIN_QUERY_FALLBACK_BLOCK_SPAN || 5000)
);

export type RelayLaneDirection = '138-to-monad' | 'monad-to-138';
export type RelayLaneStatus = 'initiated' | 'relaying' | 'delivered' | 'failed';

export interface RelayLaneInitiatedEvent {
  messageId: string;
  direction: RelayLaneDirection;
  sourceChainId: number;
  destChainId: number;
  destSelector: string;
  sourceTxHash: string;
  sourceBlockNumber: number;
  sender: string;
  recipient: string;
  amountWei: string;
  tokenSymbol: string;
}

export interface RelayLaneCompletedEvent {
  messageId: string;
  direction: RelayLaneDirection;
  destChainId: number;
  destTxHash: string;
  destBlockNumber: number;
  recipient: string;
  amountWei: string;
}

export interface RelayLaneMessage {
  messageId: string;
  laneId: string;
  direction: RelayLaneDirection;
  status: RelayLaneStatus;
  sourceChainId: number;
  destChainId: number;
  tokenSymbol: string;
  amountWei: string;
  sender?: string;
  recipient?: string;
  sourceTxHash?: string;
  sourceBlockNumber?: number;
  relayTxHash?: string;
  destTxHash?: string;
  destBlockNumber?: number;
  explorerLinks: {
    sourceTx?: string;
    relayTx?: string;
    destTx?: string;
  };
}

export interface RelayLaneReport {
  generatedAt: string;
  laneId: string;
  description: string;
  integration: {
    ccipExplorer: boolean;
    creRequired: boolean;
    relayService?: string;
    notes?: string;
  };
  chains: {
    chain138: { chainId: number; selector: string; explorer: string };
    monad: { chainId: number; selector: string; explorer: string };
  };
  contracts: {
    chain138SendBridge: string;
    chain138RelayBridge: string;
    monadSendBridge: string;
    monadRelayBridge: string;
  };
  messages: RelayLaneMessage[];
  stats: {
    total: number;
    delivered: number;
    pending: number;
  };
}

function txExplorer(base: string, txHash?: string): string | undefined {
  if (!txHash?.startsWith('0x')) return undefined;
  const trimmed = base.replace(/\/$/, '');
  return `${trimmed}/tx/${txHash}`;
}

function isRpcRangeLimitError(error: unknown): boolean {
  const parts = [
    typeof error === 'object' && error ? (error as { message?: string }).message : '',
    typeof error === 'object' && error ? (error as { shortMessage?: string }).shortMessage : '',
    typeof error === 'object' && error
      ? ((error as { error?: { message?: string } }).error?.message ?? '')
      : '',
  ];
  return parts.some((part) => typeof part === 'string' && /range limit|exceeds maximum rpc range/i.test(part));
}

async function queryFilterWithRangeFallback(
  contract: ethers.Contract,
  filter: ReturnType<ethers.Contract['filters'][string]>,
  fromBlock: number,
  toBlock: number
): Promise<ethers.EventLog[]> {
  try {
    return (await contract.queryFilter(filter as never, fromBlock, toBlock)) as ethers.EventLog[];
  } catch (error) {
    if (!isRpcRangeLimitError(error) || fromBlock >= toBlock) throw error;
  }

  const logs: ethers.EventLog[] = [];
  const chunkSpan = Math.min(CROSS_CHAIN_QUERY_FALLBACK_BLOCK_SPAN, toBlock - fromBlock + 1);
  for (let start = fromBlock; start <= toBlock; start += chunkSpan) {
    const end = Math.min(start + chunkSpan - 1, toBlock);
    const chunk = await queryFilterWithRangeFallback(contract, filter, start, end);
    logs.push(...chunk);
  }
  return logs;
}

async function fetchInitiated(
  provider: ethers.JsonRpcProvider,
  bridgeAddress: string,
  destSelector: string,
  direction: RelayLaneDirection,
  sourceChainId: number,
  destChainId: number,
  tokenSymbol: string,
  fromBlock: number,
  toBlock: number
): Promise<RelayLaneInitiatedEvent[]> {
  const events: RelayLaneInitiatedEvent[] = [];
  try {
    const contract = new ethers.Contract(bridgeAddress, CCIP_TRANSFER_ABI, provider);
    const filter = contract.filters.CrossChainTransferInitiated();
    const logs = await queryFilterWithRangeFallback(contract, filter, fromBlock, toBlock);

    for (const log of logs) {
      const args = (log as ethers.EventLog).args as unknown as {
        messageId: string;
        sender: string;
        destinationChainSelector: bigint;
        recipient: string;
        amount: bigint;
      };
      if (args.destinationChainSelector?.toString() !== destSelector) continue;

      events.push({
        messageId: args.messageId,
        direction,
        sourceChainId,
        destChainId,
        destSelector,
        sourceTxHash: log.transactionHash,
        sourceBlockNumber: log.blockNumber,
        sender: args.sender,
        recipient: args.recipient,
        amountWei: args.amount?.toString() ?? '0',
        tokenSymbol,
      });
    }
  } catch (err) {
    logger.warn(`Relay lane indexer: initiated events for ${bridgeAddress} failed:`, err);
  }
  return events;
}

async function fetchCompleted(
  provider: ethers.JsonRpcProvider,
  bridgeAddress: string,
  sourceSelector: string,
  direction: RelayLaneDirection,
  destChainId: number,
  fromBlock: number,
  toBlock: number
): Promise<RelayLaneCompletedEvent[]> {
  const events: RelayLaneCompletedEvent[] = [];
  try {
    const contract = new ethers.Contract(bridgeAddress, CCIP_TRANSFER_ABI, provider);
    const filter = contract.filters.CrossChainTransferCompleted();
    const logs = await queryFilterWithRangeFallback(contract, filter, fromBlock, toBlock);

    for (const log of logs) {
      const args = (log as ethers.EventLog).args as unknown as {
        messageId: string;
        sourceChainSelector: bigint;
        recipient: string;
        amount: bigint;
      };
      if (args.sourceChainSelector?.toString() !== sourceSelector) continue;

      events.push({
        messageId: args.messageId,
        direction,
        destChainId,
        destTxHash: log.transactionHash,
        destBlockNumber: log.blockNumber,
        recipient: args.recipient,
        amountWei: args.amount?.toString() ?? '0',
      });
    }
  } catch (err) {
    logger.warn(`Relay lane indexer: completed events for ${bridgeAddress} failed:`, err);
  }
  return events;
}

/** Merge initiated + completed + seed rows into explorer-facing messages. Exported for tests. */
export function mergeRelayLaneMessages(
  config: MonadRelayLaneConfig,
  initiated: RelayLaneInitiatedEvent[],
  completed: RelayLaneCompletedEvent[],
  seeds: MonadRelaySeedMessage[] = []
): RelayLaneMessage[] {
  const byId = new Map<string, RelayLaneMessage>();

  const upsert = (messageId: string, patch: Partial<RelayLaneMessage> & Pick<RelayLaneMessage, 'direction'>): void => {
    const key = messageId.toLowerCase();
    const existing = byId.get(key);
    const sourceExplorer =
      patch.direction === '138-to-monad' ? config.chain138.explorer : config.monad.explorer;
    const destExplorer =
      patch.direction === '138-to-monad' ? config.monad.explorer : config.chain138.explorer;

    const merged: RelayLaneMessage = {
      messageId,
      laneId: config.laneId,
      direction: patch.direction,
      status: patch.status ?? existing?.status ?? 'initiated',
      sourceChainId: patch.sourceChainId ?? existing?.sourceChainId ?? (patch.direction === '138-to-monad' ? 138 : 143),
      destChainId: patch.destChainId ?? existing?.destChainId ?? (patch.direction === '138-to-monad' ? 143 : 138),
      tokenSymbol: patch.tokenSymbol ?? existing?.tokenSymbol ?? (patch.direction === '138-to-monad' ? 'WETH' : 'WMON'),
      amountWei: patch.amountWei ?? existing?.amountWei ?? '0',
      sender: patch.sender ?? existing?.sender,
      recipient: patch.recipient ?? existing?.recipient,
      sourceTxHash: patch.sourceTxHash ?? existing?.sourceTxHash,
      sourceBlockNumber: patch.sourceBlockNumber ?? existing?.sourceBlockNumber,
      relayTxHash: patch.relayTxHash ?? existing?.relayTxHash,
      destTxHash: patch.destTxHash ?? existing?.destTxHash,
      destBlockNumber: patch.destBlockNumber ?? existing?.destBlockNumber,
      explorerLinks: {
        sourceTx: txExplorer(sourceExplorer, patch.sourceTxHash ?? existing?.sourceTxHash),
        relayTx: txExplorer(destExplorer, patch.relayTxHash ?? existing?.relayTxHash ?? patch.destTxHash ?? existing?.destTxHash),
        destTx: txExplorer(destExplorer, patch.destTxHash ?? existing?.destTxHash),
      },
    };
    byId.set(key, merged);
  };

  for (const e of initiated) {
    upsert(e.messageId, {
      direction: e.direction,
      status: 'initiated',
      sourceChainId: e.sourceChainId,
      destChainId: e.destChainId,
      tokenSymbol: e.tokenSymbol,
      amountWei: e.amountWei,
      sender: e.sender,
      recipient: e.recipient,
      sourceTxHash: e.sourceTxHash,
      sourceBlockNumber: e.sourceBlockNumber,
    });
  }

  for (const e of completed) {
    upsert(e.messageId, {
      direction: e.direction,
      status: 'delivered',
      destChainId: e.destChainId,
      amountWei: e.amountWei,
      recipient: e.recipient,
      relayTxHash: e.destTxHash,
      destTxHash: e.destTxHash,
      destBlockNumber: e.destBlockNumber,
    });
  }

  for (const seed of seeds) {
    upsert(seed.messageId, {
      direction: seed.direction,
      status: seed.status,
      tokenSymbol: seed.tokenSymbol,
      amountWei: seed.amountWei,
      sender: seed.sender,
      recipient: seed.recipient,
      sourceTxHash: seed.sourceTxHash,
      relayTxHash: seed.relayTxHash,
      destTxHash: seed.relayTxHash,
    });
  }

  return [...byId.values()].sort((a, b) => {
    const blockA = a.sourceBlockNumber ?? a.destBlockNumber ?? 0;
    const blockB = b.sourceBlockNumber ?? b.destBlockNumber ?? 0;
    return blockB - blockA;
  });
}

export interface BuildRelayLaneReportOptions {
  fromBlockDays?: number;
  limit?: number;
  skipRpc?: boolean;
}

export async function buildRelayLaneReport(
  options: BuildRelayLaneReportOptions = {}
): Promise<RelayLaneReport> {
  const config = loadMonadRelayLaneConfig();
  const fromBlockDays = options.fromBlockDays ?? 30;
  const limit = options.limit ?? 100;
  const seeds = config.seedMessages ?? [];

  let initiated: RelayLaneInitiatedEvent[] = [];
  let completed: RelayLaneCompletedEvent[] = [];

  if (!options.skipRpc) {
    try {
      const chain138Rpc = resolveChain138RpcUrl();
      const monadRpc = resolveMonadRpcUrl(config);
      const chain138Provider = new ethers.JsonRpcProvider(chain138Rpc);
      const monadProvider = new ethers.JsonRpcProvider(monadRpc);

      const [chain138Block, monadBlock] = await Promise.all([
        chain138Provider.getBlockNumber(),
        monadProvider.getBlockNumber(),
      ]);

      const chain138From = Math.max(0, chain138Block - Math.floor((fromBlockDays * 86400) / 5));
      const monadFrom = Math.max(0, monadBlock - Math.floor((fromBlockDays * 86400) / 1));

      const [
        initiated138,
        initiatedMonad,
        completedMonad,
        completed138,
      ] = await Promise.all([
        fetchInitiated(
          chain138Provider,
          config.chain138.sendBridgeWeth9,
          config.monad.selector,
          '138-to-monad',
          config.chain138.chainId,
          config.monad.chainId,
          'WETH',
          chain138From,
          chain138Block
        ),
        fetchInitiated(
          monadProvider,
          config.monad.sendBridgeWeth9,
          config.chain138.selector,
          'monad-to-138',
          config.monad.chainId,
          config.chain138.chainId,
          'WMON',
          monadFrom,
          monadBlock
        ),
        fetchCompleted(
          monadProvider,
          config.monad.relayBridgeOutbound ?? '',
          config.chain138.selector,
          '138-to-monad',
          config.monad.chainId,
          monadFrom,
          monadBlock
        ),
        fetchCompleted(
          chain138Provider,
          config.chain138.relayBridgeInbound ?? '',
          config.monad.selector,
          'monad-to-138',
          config.chain138.chainId,
          chain138From,
          chain138Block
        ),
      ]);

      initiated = [...initiated138, ...initiatedMonad];
      completed = [...completedMonad, ...completed138];
    } catch (err) {
      logger.warn('Relay lane indexer: RPC scan failed, returning seed messages only:', err);
    }
  }

  const messages = mergeRelayLaneMessages(config, initiated, completed, seeds).slice(0, limit);
  const delivered = messages.filter((m) => m.status === 'delivered').length;

  return {
    generatedAt: new Date().toISOString(),
    laneId: config.laneId,
    description: config.description,
    integration: {
      ccipExplorer: config.integration?.ccipExplorer ?? false,
      creRequired: config.integration?.creRequired ?? false,
      relayService: config.integration?.relayService,
      notes: config.integration?.creNotes,
    },
    chains: {
      chain138: {
        chainId: config.chain138.chainId,
        selector: config.chain138.selector,
        explorer: config.chain138.explorer,
      },
      monad: {
        chainId: config.monad.chainId,
        selector: config.monad.selector,
        explorer: config.monad.explorer,
      },
    },
    contracts: {
      chain138SendBridge: config.chain138.sendBridgeWeth9,
      chain138RelayBridge: config.chain138.relayBridgeInbound ?? '',
      monadSendBridge: config.monad.sendBridgeWeth9,
      monadRelayBridge: config.monad.relayBridgeOutbound ?? '',
    },
    messages,
    stats: {
      total: messages.length,
      delivered,
      pending: messages.length - delivered,
    },
  };
}
