/** Read-only mainnet verification. Provider credentials never enter returned errors. */
export type UsdtNetwork = 'ethereum' | 'tron';
export type UsdtVerificationCode = 'invalid_tx' | 'transfer_mismatch' | 'ambiguous_transfer' | 'provider_unavailable';
export interface VerifyUsdtTransferInput {
  network: UsdtNetwork;
  address: string;
  amountMicro: string;
  createdAt: string;
  expiresAt: string;
  transactionHash: string;
  apiKey: string;
}
export interface VerifiedUsdtTransfer {
  status: 'confirmed';
  txHash: string;
  logIndex: number;
  blockNumber: number;
  blockHash: string;
  blockTime: string;
  amountMicro: string;
  address: string;
  network: UsdtNetwork;
}
export type UsdtVerificationResult = VerifiedUsdtTransfer | {status: 'pending'};
export class UsdtVerificationError extends Error {
  readonly code: UsdtVerificationCode;
  constructor(code: UsdtVerificationCode) {
    super(code);
    this.name = 'UsdtVerificationError';
    this.code = code;
  }
}

const ETH_TOKEN = '0xdac17f958d2ee523a2206206994597c13d831ec7';
const TRON_TOKEN = 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t';
const TRON_TOKEN_HEX = 'a614f803b6fd780986a42c78ec9c7f77e6ded13c';
const TRANSFER = 'ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef';
const BASE58 = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
const ZERO = BigInt(0);
const MAX_UINT256 = (BigInt(1) << BigInt(256)) - BigInt(1);
const MAX_BODY_BYTES = 2 * 1024 * 1024;
const REQUEST_TIMEOUT_MS = 10_000;
type JsonObject = Record<string, unknown>;
type Fetcher = typeof fetch;

function fail(code: UsdtVerificationCode): never { throw new UsdtVerificationError(code); }
function object(value: unknown): JsonObject | null {
  return value !== null && typeof value === 'object' && !Array.isArray(value) ? value as JsonObject : null;
}
function hex(bytes: Uint8Array): string { return Array.from(bytes, b => b.toString(16).padStart(2, '0')).join(''); }
function safeInteger(value: unknown, minimum = 0): value is number {
  return typeof value === 'number' && Number.isSafeInteger(value) && value >= minimum;
}
function quantity(value: unknown): number | null {
  if (typeof value !== 'string' || !/^0x(?:0|[1-9a-f][0-9a-f]*)$/i.test(value) || value.length > 16) return null;
  const number = Number(BigInt(value));
  return Number.isSafeInteger(number) && number >= 0 ? number : null;
}
function ethHash(value: unknown): value is string { return typeof value === 'string' && /^0x[0-9a-f]{64}$/i.test(value); }
function tronHash(value: unknown): value is string { return typeof value === 'string' && /^[0-9a-f]{64}$/i.test(value); }

async function tronAddressBytes(address: string): Promise<Uint8Array | null> {
  if (!/^T[1-9A-HJ-NP-Za-km-z]{33}$/.test(address)) return null;
  let value = ZERO;
  for (const character of address) value = value * BigInt(58) + BigInt(BASE58.indexOf(character));
  const bytes = new Uint8Array(25);
  for (let index = 24; index >= 0; index--) { bytes[index] = Number(value & BigInt(255)); value >>= BigInt(8); }
  if (value !== ZERO || bytes[0] !== 0x41) return null;
  const payload = bytes.slice(0, 21);
  const first = await crypto.subtle.digest('SHA-256', payload);
  const checksum = new Uint8Array(await crypto.subtle.digest('SHA-256', first));
  if (!bytes.slice(21).every((byte, index) => byte === checksum[index])) return null;
  return payload;
}

/** Accepts only deposit accounts, never the token contract or a zero account. */
export async function validateDepositAddress(network: UsdtNetwork, address: string): Promise<boolean> {
  if (typeof address !== 'string') return false;
  if (network === 'ethereum') {
    const normalized = address.toLowerCase();
    return /^0x[0-9a-f]{40}$/.test(normalized) && normalized !== ETH_TOKEN && normalized !== `0x${'0'.repeat(40)}`;
  }
  if (network !== 'tron' || address === TRON_TOKEN) return false;
  const bytes = await tronAddressBytes(address);
  return bytes !== null && bytes.slice(1).some(byte => byte !== 0);
}

