import { ethers } from 'ethers';
import type { CanonicalPaymentMessage } from '@dbis/checkpoint-core';
import type { ListenerConfig, ResourceRegistry } from '../types';

const GATEWAY_ABI = [
  'function submitInbound((bytes32 instructionId, bytes32 endToEndIdHash, bytes32 uetr, bytes32 payloadHash, bytes32 debtorRefHash, bytes32 creditorRefHash, bytes32 purposeHash, bytes32 authAttestationHash, uint8 msgTypeCode, address token, uint256 amount, bytes3 currencyCode) m) external',
  'function processedInstructions(bytes32) view returns (bool)',
];

const RPC_TIMEOUT_MS = 15_000;

async function withRpcTimeout<T>(label: string, fn: () => Promise<T>): Promise<T> {
  let timer: ReturnType<typeof setTimeout> | undefined;
  try {
    return await Promise.race([
      fn(),
      new Promise<T>((_, reject) => {
        timer = setTimeout(() => reject(new Error(`${label} timed out after ${RPC_TIMEOUT_MS}ms`)), RPC_TIMEOUT_MS);
      }),
    ]);
  } finally {
    if (timer) clearTimeout(timer);
  }
}

function toBytes3(code: string): string {
  const b = new Uint8Array(3);
  const enc = new TextEncoder().encode(code.slice(0, 3).toUpperCase());
  b.set(enc.slice(0, 3));
  return ethers.hexlify(b);
}

function resolveTokenAddress(
  canonical: CanonicalPaymentMessage,
  registry: ResourceRegistry
): string {
  if (canonical.tokenAddress && ethers.isAddress(canonical.tokenAddress)) {
    return ethers.getAddress(canonical.tokenAddress);
  }
  for (const sym of [canonical.currencyCode, 'cUSDC', 'cUSDT']) {
    for (const [addr, meta] of registry.addresses.entries()) {
      if (meta.symbol?.toUpperCase() === sym.toUpperCase() && meta.chainId === 138) {
        return ethers.getAddress(addr);
      }
    }
  }
  return ethers.ZeroAddress;
}

export function buildGatewayMessage(
  canonical: CanonicalPaymentMessage,
  registry: ResourceRegistry
) {
  return {
    instructionId: canonical.instructionIdBytes32,
    endToEndIdHash: canonical.endToEndIdHash,
    uetr: canonical.uetrBytes32,
    payloadHash: canonical.payloadHash,
    debtorRefHash: canonical.debtorRefHash,
    creditorRefHash: canonical.creditorRefHash,
    purposeHash: canonical.purposeHash,
    authAttestationHash: canonical.authAttestationHash ?? ethers.ZeroHash,
    msgTypeCode: canonical.msgTypeCode,
    token: resolveTokenAddress(canonical, registry),
    amount: BigInt(canonical.amountSmallestUnit || '0'),
    currencyCode: toBytes3(canonical.currencyCode || 'USD'),
  };
}

function shouldExecuteSubmit(gwCfg: NonNullable<ListenerConfig['outbound']>['intakeGateway']): boolean {
  const explicit = process.env[gwCfg.executeEnv];
  if (explicit === '1') return true;
  if (explicit === '0') return false;
  if (gwCfg.autoExecuteWhenKeyPresent) {
    return Boolean(process.env[gwCfg.privateKeyEnv] ?? process.env.PRIVATE_KEY);
  }
  return !gwCfg.dryRunDefault;
}

export async function submitToIntakeGateway(
  config: ListenerConfig,
  registry: ResourceRegistry,
  canonical: CanonicalPaymentMessage
): Promise<{ dryRun: boolean; txHash?: string; error?: string; gateway?: string; prepared?: boolean }> {
  const gwCfg = config.outbound?.intakeGateway;
  if (!gwCfg?.enabled) return { dryRun: true };

  const gateway = process.env[gwCfg.gatewayAddressEnv] ?? gwCfg.gatewayAddressFallback;
  const rpc = process.env[gwCfg.rpcUrlEnv] ?? process.env.RPC_URL_138 ?? 'http://127.0.0.1:8545';
  const pk = process.env[gwCfg.privateKeyEnv] ?? process.env.PRIVATE_KEY ?? '';
  const execute = shouldExecuteSubmit(gwCfg);

  const message = buildGatewayMessage(canonical, registry);

  if (
    canonical.msgTypeCode === 5 &&
    process.env.KTT_REQUIRE_ATTESTATION === '1' &&
    (message.authAttestationHash === ethers.ZeroHash || !message.authAttestationHash)
  ) {
    return { dryRun: false, gateway, error: 'KTT requires authAttestationHash' };
  }

  if (!execute) {
    return { dryRun: true, gateway, prepared: true };
  }

  if (!pk) {
    return { dryRun: false, gateway, error: 'PRIVATE_KEY not set' };
  }

  try {
    const provider = new ethers.JsonRpcProvider(rpc, gwCfg.chainId);
    const wallet = new ethers.Wallet(pk, provider);
    const contract = new ethers.Contract(gateway, GATEWAY_ABI, wallet);

    const already = await withRpcTimeout('processedInstructions', () =>
      contract.processedInstructions(message.instructionId)
    );
    if (already) {
      return { dryRun: false, gateway, error: 'duplicate instruction' };
    }

    const tx = await withRpcTimeout('submitInbound', () => contract.submitInbound(message));
    const receipt = (await withRpcTimeout('tx.wait', () => tx.wait())) as ethers.TransactionReceipt | null;
    return { dryRun: false, gateway, txHash: receipt?.hash ?? tx.hash };
  } catch (e) {
    return { dryRun: false, gateway, error: e instanceof Error ? e.message : String(e) };
  }
}
