import express from 'express';
import * as fs from 'fs';
import * as path from 'path';
import { ethers } from 'ethers';
import { checkpointIndexerConfig as cfg } from './config';
import { enrichLeaves, type LeafRecord } from './tokenEnrich';
import { merkleVerifyValueWei } from '@dbis/checkpoint-core';
import { merkleProofs, paymentLeafHash } from './merkle';
import { jsonSafe } from './serialize';
import {
  buildAddressIndex,
  getAddressTransactions,
  summarizeAddressActivity,
  type AddressIndex,
} from './addressIndex';
import { chainlistSupplement, etherscanV2Api } from './etherscanV2Shim';
import { chain138ExplorerBase, chain138ExplorerTxUrl } from './chain138Explorer';
import { linksFromBatchMeta, resolveTxMainnetLinks } from './txMainnetLinks';

const CHECKPOINT_ABI = [
  'function getLatestBatchId() view returns (uint64)',
  'function latestCheckpointBlock() view returns (uint256)',
  'function getLatestCheckpoint() view returns (tuple(uint64,uint64,uint64,uint256,uint256,uint256,bytes32,bytes32,bytes32,bytes32,uint16,uint32,uint64,address,bytes32))',
  'function getCheckpoint(uint64) view returns (tuple(uint64,uint64,uint64,uint256,uint256,uint256,bytes32,bytes32,bytes32,bytes32,uint16,uint32,uint64,address,bytes32))',
  'function isTxIncluded(bytes32) view returns (bool,uint64)',
];

const MIRROR_ABI = ['function getMirroredTransactionCount() view returns (uint256)'];

const app = express();
const mainnet = new ethers.JsonRpcProvider(cfg.mainnetRpc);
const chain138 = new ethers.JsonRpcProvider(cfg.chain138Rpc);
const hub = cfg.checkpointProxy ? new ethers.Contract(cfg.checkpointProxy, CHECKPOINT_ABI, mainnet) : null;
const mirror = new ethers.Contract(cfg.legacyMirror, MIRROR_ABI, mainnet);

let addressIndexCache: AddressIndex | null = null;
let addressIndexBuiltAt = 0;
const ADDRESS_INDEX_TTL_MS = 60_000;

function getAddressIndex(): AddressIndex {
  const now = Date.now();
  if (!addressIndexCache || now - addressIndexBuiltAt > ADDRESS_INDEX_TTL_MS) {
    addressIndexCache = buildAddressIndex(cfg.batchPayloadDir);
    addressIndexBuiltAt = now;
  }
  return addressIndexCache;
}

function loadBatchPayload(batchId: string): {
  batchId?: string;
  leaves?: LeafRecord[];
  mainnetAttestation?: Record<string, unknown>;
} | null {
  const p = path.join(cfg.batchPayloadDir, `batch-${batchId}.json`);
  if (!fs.existsSync(p)) return null;
  return JSON.parse(fs.readFileSync(p, 'utf8')) as {
    batchId?: string;
    leaves?: LeafRecord[];
    mainnetAttestation?: Record<string, unknown>;
  };
}

async function payloadWithEnrichedLeaves(
  batchId: string,
  raw: { batchId?: string; leaves?: LeafRecord[]; chainId?: number; paymentsRoot?: string } | null
): Promise<object | null> {
  if (!raw?.leaves?.length) return raw;
  const chainId = raw.chainId ?? 138;
  let leaves = raw.leaves;
  if (cfg.enrichTokenTransfers || cfg.usdEnrichEnabled) {
    leaves = await enrichLeaves(leaves, cfg.blockscoutApi);
  }
  const hashes = leaves.map((l) =>
    paymentLeafHash(chainId, {
      txHash: String(l.txHash),
      from: String(l.from),
      to: String(l.to),
      value: String(l.value ?? '0'),
      nativeValueWei: l.nativeValueWei != null ? String(l.nativeValueWei) : undefined,
      onChainValueWei: l.onChainValueWei != null ? String(l.onChainValueWei) : undefined,
      tokenValue: l.tokenValue != null ? String(l.tokenValue) : undefined,
      blockNumber: Number(l.blockNumber),
      blockTimestamp: Number(l.blockTimestamp),
      gasUsed: String(l.gasUsed ?? '0'),
      success: Boolean(l.success),
    })
  );
  const { root, proofs } = merkleProofs(hashes);
  const leavesWithProofs = leaves.map((l, i) => ({
    ...l,
    leafHash: hashes[i],
    merkleProof: proofs[i],
  }));
  return {
    ...raw,
    batchId: raw.batchId ?? batchId,
    leaves: leavesWithProofs,
    merkleRootComputed: root,
    merkleRootMatches: raw.paymentsRoot ? root.toLowerCase() === String(raw.paymentsRoot).toLowerCase() : null,
    walletAttestationCopy:
      'Balances and payments as of Chain 138 checkpoint block in this batch, attested on Ethereum mainnet.',
  };
}

