'use client';

/**
 * /sign/x402-register
 *
 * One-tap CDP Bazaar registration sweep for all 72 paid Vaultfire endpoints.
 *
 * Per the official x402 Bazaar docs:
 *   "The facilitator catalogs your service the first time it processes a
 *    payment (verify + settle) for that endpoint."
 *
 * Our trust routes use a hand-rolled verifier that settles EIP-3009 directly
 * on-chain — CDP never sees those payments, so they never get cataloged.
 * To work around this we POST every signed payment to /api/x402/cdp-register,
 * a server-side proxy that forwards verify+settle straight to CDP's
 * facilitator with `resource.url` set to the route we want cataloged. CDP
 * indexes the resource as a side-effect of the settle.
 *
 * The connected wallet is also the payTo wallet (ghostkey.base.eth), so this
 * is a pure self-pay sweep — only cost is Base gas + tiny USDC self-transfers.
 *
 * Per-endpoint flow:
 *   1. Build PaymentRequirements client-side with `resource = <route URL>`
 *   2. Sign EIP-3009 transferWithAuthorization (createPaymentHeader v2)
 *   3. POST {paymentHeader, resourceUrl, amount} → /api/x402/cdp-register
 *   4. Proxy runs CDP /verify + /settle → bazaar catalogs the resource URL
 *
 * Idempotent: re-running just refreshes lastUpdated timestamps in CDP.
 */

import React, { useEffect, useState } from 'react';
import { useWalletAuth } from '../../lib/WalletAuthContext';
import { createPaymentHeader } from 'x402/client';
import { createWalletClient, custom } from 'viem';
import { base } from 'viem/chains';

const MANIFEST_URL = 'https://theloopbreaker.com/x402-monetize.json';
const BASE_CHAIN_HEX = '0x2105';
const BASE_CHAIN_ID = 8453;

// The 3 routes confirmed missing from CDP bazaar after the original sweep.
// Verified via paginated CDP discovery on 2026-05-08. Hitting
// /sign/x402-register?missing=1 narrows the sweep to just these.
const MISSING_PATHS = [
  '/api/x402/oracle/agent-stake-usd',
  '/api/x402/oracle/price-feed',
  '/api/x402/trust/email-verify',
];

interface PaidRoute {
  path: string;
  method: string;
  price: string;
  description?: string;
}

const PAY_TO = '0xfA15Ee28939B222B0448261A22156070f0A7813C';
const USDC_BASE = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';

// Wallets allowed to run the sweep. CDP V2 facilitator blocks self-send
// (from == to) at /verify, so ghostkey.base.eth alone cannot register routes.
// The burner pays ghostkey and registers each route as a side effect; funds
// flow back to ghostkey at the end of the sweep.
const GHOSTKEY = '0xfA15Ee28939B222B0448261A22156070f0A7813C';
const BURNER   = '0xb7d84310847f5a2288CFE77638B184b0618827c5';
const ALLOWED_PAYERS = new Set(
  [GHOSTKEY, BURNER].map((a) => a.toLowerCase()),
);

/**
 * Convert a manifest price string like "$0.001" or "0.001 USDC" or
 * "1000" (already micros) to USDC base units (6 decimals).
 */
function priceToMicros(price: string): string {
  const trimmed = price.trim();
  if (/^\d+$/.test(trimmed)) return trimmed; // already micros
  const m = trimmed.match(/([0-9]*\.?[0-9]+)/);
  if (!m) throw new Error(`Cannot parse price: ${price}`);
  const dollars = parseFloat(m[1]);
  if (!isFinite(dollars) || dollars <= 0) {
    throw new Error(`Invalid price: ${price}`);
  }
  // Round to nearest micro to avoid floating-point drift
  return String(Math.round(dollars * 1_000_000));
}

interface RouteState {
  path: string;
  method: string;
  status: 'pending' | 'paying' | 'success' | 'error' | 'skipped';
  message?: string;
}

type LogLine = { ts: string; msg: string; level: 'info' | 'ok' | 'err' };

