import { NextResponse } from 'next/server';
import { hexToUtf8 } from '../../_lib/agent-uri';

/**
 * GET /api/agent/discover
 *
 * Returns all registered agents across all 4 chains — live from on-chain
 * ERC-8004 Identity Registries. No hardcoded addresses.
 *
 * Enumeration strategy:
 *   1. getTotalAgents() → total count
 *   2. registeredAgents(index) → address at each index
 *   3. getAgent(address) → agentURI, active, agentType, registeredAt
 *
 * Query params:
 *   ?chain=base          — filter by chain
 *   ?address=0x...       — lookup specific address
 *   ?format=minimal      — return only name, address, chain (smaller payload)
 */

const CHAINS = {
  base: {
    rpcs: ['https://base-rpc.publicnode.com', 'https://mainnet.base.org'],
    registry: '0xa7BD20bf5De63df949cA5Be2F20835978eCba81A',
    explorer: 'https://basescan.org',
  },
  avalanche: {
    rpcs: ['https://api.avax.network/ext/bc/C/rpc', 'https://avalanche-c-chain-rpc.publicnode.com'],
    registry: '0x7448057C95Fb8a8B974a566cdcc9Cd042166A3f8',
    explorer: 'https://snowtrace.io',
  },
  arbitrum: {
    rpcs: ['https://arb1.arbitrum.io/rpc', 'https://arbitrum-one-rpc.publicnode.com'],
    registry: '0x83dd216449B3F0574E39043ECFE275946fa492e9',
    explorer: 'https://arbiscan.io',
  },
  polygon: {
    rpcs: ['https://polygon-bor-rpc.publicnode.com'],
    registry: '0xD9bF6D92a1D9ee44a48c38481c046a819CBdf2ba',
    explorer: 'https://polygonscan.com',
  },
} as const;

type ChainKey = keyof typeof CHAINS;
const CHAIN_KEYS = Object.keys(CHAINS) as ChainKey[];

// Selectors
const SEL_TOTAL_AGENTS    = '0x3731a16f'; // getTotalAgents()
const SEL_REGISTERED_AGENT = '0xa7bd211c'; // registeredAgents(uint256)
const SEL_GET_AGENT       = '0xfb3551ff'; // getAgent(address)

function encodeUint(n: number): string {
  return n.toString(16).padStart(64, '0');
}
function encodeAddress(addr: string): string {
  return addr.replace('0x', '').toLowerCase().padStart(64, '0');
}

async function rpcCallOne(url: string, data: string, to: string): Promise<string | null> {
  try {
    const res = await fetch(url, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        jsonrpc: '2.0',
        id: 1,
        method: 'eth_call',
        params: [{ to, data }, 'latest'],
      }),
      signal: AbortSignal.timeout(10000),
    });
    const json = await res.json();
    if (json.error || !json.result || json.result === '0x') return null;
    return json.result as string;
  } catch {
    return null;
  }
}

async function rpcCall(rpcs: readonly string[], data: string, to: string): Promise<string | null> {
  // Try each RPC in order; first non-null wins.
  for (const url of rpcs) {
    const result = await rpcCallOne(url, data, to);
    if (result) return result;
  }
  return null;
}

function decodeAddress(hex: string): string {
  // 32-byte word → last 20 bytes are the address
  const clean = hex.replace('0x', '');
  if (clean.length < 64) return '';
  return '0x' + clean.slice(-40);
}

/**
 * Decode getAgent ABI tuple: (string agentURI, bool active, string agentType, uint256 registeredAt)
 * ABI layout: 4 head words (32 bytes each) then tail for dynamic strings.
 */
function decodeGetAgent(hex: string): {
  agentURI: string;
  active: boolean;
  agentType: string;
  registeredAt: number;
} | null {
  try {
    const data = hex.replace('0x', '');
    if (data.length < 256) return null;

    // head[0] = offset to agentURI
    const uriOffset = parseInt(data.slice(0, 64), 16) * 2;
    // head[1] = bool active
    const active = parseInt(data.slice(64, 128), 16) === 1;
    // head[2] = offset to agentType
    const typeOffset = parseInt(data.slice(128, 192), 16) * 2;
    // head[3] = uint256 registeredAt
    const registeredAt = parseInt(data.slice(192, 256), 16);

    const uriLen = parseInt(data.slice(uriOffset, uriOffset + 64), 16);
    const uriHex = data.slice(uriOffset + 64, uriOffset + 64 + uriLen * 2);
    const agentURI = hexToUtf8(uriHex);

    const typeLen = parseInt(data.slice(typeOffset, typeOffset + 64), 16);
    const typeHex = data.slice(typeOffset + 64, typeOffset + 64 + typeLen * 2);
    const agentType = hexToUtf8(typeHex);

    return { agentURI, active, agentType, registeredAt };
  } catch {
    return null;
  }
}