export async function normalizeAddress(network: UsdtNetwork, address: string): Promise<string> {
  if (!await validateDepositAddress(network, address)) fail('invalid_tx');
  return network === 'ethereum' ? address.toLowerCase() : address;
}

export function validateTransactionHash(network: UsdtNetwork, hash: string): boolean {
  return network === 'ethereum' ? ethHash(hash) : network === 'tron' && tronHash(hash);
}

async function request(url: string, body: JsonObject, apiKey: string | null, fetcher: Fetcher): Promise<unknown> {
  const controller = new AbortController();
  let timer: ReturnType<typeof setTimeout> | undefined;
  const timedOut = new Promise<never>((_, reject) => {
    timer = setTimeout(() => { controller.abort(); reject(new UsdtVerificationError('provider_unavailable')); }, REQUEST_TIMEOUT_MS);
  });
  try {
    return await Promise.race([timedOut, (async () => {
      const response = await fetcher(url, {
        method: 'POST', redirect: 'error', signal: controller.signal,
        headers: {'Content-Type': 'application/json', Accept: 'application/json', ...(apiKey ? {'TRON-PRO-API-KEY': apiKey} : {})},
        body: JSON.stringify(body),
      });
      if (!response.ok || !response.body) fail('provider_unavailable');
      const length = response.headers.get('content-length');
      if (length !== null && (!/^\d+$/.test(length) || Number(length) > MAX_BODY_BYTES)) {
        controller.abort(); fail('provider_unavailable');
      }
      const reader = response.body.getReader();
      const chunks: Uint8Array[] = [];
      let total = 0;
      try {
        while (true) {
          const {done, value} = await reader.read();
          if (done) break;
          total += value.byteLength;
          if (total > MAX_BODY_BYTES) { controller.abort(); void reader.cancel(); fail('provider_unavailable'); }
          chunks.push(value);
        }
      } finally { reader.releaseLock(); }
      const bytes = new Uint8Array(total);
      let offset = 0;
      for (const chunk of chunks) { bytes.set(chunk, offset); offset += chunk.byteLength; }
      return JSON.parse(new TextDecoder('utf-8', {fatal: true}).decode(bytes)) as unknown;
    })()]);
  } catch { fail('provider_unavailable'); }
  finally { if (timer !== undefined) clearTimeout(timer); }
}

function validTime(value: unknown): number | null {
  if (!safeInteger(value, 1) || value > 8_640_000_000_000_000) return null;
  return value;
}
function paymentWindow(input: VerifyUsdtTransferInput): [number, number] {
  const iso = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?(?:Z|[+-]\d{2}:\d{2})$/;
  if (typeof input.createdAt !== 'string' || typeof input.expiresAt !== 'string' || !iso.test(input.createdAt) || !iso.test(input.expiresAt)) fail('invalid_tx');
  const start = Date.parse(input.createdAt), end = Date.parse(input.expiresAt);
  if (!Number.isFinite(start) || !Number.isFinite(end) || start < 0 || end < start) fail('invalid_tx');
  return [start, end];
}
function checkTime(time: number | null, window: [number, number]): number {
  if (time === null) fail('invalid_tx');
  if (time < window[0] || time >= window[1]) fail('transfer_mismatch');
  return time;
}
function eventTransfer(log: JsonObject, prefix: '' | '0x'): {sender: string; recipient: string; amount: string} | null {
  const topics = log.topics;
  if (!Array.isArray(topics) || topics.length === 0 || typeof topics[0] !== 'string' || topics[0].toLowerCase() !== prefix + TRANSFER) return null;
  const word = new RegExp(`^${prefix}[0-9a-f]{64}$`, 'i');
  const addressWord = new RegExp(`^${prefix}0{24}[0-9a-f]{40}$`, 'i');
  if (topics.length !== 3 || !topics.slice(1).every(topic => typeof topic === 'string' && addressWord.test(topic)) || typeof log.data !== 'string' || !word.test(log.data)) fail('invalid_tx');
  const encoded = prefix ? log.data : `0x${log.data}`;
  return {sender: (topics[1] as string).slice(-40).toLowerCase(), recipient: (topics[2] as string).slice(-40).toLowerCase(), amount: BigInt(encoded).toString()};
}