app.get('/health', (_req, res) => {
  res.json({ ok: true, proxy: cfg.checkpointProxy || null, batchDir: cfg.batchPayloadDir });
});

/** Etherscan V2 supplement for Chain 138 (unsupported on official chainlist). */
app.get('/v2/chainlist-supplement', chainlistSupplement);
app.get('/v2/api', (req, res) => {
  void etherscanV2Api(req, res);
});

/** Public batch JSON for contentURI resolution (CHECKPOINT_PAYLOAD_PUBLIC_URL). */
app.get('/v1/payload/batch-:batchId.json', (req, res) => {
  const raw = loadBatchPayload(req.params.batchId);
  if (!raw) return res.status(404).json({ error: 'batch not found' });
  res.type('json').send(JSON.stringify(raw, null, 2));
});

app.get('/v1/checkpoint/latest', async (_req, res) => {
  if (!hub) return res.status(503).json({ error: 'CHAIN138_MAINNET_CHECKPOINT_PROXY not set' });
  try {
    const batchId = await hub.getLatestBatchId();
    const header = await hub.getLatestCheckpoint();
    let v1Count = 0n;
    try {
      v1Count = await mirror.getMirroredTransactionCount();
    } catch {
      v1Count = 0n;
    }
    const raw = batchId > 0n ? loadBatchPayload(batchId.toString()) : null;
    const payload = raw ? await payloadWithEnrichedLeaves(batchId.toString(), raw) : null;
    res.json({
      source: 'v2',
      batchId: batchId.toString(),
      header: jsonSafe(header),
      legacy: { mirrorCount: v1Count.toString(), tether: cfg.legacyTether },
      payload,
    });
  } catch (e) {
    res.status(500).json({ error: String(e) });
  }
});

app.get('/v1/checkpoint/:batchId', async (req, res) => {
  if (!hub) return res.status(503).json({ error: 'proxy not set' });
  try {
    const batchId = req.params.batchId;
    const header = await hub.getCheckpoint(BigInt(batchId));
    const raw = loadBatchPayload(batchId);
    const payload = raw ? await payloadWithEnrichedLeaves(batchId, raw) : null;
    res.json({ batchId, header: jsonSafe(header), payload });
  } catch (e) {
    res.status(500).json({ error: String(e) });
  }
});

app.get('/v1/tx/:txHash/attestation', async (req, res) => {
  if (!hub) return res.status(503).json({ error: 'proxy not set' });
  try {
    const txHash = req.params.txHash;
    const [included, batchId] = await hub.isTxIncluded(txHash);
    const batchIdStr = batchId.toString();
    const raw = included && batchId > 0n ? loadBatchPayload(batchIdStr) : null;
    const payload = raw ? await payloadWithEnrichedLeaves(batchIdStr, raw) : null;
    const metaLinks = raw ? linksFromBatchMeta(raw) : [];
    let rpcLinks: Awaited<ReturnType<typeof resolveTxMainnetLinks>> = [];
    if (included) {
      try {
        rpcLinks = await resolveTxMainnetLinks(mainnet, txHash, batchIdStr);
      } catch {
        /* prefer batch metadata when RPC is rate-limited */
      }
    }
    const etherscanLinks = [...metaLinks, ...rpcLinks].filter((link, i, arr) => {
      const k = `${link.layer}:${link.mainnetTxHash}`;
      return arr.findIndex((x) => `${x.layer}:${x.mainnetTxHash}` === k) === i;
    });
    res.json({
      txHash,
      included,
      batchId: batchIdStr,
      payload,
      etherscanLinks,
      etherscan: {
        base: 'https://etherscan.io',
        txUrlTemplate: 'https://etherscan.io/tx/{txHash}',
      },
    });
  } catch (e) {
    res.status(500).json({ error: String(e) });
  }
});

/** Wallet helper: verify leaf against on-chain hub (client may also use @dbis/checkpoint-sdk). */
app.get('/v1/checkpoint/:batchId/verify/:leafIndex', async (req, res) => {
  if (!hub) return res.status(503).json({ error: 'proxy not set' });
  try {
    const batchId = req.params.batchId;
    const leafIndex = parseInt(req.params.leafIndex, 10);
    const raw = loadBatchPayload(batchId);
    if (!raw?.leaves?.length || leafIndex < 0 || leafIndex >= raw.leaves.length) {
      return res.status(404).json({ error: 'leaf not found' });
    }
    const payload = await payloadWithEnrichedLeaves(batchId, raw);
    const leaves = (payload as { leaves?: LeafRecord[] }).leaves;
    const leaf = leaves?.[leafIndex];
    if (!leaf || !(leaf as LeafRecord).merkleProof) {
      return res.status(404).json({ error: 'proof missing' });
    }
    const VERIFY_ABI = [
      'function verifyPaymentInBatch(uint64 batchId, tuple(bytes32,address,address,uint256,uint256,uint64,uint256,bool) leaf, bytes32[] proof) view returns (bool)',
    ];
    const verifyHub = new ethers.Contract(cfg.checkpointProxy, VERIFY_ABI, mainnet);
    const l = leaf as LeafRecord;
    const ok = await verifyHub.verifyPaymentInBatch(
      BigInt(batchId),
      [
        l.txHash,
        l.from,
        l.to,
        merkleVerifyValueWei(l),
        BigInt(String(l.blockNumber)),
        BigInt(String(l.blockTimestamp)),
        BigInt(String(l.gasUsed ?? 0)),
        Boolean(l.success),
      ],
      (l as { merkleProof: string[] }).merkleProof
    );
    res.json({ batchId, leafIndex, verified: ok, leaf });
  } catch (e) {
    res.status(500).json({ error: String(e) });
  }
});

