/**
 * GET/POST /api/x402/contract-call
 *
 * Generic priced contract-call endpoint — $0.01 USDC per read.
 *
 * Usage (POST):
 *   POST /api/x402/contract-call
 *   X-PAYMENT: <x402 payment payload>
 *   { "chain": "base", "contract": "ERC8004IdentityRegistry"|"0xa7BD...A81A",
 *     "function": "isAgentActive(address)" | "isAgentActive",
 *     "args": ["0xfA15Ee..."] }
 *
 * Usage (GET):
 *   GET /api/x402/contract-call?chain=base&contract=ERC8004IdentityRegistry
 *       &function=isAgentActive&args=0xfA15Ee...
 *   (multiple args via repeated &args=... or comma-separated &args=a,b,c)
 *
 * Hard rules:
 *   - Allowlist: only the 134 Vaultfire contracts (loaded from app/lib/contracts.ts)
 *   - Read-only: only view/pure functions — state-changing calls rejected
 *   - 4 chains: base, avalanche, arbitrum, polygon
 *
 * Returns the decoded result + on-chain provenance (block number, gas used hint via eth_call).
 */

import { NextRequest, NextResponse } from 'next/server';
import { withX402 } from 'x402-next';
import { withActionLogging } from '../actions/_lib/log-action';
import { facilitator } from '@coinbase/x402';
import { encodeFunctionData, decodeFunctionResult } from 'viem';

import { resolveContract, TOTAL_ALLOWLISTED } from './_lib/registry';
import { resolveFunction, coerceArgs } from './_lib/function-resolver';

const PAY_TO = '0xfA15Ee28939B222B0448261A22156070f0A7813C';

const CORS_HEADERS = {
  'Access-Control-Allow-Origin': '*',
  'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
  'Access-Control-Allow-Headers': 'Content-Type, X-PAYMENT',
};

export async function OPTIONS() {
  return new NextResponse(null, { status: 204, headers: CORS_HEADERS });
}

type CallSpec = {
  chain?: string;
  contract: string;
  function: string;
  args: unknown[];
};

function jsonError(status: number, error: string, hint?: string) {
  return NextResponse.json(
    { ok: false, error, ...(hint ? { hint } : {}) },
    { status, headers: CORS_HEADERS },
  );
}

/** Stringify bigints / etc. so JSON.stringify doesn't throw. */
function jsonSafe(v: any): any {
  if (typeof v === 'bigint') return v.toString();
  if (Array.isArray(v)) return v.map(jsonSafe);
  if (v && typeof v === 'object') {
    const out: Record<string, any> = {};
    for (const [k, val] of Object.entries(v)) out[k] = jsonSafe(val);
    return out;
  }
  return v;
}

async function executeCall(spec: CallSpec, originUrl: string) {
  // 1. Resolve contract against allowlist
  const entry = resolveContract({ chain: spec.chain, contract: spec.contract });
  if (!entry) {
    return jsonError(
      400,
      `Contract not in allowlist: ${spec.contract}${spec.chain ? ` on ${spec.chain}` : ''}`,
      `Only the ${TOTAL_ALLOWLISTED} Vaultfire contracts are callable. Use /api/x402/trust/contracts to list them.`,
    );
  }

  // 2. Resolve function
  const fn = resolveFunction(entry.abi, spec.function);
  if ('error' in fn) {
    return jsonError(400, fn.error, fn.hint);
  }

  // 3. Coerce args
  let coercedArgs: unknown[];
  try {
    coercedArgs = coerceArgs(spec.args ?? [], fn.abiItem.inputs);
  } catch (e) {
    return jsonError(400, `Argument error: ${(e as Error).message}`);
  }

  // 4. Encode calldata
  let calldata: `0x${string}`;
  try {
    calldata = encodeFunctionData({
      abi: [fn.abiItem],
      functionName: fn.abiItem.name,
      args: coercedArgs as any,
    });
  } catch (e) {
    return jsonError(400, `Failed to encode calldata: ${(e as Error).message}`);
  }

  // 5. Make the eth_call
  let rpcResponse: any;
  let blockNumber: string | null = null;
  try {
    const [callRes, blockRes] = await Promise.all([
      fetch(entry.rpcUrl, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          jsonrpc: '2.0',
          id: 1,
          method: 'eth_call',
          params: [{ to: entry.address, data: calldata }, 'latest'],
        }),
        signal: AbortSignal.timeout(12_000),
      }),
      fetch(entry.rpcUrl, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'eth_blockNumber', params: [] }),
        signal: AbortSignal.timeout(8_000),
      }),
    ]);
    rpcResponse = await callRes.json();
    const bnJson = await blockRes.json();
    blockNumber = bnJson?.result ?? null;
  } catch (e) {
    return jsonError(502, `RPC call failed: ${(e as Error).message}`);
  }

  if (rpcResponse?.error) {
    return jsonError(
      502,
      `On-chain call reverted or RPC error: ${rpcResponse.error.message ?? 'unknown'}`,
      'This usually means the function reverted (bad inputs) or the RPC is rate-limited.',
    );
  }

  const rawHex: string = rpcResponse?.result ?? '0x';

  // 6. Decode result
  let decoded: any;
  try {
    decoded = decodeFunctionResult({
      abi: [fn.abiItem],
      functionName: fn.abiItem.name,
      data: rawHex as `0x${string}`,
    });
  } catch (e) {
    return jsonError(
      502,
      `Failed to decode return data: ${(e as Error).message}`,
      'The contract returned data that does not match the ABI.',
    );
  }

  return NextResponse.json(
    {
      ok: true,
      chain: entry.chain,
      contract: { name: entry.name, address: entry.address },
      function: fn.signature,
      inputs: fn.abiItem.inputs.map((i, idx) => ({
        name: i.name || `arg${idx}`,
        type: i.type,
        value: jsonSafe(coercedArgs[idx]),
      })),
      result: jsonSafe(decoded),
      raw: rawHex,
      block: blockNumber,
      explorer: `${entry.explorerUrl}/address/${entry.address}#readContract`,
      _x402: {
        paid: true,
        price: '$0.01 USDC',
        service: 'vaultfire-trust',
        facilitator: 'cdp',
      },
    },
    { headers: { ...CORS_HEADERS, 'Cache-Control': 'no-store' } },
  );
}