async function verifyEthereum(input: VerifyUsdtTransferInput, address: string, window: [number, number], fetcher: Fetcher): Promise<UsdtVerificationResult> {
  const txHash = input.transactionHash.toLowerCase();
  const url = `https://eth-mainnet.g.alchemy.com/v2/${encodeURIComponent(input.apiKey)}`;
  let id = 0;
  async function rpc(method: string, params: unknown[]): Promise<unknown> {
    const requestId = ++id;
    const response = object(await request(url, {jsonrpc: '2.0', id: requestId, method, params}, null, fetcher));
    if (!response || response.jsonrpc !== '2.0' || response.id !== requestId || response.error !== undefined || !Object.prototype.hasOwnProperty.call(response, 'result')) fail('provider_unavailable');
    return response.result;
  }
  const [chain, receiptResult, finalizedResult, decimals] = await Promise.all([
    rpc('eth_chainId', []), rpc('eth_getTransactionReceipt', [txHash]),
    rpc('eth_getBlockByNumber', ['finalized', false]), rpc('eth_call', [{to: ETH_TOKEN, data: '0x313ce567'}, 'finalized']),
  ]);
  if (chain !== '0x1' || decimals !== `0x${'0'.repeat(63)}6`) fail('provider_unavailable');
  if (receiptResult === null || finalizedResult === null) return {status: 'pending'};
  const receipt = object(receiptResult), finalized = object(finalizedResult);
  if (!receipt || !finalized) fail('provider_unavailable');
  const blockNumber = quantity(receipt.blockNumber), finalizedNumber = quantity(finalized.number);
  if (!ethHash(receipt.transactionHash) || receipt.transactionHash.toLowerCase() !== txHash || !ethHash(receipt.blockHash) || blockNumber === null || blockNumber === 0 || finalizedNumber === null || !ethHash(finalized.hash)) fail('invalid_tx');
  if (receipt.status !== '0x1') fail('invalid_tx');
  if (blockNumber > finalizedNumber) return {status: 'pending'};
  const block = object(await rpc('eth_getBlockByNumber', [receipt.blockNumber, false]));
  if (!block || quantity(block.number) !== blockNumber || !ethHash(block.hash) || block.hash.toLowerCase() !== receipt.blockHash.toLowerCase() || (blockNumber === finalizedNumber && block.hash.toLowerCase() !== finalized.hash.toLowerCase())) fail('invalid_tx');
  if (!Array.isArray(block.transactions) || block.transactions.filter(hash => typeof hash === 'string' && hash.toLowerCase() === txHash).length !== 1) fail('invalid_tx');
  const seconds = quantity(block.timestamp);
  const blockTime = checkTime(seconds === null ? null : validTime(seconds * 1000), window);
  if (!Array.isArray(receipt.logs) || receipt.logs.length > 10_000) fail('invalid_tx');
  const indices = new Set<number>();
  const matches: number[] = [];
  for (const value of receipt.logs) {
    const log = object(value);
    if (!log || (log.removed !== undefined && log.removed !== false) || !ethHash(log.blockHash) || log.blockHash.toLowerCase() !== block.hash.toLowerCase() || quantity(log.blockNumber) !== blockNumber || !ethHash(log.transactionHash) || log.transactionHash.toLowerCase() !== txHash) fail('invalid_tx');
    const index = quantity(log.logIndex);
    if (index === null || indices.has(index)) fail('invalid_tx');
    indices.add(index);
    if (typeof log.address !== 'string' || log.address.toLowerCase() !== ETH_TOKEN) continue;
    const transfer = eventTransfer(log, '0x');
    // A self-transfer or a transaction that also spends this account's USDT is
    // not an unambiguous deposit. Leave such cases for separate reconciliation.
    if (transfer && transfer.sender === address.slice(2) && transfer.amount !== '0') fail('transfer_mismatch');
    if (transfer && transfer.recipient === address.slice(2) && transfer.amount === input.amountMicro) matches.push(index);
  }
  if (matches.length > 1) fail('ambiguous_transfer');
  if (matches.length !== 1) fail('transfer_mismatch');
  return {status: 'confirmed', network: 'ethereum', txHash, logIndex: matches[0], blockNumber, blockHash: block.hash.toLowerCase(), blockTime: new Date(blockTime).toISOString(), amountMicro: input.amountMicro, address};
}

