import { ethers } from 'ethers';
import type { Request, Response } from 'express';
import { checkpointIndexerConfig as cfg } from './config';
import {
  buildAddressIndex,
  getAddressTransactions,
  summarizeAddressActivity,
} from './addressIndex';
import {
  chain138ExplorerBase,
  chain138ExplorerTxUrl,
  enrichRegistryLogsEnvelope,
} from './chain138Explorer';
import { decodePaymentAttestedLogs, PAYMENT_ATTESTED_TOPIC0 } from './iso20022LogDecode';
import {
  CHAIN138_ACTIVITY_NOTIFIED_TOPIC0,
  decodeChain138ActivityNotifiedLogs,
} from './participantSurfaceLogDecode';

/** Etherscan V2-shaped envelope for tooling compatibility. */
function ok(result: unknown, message = 'OK') {
  return { status: '1', message, result };
}

function err(message: string, result: unknown = null) {
  return { status: '0', message, result };
}

function normalizeAddress(address: string): string {
  try {
    return ethers.getAddress(address);
  } catch {
    return ethers.getAddress(address.toLowerCase());
  }
}

function padTopicAddress(address: string): string {
  return ethers.zeroPadValue(normalizeAddress(address), 32);
}

async function blockscoutTxList(address: string, page: number, offset: number) {
  const base = cfg.blockscoutApi.replace(/\/$/, '');
  const url = `${base}/addresses/${normalizeAddress(address)}/transactions?page=${page}&offset=${offset}`;
  const res = await fetch(url);
  if (!res.ok) throw new Error(`blockscout ${res.status}`);
  const body = (await res.json()) as { items?: Array<Record<string, unknown>> };
  return (body.items ?? []).map((tx) => ({
    blockNumber: String(tx.block_number ?? tx.blockNumber ?? ''),
    timeStamp: tx.timestamp
      ? String(Math.floor(new Date(String(tx.timestamp)).getTime() / 1000))
      : '',
    hash: tx.hash,
    from: (tx.from as { hash?: string })?.hash ?? tx.from,
    to: (tx.to as { hash?: string })?.hash ?? tx.to,
    value: String(tx.value ?? '0'),
    gas: String((tx.fee as { gas_used?: string })?.gas_used ?? tx.gas_used ?? ''),
    gasPrice: String(tx.gas_price ?? tx.gasPrice ?? '0'),
    isError: tx.status === 'ok' || tx.status === 1 || tx.status === '0x1' ? '0' : '1',
    txreceipt_status: tx.status === 'ok' || tx.status === 1 ? '1' : '0',
    input: tx.raw_input ?? tx.input ?? '0x',
    contractAddress: '',
    cumulativeGasUsed: '',
    confirmations: String(tx.confirmations ?? ''),
  }));
}

async function etherscanV2GetLogs(params: URLSearchParams, apiKey: string) {
  const q = new URLSearchParams(params);
  q.set('chainid', '1');
  q.set('module', 'logs');
  q.set('action', 'getLogs');
  if (apiKey) q.set('apikey', apiKey);
  const url = `https://api.etherscan.io/v2/api?${q.toString()}`;
  const res = await fetch(url);
  return res.json();
}

export function chainlistSupplement(_req: Request, res: Response) {
  const registry = cfg.addressActivityRegistry || null;
  res.json({
    comments:
      'Supplemental chainlist for networks absent from official Etherscan V2 chainlist. Not maintained by Etherscan.',
    totalcount: 2,
    result: [
      {
        chainname: 'Defi Oracle Meta Mainnet',
        chainid: '138',
        blockexplorer: 'https://explorer.d-bis.org/',
        apiurl: `${publicBase(_req)}/v2/api?chainid=138`,
        status: 1,
        comment: 'Blockscout native + project attestation shim',
      },
      {
        chainname: 'Ethereum Mainnet (Chain 138 attestation layer)',
        chainid: '1',
        attestationLayer: true,
        blockexplorer: 'https://etherscan.io/',
        apiurl: `${publicBase(_req)}/v2/api?chainid=1&module=chain138mirror`,
        status: 1,
        comment: 'Query mirror/registry logs via chain138mirror module on this shim (not native Etherscan V2 module=account)',
        contracts: {
          transactionMirror: cfg.legacyMirror,
          addressActivityRegistry: registry,
          addressActivityRegistryV2: cfg.addressActivityRegistryV2 || null,
          iso20022IntakeGateway: cfg.iso20022IntakeGateway || null,
          checkpointHub: cfg.checkpointProxy || null,
        },
      },
    ],
  });
}