interface AgentRecord {
  name: string;
  fullName: string;
  address: string;
  chain: string;
  identityType: string;
  description: string;
  active: boolean;
  registeredAt: number;
  explorerUrl: string;
}

function parseUriToName(uri: string, address: string): { name: string; description: string; type?: string } {
  // agentURI may be a JSON blob, a .vns name, or a URL
  if (!uri) return { name: `agent-${address.slice(2, 8).toLowerCase()}`, description: '' };

  // Try JSON first
  try {
    const parsed = JSON.parse(uri);
    const rawName = parsed.name || '';
    const cleanName = rawName.replace(/\.vns$/, '').toLowerCase().trim();
    const safeName = cleanName && /^[a-z0-9][a-z0-9-]*$/.test(cleanName)
      ? cleanName
      : `agent-${address.slice(2, 8).toLowerCase()}`;
    return {
      name: safeName,
      description: parsed.description || parsed.desc || '',
      type: parsed.type,
    };
  } catch { /* not JSON */ }

  // URL
  if (uri.startsWith('http://') || uri.startsWith('https://')) {
    try {
      const url = new URL(uri);
      const parts = url.pathname.split('/').filter(Boolean);
      const last = parts[parts.length - 1] || '';
      const extracted = last.replace(/\.vns$/, '').toLowerCase().trim();
      return {
        name: extracted && /^[a-z0-9][a-z0-9-]*$/.test(extracted)
          ? extracted
          : `agent-${address.slice(2, 8).toLowerCase()}`,
        description: '',
      };
    } catch { /* fall through */ }
  }

  // Plain string (maybe .vns name)
  const cleaned = uri.replace(/\.vns$/, '').toLowerCase().trim();
  return {
    name: /^[a-z0-9][a-z0-9-]*$/.test(cleaned) ? cleaned : `agent-${address.slice(2, 8).toLowerCase()}`,
    description: '',
  };
}

/** Run an array of tasks with bounded concurrency, preserving order. */
async function pMap<T, R>(items: T[], limit: number, fn: (x: T, i: number) => Promise<R>): Promise<R[]> {
  const results: R[] = new Array(items.length) as R[];
  let cursor = 0;
  const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
    while (true) {
      const i = cursor++;
      if (i >= items.length) break;
      results[i] = await fn(items[i], i);
    }
  });
  await Promise.all(workers);
  return results;
}

async function fetchChainAgents(chain: ChainKey): Promise<AgentRecord[]> {
  const { rpcs, registry, explorer } = CHAINS[chain];

  // 1. Get total count
  const countHex = await rpcCall(rpcs, SEL_TOTAL_AGENTS, registry);
  if (!countHex) return [];
  const total = parseInt(countHex, 16);
  if (total === 0) return [];

  // 2. Fetch each address via registeredAgents(i) — limit concurrency to 4
  //    to avoid rate-limits and the knock-on failure when one RPC chokes.
  const addressResults = await pMap(
    Array.from({ length: total }, (_, i) => i),
    4,
    (i) => rpcCall(rpcs, SEL_REGISTERED_AGENT + encodeUint(i), registry)
  );
  const addresses = addressResults
    .map(r => (r ? decodeAddress(r) : ''))
    .filter(a => a && a !== '0x0000000000000000000000000000000000000000');

  // 3. Fetch metadata via getAgent(address) — same concurrency limit
  const metaResults = await pMap(
    addresses,
    4,
    (addr) => rpcCall(rpcs, SEL_GET_AGENT + encodeAddress(addr), registry)
  );

  const records: AgentRecord[] = [];
  for (let i = 0; i < addresses.length; i++) {
    const addr = addresses[i];
    const metaHex = metaResults[i];
    if (!metaHex) continue;
    const decoded = decodeGetAgent(metaHex);
    if (!decoded) continue;
    const { name, description, type } = parseUriToName(decoded.agentURI, addr);
    records.push({
      name,
      fullName: `${name}.vns`,
      address: addr,
      chain,
      identityType: type || decoded.agentType || 'agent',
      description,
      active: decoded.active,
      registeredAt: decoded.registeredAt,
      explorerUrl: `${explorer}/address/${addr}`,
    });
  }
  return records;
}