const postHandler = async (req: NextRequest): Promise<NextResponse<unknown>> => {
  let body: CallSpec;
  try {
    body = (await req.json()) as CallSpec;
  } catch {
    return jsonError(400, 'Invalid JSON body');
  }
  if (!body || !body.contract || !body.function) {
    return jsonError(
      400,
      'Body must include { chain?, contract, function, args[] }',
      'Example: { "chain": "base", "contract": "ERC8004IdentityRegistry", "function": "isAgentActive(address)", "args": ["0xfA15..."] }',
    );
  }
  return executeCall(
    {
      chain: body.chain,
      contract: body.contract,
      function: body.function,
      args: Array.isArray(body.args) ? body.args : [],
    },
    req.nextUrl.origin,
  );
};

const getHandler = async (req: NextRequest): Promise<NextResponse<unknown>> => {
  const url = req.nextUrl;
  const chain = url.searchParams.get('chain') ?? undefined;
  const contract = url.searchParams.get('contract');
  const fn = url.searchParams.get('function');
  if (!contract || !fn) {
    return jsonError(
      400,
      'Query params required: contract, function (chain optional)',
      'Example: ?chain=base&contract=ERC8004IdentityRegistry&function=isAgentActive&args=0xfA15...',
    );
  }
  // Args: support repeated ?args=...&args=... OR a single comma-separated ?args=a,b,c
  const argEntries = url.searchParams.getAll('args');
  let args: string[] = [];
  if (argEntries.length === 1 && argEntries[0].includes(',')) {
    args = argEntries[0].split(',').map((s) => s.trim()).filter(Boolean);
  } else {
    args = argEntries;
  }
  return executeCall(
    { chain: chain ?? undefined, contract, function: fn, args },
    req.nextUrl.origin,
  );
};

const x402Config = {
  price: '$0.01',
  network: 'base' as const,
  config: {
    description:
      'Generic on-chain read against any of the 134 Vaultfire Protocol contracts across Base, Avalanche, Arbitrum, and Polygon. Pass { chain, contract (name or address), function, args } and receive the decoded return value plus block provenance. Read-only (view/pure) calls only.',
    mimeType: 'application/json',
    outputSchema: {
      input: {
        type: 'http',
        method: 'POST',
        discoverable: true,
        bodySchema: {
          type: 'object',
          required: ['contract', 'function'],
          properties: {
            chain: { type: 'string', enum: ['base', 'avalanche', 'arbitrum', 'polygon'] },
            contract: { type: 'string', description: 'Contract name or 0x-address' },
            function: { type: 'string', description: 'Name or canonical signature, e.g. isAgentActive(address)' },
            args: { type: 'array', items: {} },
          },
        },
      },
      output: {
        type: 'object',
        properties: {
          ok: { type: 'boolean' },
          chain: { type: 'string' },
          contract: { type: 'object' },
          function: { type: 'string' },
          inputs: { type: 'array' },
          result: {},
          raw: { type: 'string' },
          block: { type: 'string' },
          explorer: { type: 'string' },
        },
      },
    },
  },
};

// Wrap each handler with payer-attribution logging so paid calls appear on /live.
// withActionLogging is a no-op observer — it doesn't change request/response
// behavior, just emits a structured log line and persists to KV.
const PRICE = '$0.01';
export const POST = withX402(
  withActionLogging(postHandler, PRICE, '/x402/contract-call'),
  PAY_TO,
  x402Config,
  facilitator,
);
export const GET = withX402(
  withActionLogging(getHandler, PRICE, '/x402/contract-call'),
  PAY_TO,
  x402Config,
  facilitator,
);