async function verifyTron(input: VerifyUsdtTransferInput, address: string, window: [number, number], fetcher: Fetcher): Promise<UsdtVerificationResult> {
  const txHash = input.transactionHash.toLowerCase();
  async function node(path: 'gettransactioninfobyid' | 'getblockbynum', body: JsonObject): Promise<JsonObject> {
    const response = object(await request(`https://api.trongrid.io/walletsolidity/${path}`, body, input.apiKey, fetcher));
    if (!response || response.Error !== undefined || response.error !== undefined || response.success === false) fail('provider_unavailable');
    return response;
  }
  const info = await node('gettransactioninfobyid', {value: txHash});
  if (Object.keys(info).length === 0) return {status: 'pending'};
  const receipt = object(info.receipt);
  if (!tronHash(info.id) || info.id.toLowerCase() !== txHash || !receipt || receipt.result !== 'SUCCESS' || (info.result !== undefined && info.result !== 'SUCCESS' && info.result !== 'SUCESS') || !safeInteger(info.blockNumber, 1)) fail('invalid_tx');
  const blockTime = checkTime(validTime(info.blockTimeStamp), window);
  const block = await node('getblockbynum', {num: info.blockNumber});
  const header = object(object(block.block_header)?.raw_data);
  if (!tronHash(block.blockID) || !header || header.number !== info.blockNumber || header.timestamp !== blockTime || BigInt(`0x${block.blockID.slice(0, 16)}`) !== BigInt(info.blockNumber) || !Array.isArray(block.transactions)) fail('invalid_tx');
  const transactions = block.transactions.filter(value => {
    const transaction = object(value);
    return transaction && typeof transaction.txID === 'string' && transaction.txID.toLowerCase() === txHash;
  });
  if (transactions.length !== 1) fail('invalid_tx');
  if (!Array.isArray(info.log) || info.log.length > 10_000) fail('invalid_tx');
  const destination = hex((await tronAddressBytes(address))!.slice(1));
  const matches: number[] = [];
  for (let index = 0; index < info.log.length; index++) {
    const log = object(info.log[index]);
    if (!log || (log.removed !== undefined && log.removed !== false)) fail('invalid_tx');
    if (typeof log.address !== 'string' || log.address.toLowerCase() !== TRON_TOKEN_HEX) continue;
    const transfer = eventTransfer(log, '');
    if (transfer && transfer.sender === destination && transfer.amount !== '0') fail('transfer_mismatch');
    if (transfer && transfer.recipient === destination && transfer.amount === input.amountMicro) matches.push(index);
  }
  if (matches.length > 1) fail('ambiguous_transfer');
  if (matches.length !== 1) fail('transfer_mismatch');
  return {status: 'confirmed', network: 'tron', txHash, logIndex: matches[0], blockNumber: info.blockNumber, blockHash: block.blockID.toLowerCase(), blockTime: new Date(blockTime).toISOString(), amountMicro: input.amountMicro, address};
}

export async function verifyUsdtTransfer(input: VerifyUsdtTransferInput, fetcher: Fetcher = fetch): Promise<UsdtVerificationResult> {
  if (!input || !validateTransactionHash(input.network, input.transactionHash)) fail('invalid_tx');
  const address = await normalizeAddress(input.network, input.address);
  if (typeof input.amountMicro !== 'string' || !/^[1-9]\d{0,77}$/.test(input.amountMicro) || BigInt(input.amountMicro) > MAX_UINT256) fail('invalid_tx');
  const window = paymentWindow(input);
  if (typeof input.apiKey !== 'string' || !/^[A-Za-z0-9_-]{8,256}$/.test(input.apiKey)) fail('provider_unavailable');
  return input.network === 'ethereum' ? verifyEthereum(input, address, window, fetcher) : verifyTron(input, address, window, fetcher);
}