export default function X402RegisterSweepPage() {
  const { address, chainId, connect } = useWalletAuth();
  const [routes, setRoutes] = useState<PaidRoute[]>([]);
  const [routeStates, setRouteStates] = useState<RouteState[]>([]);
  const [running, setRunning] = useState(false);
  const [logs, setLogs] = useState<LogLine[]>([]);
  const [doneCount, setDoneCount] = useState(0);
  const [errCount, setErrCount] = useState(0);

  const log = (msg: string, level: 'info' | 'ok' | 'err' = 'info') =>
    setLogs((prev) => [
      ...prev,
      { ts: new Date().toLocaleTimeString(), msg, level },
    ]);

  // Optional URL filter: ?only=path1,path2  or  ?missing=1  to target only
  // the 3 routes that didn't make it through the original sweep.
  //
  // CRITICAL: We compute filterPaths/filterMode SYNCHRONOUSLY from
  // window.location during the initial render so the manifest-loading
  // effect already has them on its very first run. An earlier version
  // used two effects (one to read URL params, another to fetch the
  // manifest) which caused two manifest fetches to race — the unfiltered
  // fetch from the initial render sometimes resolved last and clobbered
  // the filtered results, loading 72 routes when only 3 were intended.
  function readFilterFromUrl(): {
    paths: string[] | null;
    mode: 'all' | 'only' | 'missing';
  } {
    if (typeof window === 'undefined') return { paths: null, mode: 'all' };
    const params = new URLSearchParams(window.location.search);
    if (params.get('missing') === '1') {
      return { paths: MISSING_PATHS, mode: 'missing' };
    }
    const only = params.get('only');
    if (only) {
      const list = only
        .split(',')
        .map((p) => p.trim())
        .filter(Boolean)
        .map((p) => (p.startsWith('/') ? p : `/${p}`));
      if (list.length > 0) return { paths: list, mode: 'only' };
    }
    return { paths: null, mode: 'all' };
  }

  const [filterState] = useState(readFilterFromUrl);
  const filterPaths = filterState.paths;
  const filterMode = filterState.mode;

  // Load manifest once. The filter is captured at first render (above),
  // so this effect runs exactly once and applies the correct subset.
  useEffect(() => {
    (async () => {
      try {
        const res = await fetch(MANIFEST_URL, { cache: 'no-store' });
        const m = await res.json();
        let paid: PaidRoute[] = (m.routes || []).filter(
          (r: PaidRoute) => r.price && r.price !== 'free',
        );
        if (filterPaths && filterPaths.length > 0) {
          // Match either exact path or suffix match. Suffix match means
          // `?only=oracle/price-feed` resolves to `/api/x402/oracle/price-feed`
          // without the user needing to know the full prefix.
          const wanted = filterPaths.map((p) => p.toLowerCase());
          paid = paid.filter((r) => {
            const rp = r.path.toLowerCase();
            return wanted.some(
              (w) => rp === w || rp.endsWith(w) || rp.endsWith('/' + w.replace(/^\/+/, '')),
            );
          });
          if (paid.length === 0) {
            log(
              `Filter matched 0 routes — check that ?only= paths exist in manifest`,
              'err',
            );
          } else if (filterMode === 'only') {
            log(
              `Filter matched ${paid.length} routes: ${paid.map((r) => r.path).join(', ')}`,
              'ok',
            );
          }
        }
        setRoutes(paid);
        setRouteStates(
          paid.map((r) => ({
            path: r.path,
            method: r.method || 'GET',
            status: 'pending',
          })),
        );
        const label =
          filterMode === 'missing'
            ? `${paid.length} missing-only routes`
            : filterMode === 'only'
              ? `${paid.length} filtered routes`
              : `${paid.length} paid routes`;
        log(`Loaded ${label} from manifest`, 'ok');
      } catch (e: any) {
        log(`Failed to load manifest: ${e?.message ?? e}`, 'err');
      }
    })();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  function updateRoute(idx: number, patch: Partial<RouteState>) {
    setRouteStates((prev) => {
      const next = [...prev];
      next[idx] = { ...next[idx], ...patch };
      return next;
    });
  }

  async function ensureBase() {
    if (chainId === BASE_CHAIN_ID) return;
    log('Switching wallet to Base mainnet…');
    try {
      await (window as any).ethereum.request({
        method: 'wallet_switchEthereumChain',
        params: [{ chainId: BASE_CHAIN_HEX }],
      });
    } catch (e: any) {
      throw new Error(`Failed to switch to Base: ${e?.message ?? e}`);
    }
  }

  async function payOnce(route: PaidRoute, idx: number) {
    const resourceUrl = `https://theloopbreaker.com${route.path}`;
    const amountMicros = priceToMicros(route.price);

    updateRoute(idx, { status: 'paying', message: 'building requirements' });

    // 1. Build PaymentRequirements ourselves — the proxy will mirror this
    //    exact shape when it calls CDP /verify + /settle. Setting `resource`
    //    to this route's URL is what makes CDP catalog *this specific URL*.
    const requirements = {
      scheme: 'exact' as const,
      network: 'base' as const,
      maxAmountRequired: amountMicros,
      resource: resourceUrl,
      description: route.description || `Vaultfire ${route.method} ${route.path}`,
      mimeType: 'application/json',
      payTo: PAY_TO,
      // 10 min window. SDK uses this to compute validBefore = now + maxTimeoutSeconds.
      // 60s was too tight: phone -> page -> proxy -> CDP regularly takes >60s on
      // mobile, causing invalid_exact_evm_payload_authorization_valid_before.
      maxTimeoutSeconds: 600,
      asset: USDC_BASE,
      extra: {
        name: 'USD Coin',
        version: '2',
      },
    };

    // 2. Build viem wallet client from injected provider
    const wallet = createWalletClient({
      account: address as `0x${string}`,
      chain: base,
      transport: custom((window as any).ethereum),
    });

    // 3. Sign EIP-3009 transferWithAuthorization (x402 v2 — base64 string)
    updateRoute(idx, { message: 'awaiting wallet signature' });
    const paymentHeader = await createPaymentHeader(
      wallet as any,
      2,
      requirements as any,
    );

    // 4. POST to the cdp-register proxy — it forwards to CDP facilitator
    updateRoute(idx, { message: 'submitting to CDP via proxy' });
    const proxyRes = await fetch('/api/x402/cdp-register', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        paymentHeader,
        resourceUrl,
        amount: amountMicros,
        description: requirements.description,
      }),
    });

    let proxyJson: any = null;
    try {
      proxyJson = await proxyRes.json();
    } catch {
      throw new Error(`proxy returned non-JSON (${proxyRes.status})`);
    }

    if (proxyRes.ok && proxyJson?.success) {
      updateRoute(idx, {
        status: 'success',
        message: proxyJson.txHash
          ? `cataloged · tx ${proxyJson.txHash.slice(0, 10)}…`
          : 'cataloged',
      });
      return;
    }

    // Surface CDP's specific invalidReason / errorMessage so we can debug.
    const cdp = proxyJson?.cdpResponse || {};
    const detail =
      cdp.invalidReason ||
      cdp.errorMessage ||
      cdp.errorType ||
      proxyJson?.error ||
      `proxy ${proxyRes.status}`;
    const stage = proxyJson?.stage ? ` [${proxyJson.stage}]` : '';
    log(`  CDP detail: ${JSON.stringify(cdp).slice(0, 240)}`, 'err');
    throw new Error(`${detail}${stage}`);
  }

  async function runAll() {
    setRunning(true);
    setLogs([]);
    setDoneCount(0);
    setErrCount(0);
    try {
      if (!address) {
        log('Connecting wallet…');
        await connect();
        await new Promise((r) => setTimeout(r, 600));
      }
      if (address && !ALLOWED_PAYERS.has(address.toLowerCase())) {
        log(`Refusing to sweep from non-allowlisted wallet ${address}`, 'err');
        setRunning(false);
        return;
      }
      await ensureBase();

      log(`▶ Starting registration sweep across ${routes.length} routes`);
      log('Wallet will prompt for one EIP-3009 signature per route.');
      log('Each is a tiny USDC transfer back to ghostkey.base.eth (self-pay).');

      for (let i = 0; i < routes.length; i++) {
        const r = routes[i];
        log(`[${i + 1}/${routes.length}] ${r.method} ${r.path}`);
        try {
          await payOnce(r, i);
          setDoneCount((c) => c + 1);
          log(`  ✅ cataloged`, 'ok');
        } catch (e: any) {
          setErrCount((c) => c + 1);
          updateRoute(i, { status: 'error', message: e?.message ?? 'error' });
          log(`  ❌ ${e?.message ?? e}`, 'err');
          // Pause briefly so you can decide to keep going if something serial fails
          await new Promise((r) => setTimeout(r, 250));
        }
      }
      log('━━━ SWEEP COMPLETE ━━━', 'ok');
      log(`${doneCount} cataloged, ${errCount} errors`, errCount === 0 ? 'ok' : 'err');
    } catch (e: any) {
      log(`FATAL: ${e?.message ?? String(e)}`, 'err');
    } finally {
      setRunning(false);
    }
  }

  async function retryFailed() {
    setRunning(true);
    try {
      await ensureBase();
      const failedIdx = routeStates
        .map((s, i) => ({ s, i }))
        .filter(({ s }) => s.status === 'error');
      log(`▶ Retrying ${failedIdx.length} failed routes`);
      for (const { i } of failedIdx) {
        const r = routes[i];
        log(`[retry] ${r.method} ${r.path}`);
        try {
          await payOnce(r, i);
          setDoneCount((c) => c + 1);
          setErrCount((c) => Math.max(0, c - 1));
          log(`  ✅ cataloged`, 'ok');
        } catch (e: any) {
          log(`  ❌ ${e?.message ?? e}`, 'err');
        }
      }
      log('━━━ RETRY COMPLETE ━━━', 'ok');
    } catch (e: any) {
      log(`FATAL: ${e?.message ?? String(e)}`, 'err');
    } finally {
      setRunning(false);
    }
  }

  // Index of the next pending (or errored) route to sign — drives the
  // “Sign next” button and lets the user step through one at a time.
  const nextPendingIdx = routeStates.findIndex(
    (s) => s.status === 'pending' || s.status === 'error',
  );

  // Sign one specific route by index. Used by the per-row sign button and
  // by signNext(). Reuses the same payOnce + bookkeeping logic.
  async function signOne(idx: number) {
    if (running) return;
    if (idx < 0 || idx >= routes.length) return;
    setRunning(true);
    try {
      if (!address) {
        log('Connecting wallet…');
        await connect();
        await new Promise((r) => setTimeout(r, 600));
      }
      // Hard guard: refuse to sign from any wallet that isn't allowlisted.
      // Prevents accidentally mass-signing from a wrong wallet on mobile.
      if (address && !ALLOWED_PAYERS.has(address.toLowerCase())) {
        log(`Refusing to sign from non-allowlisted wallet ${address}`, 'err');
        setRunning(false);
        return;
      }
      await ensureBase();
      const r = routes[idx];
      const wasError = routeStates[idx]?.status === 'error';
      log(`[one] ${r.method} ${r.path}`);
      try {
        await payOnce(r, idx);
        setDoneCount((c) => c + 1);
        if (wasError) setErrCount((c) => Math.max(0, c - 1));
        log(`  ✅ cataloged`, 'ok');
      } catch (e: any) {
        if (!wasError) setErrCount((c) => c + 1);
        updateRoute(idx, { status: 'error', message: e?.message ?? 'error' });
        log(`  ❌ ${e?.message ?? e}`, 'err');
      }
    } catch (e: any) {
      log(`FATAL: ${e?.message ?? String(e)}`, 'err');
    } finally {
      setRunning(false);
    }
  }

  // “Sign next” — finds the first pending/errored route and signs just that one.
  async function signNext() {
    if (nextPendingIdx === -1) return;
    await signOne(nextPendingIdx);
  }

  const totalPaid = routes.length;
  const totalCostUsd = routes.reduce((sum, r) => {
    try {
      return sum + Number(priceToMicros(r.price)) / 1_000_000;
    } catch {
      return sum;
    }
  }, 0);
  const totalCost = totalPaid > 0
    ? `~$${totalCostUsd.toFixed(3)} USDC + Base gas`
    : '—';

  return (
    <div className="min-h-screen bg-black text-white px-5 py-8 font-mono">
      <div className="max-w-3xl mx-auto">
        <h1 className="text-2xl mb-1">CDP Bazaar Registration Sweep</h1>
        <p className="text-sm text-gray-400 mb-6">
          Self-pay every paid Vaultfire endpoint once so the CDP facilitator catalogs
          it in the x402 Bazaar discovery API. Only cost is Base gas — payments go
          back to ghostkey.base.eth.
        </p>

        {filterMode === 'missing' && (
          <div className="mb-4 p-3 bg-blue-950 border border-blue-700 rounded text-xs text-blue-200">
            <strong>Targeted mode:</strong> only signing the 3 routes that were
            missing from CDP bazaar after the first sweep.
            <ul className="mt-1 ml-4 list-disc text-blue-300">
              {MISSING_PATHS.map((p) => (
                <li key={p}>{p}</li>
              ))}
            </ul>
          </div>
        )}
        {filterMode === 'only' && (
          <div className="mb-4 p-3 bg-blue-950 border border-blue-700 rounded text-xs text-blue-200">
            <strong>Filtered mode:</strong> only signing {filterPaths?.length ?? 0} route(s)
            from the <code>?only=</code> query.
          </div>
        )}

        {address && !ALLOWED_PAYERS.has(address.toLowerCase()) && (
          <div className="mb-4 p-3 bg-red-950 border border-red-700 rounded text-xs text-red-300">
            <strong>Wrong wallet connected.</strong> This sweep is locked to the
            burner ({BURNER.slice(0, 10)}…) or ghostkey.base.eth. Disconnect and
            reconnect with the burner wallet.
          </div>
        )}
        {address?.toLowerCase() === GHOSTKEY.toLowerCase() && (
          <div className="mb-4 p-3 bg-yellow-950 border border-yellow-700 rounded text-xs text-yellow-300">
            <strong>Note:</strong> ghostkey is connected, but CDP blocks
            self-pay (from == to). Connect the burner wallet instead so
            payments flow burner → ghostkey.
          </div>
        )}
        {address?.toLowerCase() === BURNER.toLowerCase() && (
          <div className="mb-4 p-3 bg-green-950 border border-green-700 rounded text-xs text-green-300">
            <strong>Burner connected.</strong> Each signature pays ghostkey.base.eth.
            Funds return to you when the sweep completes.
          </div>
        )}

        <div className="mb-4 p-3 bg-gray-900 rounded border border-gray-800 text-xs space-y-1">
          <div>
            Wallet:{' '}
            {address ? (
              <span className="text-green-400">{address}</span>
            ) : (
              <span className="text-gray-500">not connected</span>
            )}
          </div>
          <div>
            Pays to: <span className="text-green-400">{PAY_TO}</span>{' '}
            <span className="text-gray-500">(ghostkey.base.eth)</span>
          </div>
          <div>
            Chain: {chainId ?? '—'}{' '}
            {chainId === BASE_CHAIN_ID ? '(Base ✓)' : chainId ? '(will switch to Base)' : ''}
          </div>
          <div>Routes loaded: {routes.length}</div>
          <div>Estimated cost: {totalCost}</div>
        </div>

        <div className="flex flex-col gap-3 mb-6">
          <button
            onClick={signNext}
            disabled={running || routes.length === 0 || nextPendingIdx === -1}
            className="w-full py-4 bg-green-500 hover:bg-green-400 disabled:bg-gray-700 disabled:cursor-not-allowed rounded-lg text-black font-bold text-lg"
          >
            {running
              ? 'Signing…'
              : !address
                ? 'Connect wallet'
                : nextPendingIdx === -1
                  ? 'All routes done'
                  : `Sign next route (${nextPendingIdx + 1}/${routes.length})`}
          </button>
          <div className="flex gap-3">
            <button
              onClick={runAll}
              disabled={running || routes.length === 0}
              className="flex-1 py-3 bg-orange-500 hover:bg-orange-400 disabled:bg-gray-700 disabled:cursor-not-allowed rounded-lg text-black font-semibold text-sm"
            >
              {running ? 'Running…' : `Run all remaining (${Math.max(0, routes.length - doneCount - errCount)})`}
            </button>
            {errCount > 0 && !running && (
              <button
                onClick={retryFailed}
                className="px-6 py-3 bg-yellow-500 hover:bg-yellow-400 rounded-lg text-black font-semibold text-sm"
              >
                Retry {errCount} failed
              </button>
            )}
          </div>
        </div>

        <div className="grid grid-cols-3 gap-3 mb-6 text-center text-sm">
          <div className="p-3 bg-gray-900 border border-gray-800 rounded">
            <div className="text-gray-500">cataloged</div>
            <div className="text-2xl text-green-400">{doneCount}</div>
          </div>
          <div className="p-3 bg-gray-900 border border-gray-800 rounded">
            <div className="text-gray-500">remaining</div>
            <div className="text-2xl text-orange-400">
              {Math.max(0, routes.length - doneCount - errCount)}
            </div>
          </div>
          <div className="p-3 bg-gray-900 border border-gray-800 rounded">
            <div className="text-gray-500">errors</div>
            <div className="text-2xl text-red-400">{errCount}</div>
          </div>
        </div>

        <div className="bg-gray-950 border border-gray-800 rounded p-4 text-xs h-72 overflow-y-auto mb-6">
          {logs.length === 0 ? (
            <span className="text-gray-600">Logs will appear here…</span>
          ) : (
            logs.map((l, i) => (
              <div
                key={i}
                className={
                  l.level === 'ok'
                    ? 'text-green-400'
                    : l.level === 'err'
                      ? 'text-red-400'
                      : 'text-gray-300'
                }
              >
                <span className="text-gray-600">[{l.ts}]</span> {l.msg}
              </div>
            ))
          )}
        </div>

        {routeStates.length > 0 && (
          <details className="bg-gray-900 border border-gray-800 rounded p-3 text-xs">
            <summary className="cursor-pointer text-gray-300">
              Per-route status ({routeStates.filter((r) => r.status === 'success').length}/
              {routeStates.length} success)
            </summary>
            <div className="mt-3 space-y-1 max-h-96 overflow-y-auto">
              {routeStates.map((s, i) => (
                <div
                  key={i}
                  className={
                    s.status === 'success'
                      ? 'text-green-400'
                      : s.status === 'error'
                        ? 'text-red-400'
                        : s.status === 'paying'
                          ? 'text-yellow-400'
                          : s.status === 'skipped'
                            ? 'text-gray-500'
                            : 'text-gray-400'
                  }
                >
                  {s.status === 'pending' || s.status === 'error' ? (
                    <button
                      onClick={() => signOne(i)}
                      disabled={running}
                      className="mr-2 px-2 py-0.5 bg-blue-600 hover:bg-blue-500 disabled:bg-gray-700 disabled:cursor-not-allowed rounded text-white text-[10px]"
                    >
                      sign
                    </button>
                  ) : null}
                  {s.status === 'success'
                    ? '✅'
                    : s.status === 'error'
                      ? '❌'
                      : s.status === 'paying'
                        ? '⏳'
                        : s.status === 'skipped'
                          ? '⏭️'
                          : '·'}{' '}
                  <span className="text-gray-500">[{s.method}]</span> {s.path}
                  {s.message && <span className="text-gray-500"> — {s.message}</span>}
                </div>
              ))}
            </div>
          </details>
        )}
      </div>
    </div>
  );
}