function publicBase(req: Request): string {
  const proto = (req.headers['x-forwarded-proto'] as string) || req.protocol;
  const host = req.headers['x-forwarded-host'] || req.headers.host || `127.0.0.1:${cfg.port}`;
  return `${proto}://${host}`;
}

export async function etherscanV2Api(req: Request, res: Response) {
  const q = req.query;
  const chainid = String(q.chainid ?? '');
  const module = String(q.module ?? '');
  const action = String(q.action ?? '');
  const apiKey = String(q.apikey ?? process.env.ETHERSCAN_API_KEY ?? '');

  if (!chainid) {
    return res.json(
      err(
        'Missing or unsupported chainid parameter (required for v2 api), please see chainlist-supplement for Chain 138'
      )
    );
  }

  try {
    if (chainid === '138') {
      return res.json(await handleChain138(module, action, q));
    }
    if (chainid === '1' && module === 'chain138mirror') {
      return res.json(await handleMainnetAttestation(action, q, apiKey));
    }
    if (chainid === '1' && module === 'proxy') {
      return res.json(
        err('Use official https://api.etherscan.io/v2/api for chainid=1 proxy module')
      );
    }
    return res.json(
      err(
        `Chain ${chainid} is not served by this shim. Chain 138 native: chainid=138. Chain 138 on mainnet attestation: chainid=1&module=chain138mirror`
      )
    );
  } catch (e) {
    return res.json(err(String(e)));
  }
}

async function handleChain138(
  module: string,
  action: string,
  q: Request['query']
): Promise<{ status: string; message: string; result: unknown }> {
  const address = String(q.address ?? '');

  if (module === 'account' && action === 'balance') {
    if (!/^0x[a-fA-F0-9]{40}$/.test(address)) return err('Invalid address');
    const provider = new ethers.JsonRpcProvider(cfg.chain138Rpc);
    const bal = await provider.getBalance(normalizeAddress(address));
    return ok(bal.toString());
  }

  if (module === 'account' && action === 'txlist') {
    if (!/^0x[a-fA-F0-9]{40}$/.test(address)) return err('Invalid address');
    const page = parseInt(String(q.page ?? '1'), 10) || 1;
    const offset = Math.min(parseInt(String(q.offset ?? '10'), 10) || 10, 100);
    const items = await blockscoutTxList(normalizeAddress(address), page, offset);
    return ok(items);
  }

  if (module === 'chain138' && action === 'attestation') {
    const txHash = String(q.txhash ?? q.txHash ?? '');
    if (!/^0x[a-fA-F0-9]{64}$/.test(txHash)) return err('Invalid txhash');
    const base = publicBaseFromQuery(q);
    const r = await fetch(`${base}/v1/tx/${txHash}/attestation`);
    if (!r.ok) return err('attestation not found');
    return ok(await r.json());
  }

  if (module === 'chain138' && action === 'participantactivity') {
    if (!/^0x[a-fA-F0-9]{40}$/.test(address)) return err('Invalid address');
    const normalized = normalizeAddress(address);
    const index = buildAddressIndex(cfg.batchPayloadDir);
    const { total, items } = getAddressTransactions(
      index,
      normalized,
      Math.min(parseInt(String(q.offset ?? '50'), 10) || 50, 200),
      parseInt(String(q.page ?? '0'), 10) || 0
    );
    const summary = summarizeAddressActivity(index.byAddress[normalized.toLowerCase()] ?? []);
    const explorerBase = chain138ExplorerBase();
    const enrichedItems = items.map((row) => ({
      ...row,
      chain138ExplorerUrl: chain138ExplorerTxUrl(row.txHash, explorerBase),
      roleLabel: row.role === 'from' ? 'debited' : 'credited',
    }));
    const onChainHint =
      total === 0 && cfg.addressActivityRegistry
        ? {
            registry: cfg.addressActivityRegistry,
            query:
              'GET /v2/api?chainid=1&module=chain138mirror&action=participantlogs&address=' +
              normalized,
          }
        : null;
    return ok({
      total,
      summary,
      items: enrichedItems,
      onChainHint,
      chain138Explorer: { base: explorerBase, txUrlTemplate: `${explorerBase}/tx/{txHash}` },
    });
  }

  if (module === 'proxy' && action === 'eth_blockNumber') {
    const provider = new ethers.JsonRpcProvider(cfg.chain138Rpc);
    const block = await provider.getBlockNumber();
    return ok(`0x${block.toString(16)}`);
  }

  return err(`Unsupported module/action for chainid=138: ${module}/${action}`);
}