// ── Simple cache ───────────────────────────────────────────────────────────
let _cache: { agents: AgentRecord[]; ts: number } | null = null;
const CACHE_TTL = 120_000; // 2 min

function isValidEthAddress(addr: string): boolean {
  return /^0x[a-fA-F0-9]{40}$/.test(addr);
}

export async function GET(request: Request) {
  const url = new URL(request.url);
  const chainFilter = url.searchParams.get('chain');
  const addressFilter = url.searchParams.get('address');
  const format = url.searchParams.get('format');
  const noCache = url.searchParams.get('nocache') === '1';

  if (chainFilter && !CHAIN_KEYS.includes(chainFilter as ChainKey)) {
    return NextResponse.json(
      {
        error: `Invalid chain parameter: "${chainFilter}". Valid chains are: ${CHAIN_KEYS.join(', ')}`,
        validChains: CHAIN_KEYS,
      },
      { status: 400 },
    );
  }

  if (addressFilter && !isValidEthAddress(addressFilter)) {
    return NextResponse.json(
      { error: 'Invalid address format. Must be a 0x-prefixed 40-character hex string.' },
      { status: 400 },
    );
  }

  try {
    // Fast path: single address lookup on specific chain(s), no cache
    if (addressFilter) {
      const chains = chainFilter ? [chainFilter as ChainKey] : CHAIN_KEYS;
      const results: AgentRecord[] = [];
      for (const chain of chains) {
        const { rpcs, registry, explorer } = CHAINS[chain];
        const metaHex = await rpcCall(rpcs, SEL_GET_AGENT + encodeAddress(addressFilter), registry);
        if (!metaHex) continue;
        const decoded = decodeGetAgent(metaHex);
        if (!decoded || decoded.registeredAt === 0) continue;
        const { name, description, type } = parseUriToName(decoded.agentURI, addressFilter);
        results.push({
          name,
          fullName: `${name}.vns`,
          address: addressFilter,
          chain,
          identityType: type || decoded.agentType || 'agent',
          description,
          active: decoded.active,
          registeredAt: decoded.registeredAt,
          explorerUrl: `${explorer}/address/${addressFilter}`,
        });
      }
      return NextResponse.json({
        agents: results,
        count: results.length,
        query: { address: addressFilter, chain: chainFilter },
      }, {
        headers: {
          'Access-Control-Allow-Origin': '*',
          'Cache-Control': 'public, max-age=30',
        },
      });
    }

    // Full directory — use cache
    if (!noCache && _cache && Date.now() - _cache.ts < CACHE_TTL) {
      const agents = chainFilter
        ? _cache.agents.filter(a => a.chain === chainFilter)
        : _cache.agents;
      const output = format === 'minimal'
        ? agents.map(a => ({ name: a.name, address: a.address, chain: a.chain }))
        : agents;
      return NextResponse.json({
        agents: output,
        count: agents.length,
        total: _cache.agents.length,
        chains: CHAIN_KEYS,
        cached: true,
      }, {
        headers: {
          'Access-Control-Allow-Origin': '*',
          'Cache-Control': 'public, max-age=60',
        },
      });
    }

    // Fetch all chains in parallel
    const chainResults = await Promise.all(CHAIN_KEYS.map(fetchChainAgents));
    const allAgents = chainResults.flat();
    _cache = { agents: allAgents, ts: Date.now() };

    const agents = chainFilter
      ? allAgents.filter(a => a.chain === chainFilter)
      : allAgents;
    const output = format === 'minimal'
      ? agents.map(a => ({ name: a.name, address: a.address, chain: a.chain }))
      : agents;

    return NextResponse.json({
      agents: output,
      count: agents.length,
      total: allAgents.length,
      chains: CHAIN_KEYS,
      cached: false,
    }, {
      headers: {
        'Access-Control-Allow-Origin': '*',
        'Cache-Control': 'public, max-age=60',
      },
    });
  } catch (err) {
    console.error('[agent/discover] Error:', err);
    return NextResponse.json(
      { error: 'Failed to fetch agent directory' },
      { status: 500 }
    );
  }
}