app.get('/v1/account/:address', async (req, res) => {
  try {
    const addr = req.params.address;
    let blockTag: number | 'latest' = 'latest';
    if (req.query.atBatch) {
      if (!hub) return res.status(503).json({ error: 'proxy not set' });
      const h = await hub.getCheckpoint(BigInt(String(req.query.atBatch)));
      blockTag = Number(h.checkpointBlock);
    } else if (hub) {
      blockTag = Number(await hub.latestCheckpointBlock());
    }
    const balance = await chain138.getBalance(addr, blockTag);
    const index = getAddressIndex();
    const rows = index.byAddress[addr.toLowerCase()] ?? [];
    const activity = summarizeAddressActivity(rows);
    res.json({
      address: addr,
      chainId: 138,
      blockTag,
      balanceWei: balance.toString(),
      attestedActivity: activity,
      mainnetSurfaces: {
        transactionMirror: cfg.legacyMirror,
        addressActivityRegistry: cfg.addressActivityRegistry || null,
        checkpointHub: cfg.checkpointProxy || null,
      },
      walletAttestationCopy:
        'Native balance as of Chain 138 checkpoint block. Attested payments (ETH + USD) via checkpoint batches on Ethereum mainnet.',
    });
  } catch (e) {
    res.status(500).json({ error: String(e) });
  }
});

app.get('/v1/account/:address/transactions', async (req, res) => {
  try {
    const addr = req.params.address;
    const limit = Math.min(parseInt(String(req.query.limit ?? '50'), 10) || 50, 200);
    const offset = parseInt(String(req.query.offset ?? '0'), 10) || 0;
    const index = getAddressIndex();
    const { total, items } = getAddressTransactions(index, addr, limit, offset);
    res.json({
      address: addr,
      chainId: 138,
      total,
      limit,
      offset,
      items,
      indexMeta: {
        builtAt: index.builtAt,
        batchCount: index.batchCount,
        txCount: index.txCount,
      },
    });
  } catch (e) {
    res.status(500).json({ error: String(e) });
  }
});

app.get('/v1/account/:address/activity', async (req, res) => {
  try {
    const addr = req.params.address;
    const index = getAddressIndex();
    const rows = index.byAddress[addr.toLowerCase()] ?? [];
    const summary = summarizeAddressActivity(rows);
    res.json({
      address: addr,
      chainId: 138,
      summary,
      recent: rows.slice(0, 25).map((r) => ({
        ...r,
        chain138ExplorerUrl: chain138ExplorerTxUrl(r.txHash),
      })),
      chain138Explorer: {
        base: chain138ExplorerBase(),
        txUrlTemplate: `${chain138ExplorerBase()}/tx/{txHash}`,
      },
      etherscan: {
        transactionMirrorEvents: `https://etherscan.io/address/${cfg.legacyMirror}#events`,
        addressActivityEvents: cfg.addressActivityRegistry
          ? `https://etherscan.io/address/${cfg.addressActivityRegistry}#events`
          : null,
      },
    });
  } catch (e) {
    res.status(500).json({ error: String(e) });
  }
});

app.get('/v1/tx/:txHash/logs', async (req, res) => {
  try {
    const txHash = req.params.txHash;
    const receipt = await chain138.getTransactionReceipt(txHash);
    if (!receipt) return res.status(404).json({ error: 'receipt not found on chain 138' });
    res.json({
      txHash,
      chainId: 138,
      blockNumber: receipt.blockNumber,
      status: receipt.status,
      gasUsed: receipt.gasUsed.toString(),
      logCount: receipt.logs.length,
      logs: receipt.logs.map((log, i) => ({
        index: i,
        address: log.address,
        topics: log.topics,
        data: log.data,
      })),
    });
  } catch (e) {
    res.status(500).json({ error: String(e) });
  }
});

app.listen(cfg.port, () => {
  console.log(`checkpoint-indexer listening on :${cfg.port}`);
});