function publicBaseFromQuery(q: Request['query']): string {
  const override = String(q.indexerBase ?? process.env.CHECKPOINT_PAYLOAD_PUBLIC_URL ?? '');
  if (override.startsWith('http')) return override.replace(/\/v1\/payload\/?$/, '').replace(/\/$/, '');
  return `http://127.0.0.1:${cfg.port}`;
}

async function handleMainnetAttestation(
  action: string,
  q: Request['query'],
  apiKey: string
): Promise<{ status: string; message: string; result: unknown }> {
  const registry = cfg.addressActivityRegistry;
  const mirror = cfg.legacyMirror;
  const address = String(q.address ?? '');

  if (action === 'contracts') {
    const explorerBase = chain138ExplorerBase();
    return ok({
      transactionMirror: mirror,
      addressActivityRegistry: registry || null,
      checkpointHub: cfg.checkpointProxy || null,
      chain138Explorer: {
        base: explorerBase,
        txUrlTemplate: `${explorerBase}/tx/{txHash}`,
      },
      eventTopics: {
        TransactionMirrored: '0xc25ce5062c7e42c68fa21fe088d21e609cc0c61b8bec3180681363bb5cf02a9e',
        ParticipantCredited: '0x853ebad0824f583553bcb4360a0df947d39654e86aba8d7c0625499cbf09f0d5',
        ParticipantDebited: '0xf122cad7c8ada37ddd6b6ddca0982b9f7be689cde15eedfd767cc207fa7dc2c8',
      },
    });
  }

  if (action === 'participantlogs') {
    if (!registry && !cfg.addressActivityRegistryV2) {
      return err('ADDRESS_ACTIVITY_REGISTRY_MAINNET or V2 not configured');
    }
    if (!/^0x[a-fA-F0-9]{40}$/.test(address)) return err('Invalid address');
    if (!apiKey) return err('ETHERSCAN_API_KEY required for mainnet log proxy');

    const topic = padTopicAddress(address);
    const params = new URLSearchParams({
      fromBlock: String(q.fromBlock ?? '0'),
      toBlock: String(q.toBlock ?? 'latest'),
    });

    let credited = { status: '0', message: 'No records', result: [] as unknown[] };
    let debited = { status: '0', message: 'No records', result: [] as unknown[] };

    if (registry) {
      const regParams = new URLSearchParams([...params.entries(), ['address', registry]]);
      credited = await etherscanV2GetLogs(
        new URLSearchParams([
          ...regParams.entries(),
          ['topic0', '0x853ebad0824f583553bcb4360a0df947d39654e86aba8d7c0625499cbf09f0d5'],
          ['topic2', topic],
        ]),
        apiKey
      );
      debited = await etherscanV2GetLogs(
        new URLSearchParams([
          ...regParams.entries(),
          ['topic0', '0xf122cad7c8ada37ddd6b6ddca0982b9f7be689cde15eedfd767cc207fa7dc2c8'],
          ['topic3', topic],
        ]),
        apiKey
      );
    }

    let surfaceNotify: {
      status: string;
      result: unknown[];
      decoded: ReturnType<typeof decodeChain138ActivityNotifiedLogs>;
    } | null = null;
    if (cfg.participantSurface) {
      const surfaceRaw = await etherscanV2GetLogs(
        new URLSearchParams([
          ...params.entries(),
          ['address', cfg.participantSurface],
          ['topic0', CHAIN138_ACTIVITY_NOTIFIED_TOPIC0],
          ['topic1', topic],
        ]),
        apiKey
      );
      const surfaceEnv = surfaceRaw as { status: string; result: unknown[] };
      surfaceNotify = {
        status: surfaceEnv.status,
        result: surfaceEnv.result ?? [],
        decoded: decodeChain138ActivityNotifiedLogs(
          surfaceEnv as {
            status: string;
            result: Array<{
              topics?: string[];
              transactionHash?: string;
              blockNumber?: string;
              logIndex?: string;
              data?: string;
            }>;
          },
          chain138ExplorerBase()
        ),
      };
    }

    const explorerBase = chain138ExplorerBase();
    const index = buildAddressIndex(cfg.batchPayloadDir);
    const normalized = normalizeAddress(address);
    const { total: batchTotal, items: batchItems } = getAddressTransactions(index, normalized, 200, 0);
    const batchIndexedActivity = batchItems.map((row) => ({
      ...row,
      chain138ExplorerUrl: chain138ExplorerTxUrl(row.txHash, explorerBase),
      roleLabel: row.role === 'from' ? 'debited' : 'credited',
    }));

    const debtorTxHashes = [
      ...new Set(
        batchItems.filter((r) => r.role === 'from').map((r) => r.txHash.toLowerCase())
      ),
    ];

    let isoV2: { status: string; result: unknown[]; decoded: ReturnType<typeof decodePaymentAttestedLogs> } | null =
      null;
    if (cfg.addressActivityRegistryV2) {
      const v2raw = await etherscanV2GetLogs(
        new URLSearchParams([
          ...params.entries(),
          ['address', cfg.addressActivityRegistryV2],
          ['topic0', PAYMENT_ATTESTED_TOPIC0],
          ['topic3', topic],
        ]),
        apiKey
      );
      const v2env = v2raw as { status: string; result: unknown[] };
      const decodedCreditor = decodePaymentAttestedLogs(
        v2env as { status: string; result: Array<{ topics?: string[]; transactionHash?: string; blockNumber?: string; logIndex?: string; data?: string }> },
        explorerBase
      );

      const decodedDebtor: ReturnType<typeof decodePaymentAttestedLogs> = [];
      for (const txHash of debtorTxHashes) {
        const byTx = await etherscanV2GetLogs(
          new URLSearchParams([
            ...params.entries(),
            ['address', cfg.addressActivityRegistryV2],
            ['topic0', PAYMENT_ATTESTED_TOPIC0],
            ['topic1', txHash],
          ]),
          apiKey
        );
        decodedDebtor.push(
          ...decodePaymentAttestedLogs(
            byTx as { status: string; result: Array<{ topics?: string[]; transactionHash?: string; blockNumber?: string; logIndex?: string; data?: string }> },
            explorerBase
          ).filter((d) => d.debtor.toLowerCase() === normalized.toLowerCase())
        );
      }

      const merged = new Map<string, (typeof decodedCreditor)[0]>();
      for (const row of [...decodedCreditor, ...decodedDebtor]) {
        merged.set(`${row.chain138TxHash}:${row.mainnetTxHash}`, row);
      }

      isoV2 = {
        status: v2env.status,
        result: v2env.result ?? [],
        decoded: [...merged.values()],
      };
    }

    return ok({
      participant: normalized,
      registry: registry || null,
      registryV2: cfg.addressActivityRegistryV2 || null,
      participantSurface: cfg.participantSurface || null,
      credited: enrichRegistryLogsEnvelope(credited),
      debited: enrichRegistryLogsEnvelope(debited),
      surfaceNotifications: surfaceNotify,
      iso20022V2: isoV2,
      batchIndexedActivity: {
        total: batchTotal,
        items: batchIndexedActivity,
      },
      chain138Explorer: {
        base: explorerBase,
        txUrlTemplate: `${explorerBase}/tx/{txHash}`,
      },
      topicIndex: {
        ParticipantCredited: { chain138TxHash: 'topic1', participant: 'topic2', counterparty: 'topic3' },
        ParticipantDebited: { chain138TxHash: 'topic1', counterparty: 'topic2', participant: 'topic3' },
        PaymentAttested: { chain138TxHash: 'topic1', instructionId: 'topic2', creditor: 'topic3', debtor: 'data' },
        Chain138ActivityNotified: {
          participant: 'topic1',
          chain138TxHash: 'topic2',
          instructionId: 'topic3',
        },
      },
      etherscanVisibility: {
        transactionsTab:
          'Incoming 0 ETH txs to participant (CHECKPOINT_SURFACE_TOPLEVEL_ZERO_ETH=1, default on) — calldata chain138Attestation(bytes32,uint8,uint64)',
        internalTxnsTab: 'Chain138ParticipantSurface wallet touch per sender and receiver',
        eventsOnSurfaceContract: 'surfaceNotifications.decoded[] — filter topic1=participant on surface contract',
        registryV1Logs: 'credited/debited.decoded[] — ParticipantCredited + ParticipantDebited',
      },
      note:
        'Each Chain 138 payment surfaces twice on mainnet: debited (from) + credited (to). batchIndexedActivity.items[] lists both roles with chain138TxHash. Full pacs.008 XML in batch payloads.',
    });
  }

  if (action === 'mirrorlogs') {
    if (!/^0x[a-fA-F0-9]{64}$/.test(String(q.txhash ?? ''))) return err('Invalid txhash');
    if (!apiKey) return err('ETHERSCAN_API_KEY required');
    const txHash = String(q.txhash);
    const params = new URLSearchParams({
      address: mirror,
      fromBlock: String(q.fromBlock ?? '0'),
      toBlock: String(q.toBlock ?? 'latest'),
      topic0: '0xc25ce5062c7e42c68fa21fe088d21e609cc0c61b8bec3180681363bb5cf02a9e',
      topic1: txHash,
    });
    const logs = await etherscanV2GetLogs(params, apiKey);
    return ok(logs);
  }

  return err(`Unsupported chain138mirror action: ${action}`);
}
