/**
 * Complete one live Vigil purchase with a CDP-managed wallet.
 *
 * The state file retains the original idempotency key and, once issued, the
 * opaque recovery token. It is deliberately private: do not commit, print,
 * or share it. A rerun after a pending response performs only a recovery GET;
 * it never creates a replacement payment authorization.
 */
import { chmod, readFile, rm, writeFile } from "node:fs/promises";
import { CdpX402Client } from "@coinbase/cdp-sdk/x402";
import { wrapFetchWithPayment } from "@x402/fetch";

const URL = "https://vigilnotary.com/api/agent/v1/verify-source";
const BASE = "eip155:8453";
const BASE_USDC = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
const RECIPIENT = "0xa4f4fe1d36eae9c7aadf2ea269b786549a27e523";
const STATE_FILE = process.env.VIGIL_PURCHASE_STATE_FILE || ".vigil-purchase-state.json";
const REQUEST = Object.freeze({
  source_url: "https://example.com",
  claim: "This domain can be used in documentation examples without needing permission.",
  options: { max_age_seconds: 0, max_price_usd: "0.03" },
});

function requestBody() {
  return JSON.stringify(REQUEST);
}

function validState(value) {
  return value && value.version === 1 &&
    ["quoted", "payment_attempted", "recovering"].includes(value.stage) &&
    typeof value.idempotencyKey === "string" && /^[A-Za-z0-9_.:-]{16,128}$/.test(value.idempotencyKey) &&
    JSON.stringify(value.request) === JSON.stringify(REQUEST) &&
    (value.requestId === undefined || typeof value.requestId === "string") &&
    (value.recoveryToken === undefined || typeof value.recoveryToken === "string");
}

async function saveState(state) {
  await writeFile(STATE_FILE, JSON.stringify(state) + "\n", { encoding: "utf8", mode: 0o600 });
  await chmod(STATE_FILE, 0o600);
}

async function loadOrCreateState() {
  try {
    const state = JSON.parse(await readFile(STATE_FILE, "utf8"));
    if (!validState(state)) throw new Error("The saved purchase state is invalid. Do not start another payment; inspect it privately first.");
    return state;
  } catch (error) {
    if (error && error.code !== "ENOENT") throw error;
    const state = {
      version: 1,
      stage: "quoted",
      idempotencyKey: `vigil-example-${crypto.randomUUID()}`,
      request: REQUEST,
    };
    await saveState(state);
    return state;
  }
}

async function clearState() {
  await rm(STATE_FILE, { force: true });
}

function parseError(text) {
  try {
    return JSON.parse(text).error?.message || text;
  } catch {
    return text;
  }
}

function validateChallenge(challenge) {
  const base = challenge.payment_required?.accepts?.find(item => item.network === BASE);
  if (
    challenge.quote?.price_usd !== "0.03" || challenge.quote?.amount_atomic !== "30000" ||
    challenge.quote?.resource_url !== URL || typeof challenge.quote?.request_id !== "string" ||
    base?.scheme !== "exact" || base?.amount !== "30000" ||
    base?.asset?.toLowerCase() !== BASE_USDC.toLowerCase() ||
    base?.payTo?.toLowerCase() !== RECIPIENT.toLowerCase()
  ) throw new Error("The live payment terms do not match this example's policy.");
}

function headersFor(state) {
  return {
    "Content-Type": "application/json",
    "Accept": "application/json",
    "Idempotency-Key": state.idempotencyKey,
  };
}

async function validateQuote(state) {
  const preview = await fetch(URL, { method: "POST", headers: headersFor(state), body: requestBody() });
  if (preview.status !== 402) throw new Error(`Expected HTTP 402, received ${preview.status}`);
  const challenge = await preview.json();
  validateChallenge(challenge);
  if (state.requestId && state.requestId !== challenge.quote.request_id) {
    throw new Error("The retained idempotency key resolved to a different request. Do not authorize a payment.");
  }
  state.requestId = challenge.quote.request_id;
  await saveState(state);
}

async function finish(response, state) {
  if (response.status === 202) {
    // The initial POST returns the token when it transitions into recovery.
    // Later recovery GETs may only return Retry-After, so retain the token already saved.
    const token = response.headers.get("X-Vigil-Recovery-Token") || state.recoveryToken;
    if (!token) {
      throw new Error("The purchase is pending but no recovery token was returned. Do not rerun or authorize another payment; contact Vigil support with the saved request ID only.");
    }
    state.recoveryToken = token;
    state.stage = "recovering";
    await saveState(state);
    console.log(JSON.stringify({ request_id: state.requestId, status: "pending", next_step: "Rerun this command later; it will only make an authenticated recovery request." }, null, 2));
    return;
  }
  const text = await response.text();
  if (response.status !== 200) {
    throw new Error(`Purchase did not complete with HTTP ${response.status}: ${parseError(text)}. The original purchase state was retained; do not authorize a replacement payment.`);
  }
  const result = JSON.parse(text);
  if (result.payment?.amount_atomic !== "30000" || result.payment?.status !== "settled") {
    throw new Error("The completed response did not contain the expected settlement.");
  }
  await clearState();
  console.log(JSON.stringify({
    request_id: result.request_id,
    claim_support: result.claim_support,
    evidence: result.evidence,
    transaction: result.payment.transaction,
    receipt_hash: result.receipt?.attestation?.receipt_hash,
  }, null, 2));
}

async function recover(state) {
  if (!state.requestId || !state.recoveryToken) {
    throw new Error("A payment attempt was recorded without a recovery token. Do not rerun or authorize a replacement payment; contact Vigil support with the saved request ID only.");
  }
  const response = await fetch(`${URL.replace("/verify-source", "/requests")}/${encodeURIComponent(state.requestId)}`, {
    method: "GET",
    headers: { "Accept": "application/json", "Authorization": `Bearer ${state.recoveryToken}` },
  });
  await finish(response, state);
}

async function main() {
  const state = await loadOrCreateState();
  if (state.stage === "recovering" || state.stage === "payment_attempted") {
    await recover(state);
    return;
  }

  await validateQuote(state);
  // Persist before the payment-aware fetch. A lost response must never cause a fresh authorization on rerun.
  state.stage = "payment_attempted";
  await saveState(state);

  // CDP_API_KEY_ID, CDP_API_KEY_SECRET, and CDP_WALLET_SECRET come from the environment.
  // Omitting `environment: "development"` selects mainnet.
  const payment = new CdpX402Client({
    spendControls: {
      maxAmountPerPayment: { atomic: 30_000n, asset: BASE_USDC },
      maxCumulativeSpend: { atomic: 30_000n, asset: BASE_USDC },
      maxCumulativeSpendWindow: "24h",
      allowedNetworks: [BASE],
    },
  });
  const { evmAddress } = await payment.getAddresses();
  console.log(`Paying exactly 0.03 USDC from ${evmAddress}`);

  const paidFetch = wrapFetchWithPayment(globalThis.fetch, payment);
  const response = await paidFetch(URL, { method: "POST", headers: headersFor(state), body: requestBody() });
  await finish(response, state);
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});
