// rh777 paywall for merchants, in one file. Hono middleware that speaks only HTTP to the facilitator:
// answer 402 with a price, verify the signed payment, run your handler, settle, return the receipt.
//
//   import { paywall } from './paywall.ts'
//   app.get('/quote', paywall({ price: '0.01', payTo: '0xYourAddress', key: process.env.RH777_KEY! }), (c) => c.json(quote()))
//
// The key comes from https://rh777.metamuse.lol/merchant/ and only settles payments addressed to payTo.
import type { Context, Next } from 'hono';

const FACILITATOR = 'https://api.metamuse.lol/rh777';
const USDG = '0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168';
const SPENDER = '0x402085c248EeA27D92E8b30b2C58ed07f9E20001';
const PERMIT2 = '0x000000000022D473030F116dDEE9F6B43aC78BA3';
export const HEADERS = { challenge: 'PAYMENT-REQUIRED', payment: 'PAYMENT-SIGNATURE', receipt: 'PAYMENT-RESPONSE' } as const;
const b64 = (o: unknown) => btoa(unescape(encodeURIComponent(JSON.stringify(o))));
const unb64 = <T,>(s: string): T => JSON.parse(decodeURIComponent(escape(atob(s))));

export type PaywallOptions = { price: string; payTo: `0x${string}`; key: string; description?: string; maxTimeoutSeconds?: number; facilitator?: string };

function toUnits(price: string): string { // USDG has six decimals
  const [w, f = ''] = price.split('.'); return (BigInt(w || '0') * 1_000_000n + BigInt((f + '000000').slice(0, 6))).toString();
}

export function paywall(o: PaywallOptions) {
  const base = o.facilitator || FACILITATOR;
  const amount = toUnits(o.price);
  return async (c: Context, next: Next) => {
    const requirement = { method: 'usdg-permit2', scheme: 'exact', network: 'eip155:4663', asset: USDG, amount, payTo: o.payTo, maxTimeoutSeconds: o.maxTimeoutSeconds ?? 300, spender: SPENDER, permit2: PERMIT2, resource: c.req.url, description: o.description, extra: { assetTransferMethod: 'permit2', name: 'Global Dollar', version: '1' } };
    const fail = (error: string, status: 402 | 412 = 402) => { c.header(HEADERS.challenge, b64({ rh777: 1, error, accepts: [requirement] })); return c.json({ rh777: 1, error, accepts: [requirement] }, status); };
    const header = c.req.header(HEADERS.payment);
    if (!header) return fail('payment required');
    let payment: unknown;
    try { payment = unb64(header); } catch { return fail('malformed payment header'); }
    const v = await fetch(base + '/verify', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ payment, requirement }) }).then((r) => r.json()).catch(() => ({ isValid: false, invalidReason: 'facilitator unreachable' }));
    if (!v.isValid) return fail(v.invalidReason || v.error || 'invalid payment', v.invalidReason === 'PERMIT2_ALLOWANCE_REQUIRED' ? 412 : 402);
    await next();
    const receipt = await fetch(base + '/settle', { method: 'POST', headers: { 'content-type': 'application/json', 'x-rh777-key': o.key }, body: JSON.stringify({ payment, requirement }) }).then((r) => r.json()).catch(() => ({ success: false, errorReason: 'facilitator unreachable' }));
    if (!receipt.success) { c.res = c.json({ rh777: 1, error: receipt.errorReason, accepts: [requirement] }, 402); return; }
    c.res.headers.set(HEADERS.receipt, b64(receipt));
    c.res.headers.set('access-control-expose-headers', `${HEADERS.receipt}, ${HEADERS.challenge}`);
  };
}
