"use client";

import * as React from "react";
import { CheckIcon, CopyIcon, CreditCardIcon, LockIcon, SearchIcon, SendIcon, SmartphoneIcon } from "lucide-react";

import { Button } from "@/components/ui/button";
import { Field, FieldDescription, FieldError, FieldGroup, FieldLabel } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { InputGroup, InputGroupAddon, InputGroupInput } from "@/components/ui/input-group";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Spinner } from "@/components/ui/spinner";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { cn } from "@/lib/utils";

import { Money, PhoneInput } from "./money";
import { MpesaStatus, type MpesaState } from "./sale";
import { TONE_BOX } from "./tones";

/** What a confirmed M-Pesa or card payment hands back to the tender panel. */
export type PaymentConfirmation =
  | { method: "M-Pesa"; channel: "STK push" | "Paybill" | "Till"; reference: string; phone?: string }
  | { method: "Card"; channel: "Terminal" | "Card not present"; brand: string; last4: string; reference: string };

const wait = (ms: number) => new Promise((r) => setTimeout(r, ms));
const fakeReceipt = () => "S" + Array.from({ length: 9 }, () => "ABCDEFGHJKLMNPQRSTUVWXYZ0123456789"[Math.floor(Math.random() * 34)]).join("");

/* ====================================== M-Pesa ======================================== */

/** M-Pesa transaction codes are 10 letters and digits, e.g. SJK4L8P2QX. */
const MPESA_CODE = /^[A-Z0-9]{10}$/;

function CopyValue({ label, value, mono = true }: { label: string; value: string; mono?: boolean }) {
  const [copied, setCopied] = React.useState(false);
  const copy = async () => {
    try {
      await navigator.clipboard.writeText(value);
      setCopied(true);
      setTimeout(() => setCopied(false), 1500);
    } catch {
      // Clipboard blocked: the value is on screen to read out.
    }
  };
  return (
    <div className="flex items-center justify-between gap-2 py-1.5">
      <span className="text-xs text-muted-foreground">{label}</span>
      <span className="flex items-center gap-1">
        <span className={cn("text-base font-semibold text-foreground", mono && "tabular-nums tracking-wide")}>{value}</span>
        <Button variant="ghost" size="icon-xs" aria-label={`Copy ${label.toLowerCase()}`} onClick={copy}>
          {copied ? <CheckIcon className="text-status-success-text" /> : <CopyIcon />}
        </Button>
      </span>
    </div>
  );
}

/**
 * Paybill / Till instructions and confirmation. The customer pays from their own phone; the cashier then
 * confirms with the M-Pesa code from the customer's SMS, or looks the payment up by account number.
 */
function ManualMpesa({
  kind,
  amount,
  number,
  accountRef,
  disabled,
  onVerify,
  onConfirmed,
}: {
  kind: "Paybill" | "Till";
  amount: number;
  number: string;
  accountRef: string;
  disabled?: boolean;
  onVerify: (q: { code?: string; accountRef: string; amount: number }) => Promise<string | null>;
  onConfirmed: (c: PaymentConfirmation) => void;
}) {
  const [code, setCode] = React.useState("");
  const [tried, setTried] = React.useState(false);
  const [busy, setBusy] = React.useState<"code" | "lookup" | null>(null);
  const [notFound, setNotFound] = React.useState(false);
  const [receipt, setReceipt] = React.useState<string | null>(null);
  const codeError = MPESA_CODE.test(code) ? "" : "Enter the 10-character code from the customer's SMS, e.g. SJK4L8P2QX";

  const run = async (how: "code" | "lookup") => {
    if (how === "code") {
      setTried(true);
      if (codeError) return;
    }
    setBusy(how);
    setNotFound(false);
    const found = await onVerify({ code: how === "code" ? code : undefined, accountRef, amount });
    setBusy(null);
    if (!found) return setNotFound(true);
    setReceipt(found);
    onConfirmed({ method: "M-Pesa", channel: kind, reference: found });
  };

  if (receipt) return <MpesaStatus state="confirmed" phone="" amount={amount} receiptNo={receipt} />;

  const steps =
    kind === "Paybill"
      ? ["M-Pesa → Lipa na M-Pesa → Pay Bill", `Business number ${number}`, `Account number ${accountRef}`, `Amount ${amount.toFixed(2)}, then PIN`]
      : ["M-Pesa → Lipa na M-Pesa → Buy Goods and Services", `Till number ${number}`, `Amount ${amount.toFixed(2)}, then PIN`];

  return (
    <div className="space-y-3">
      <div className="rounded-lg border border-border bg-muted/40 px-3 py-1.5">
        <CopyValue label={kind === "Paybill" ? "Business number" : "Till number"} value={number} />
        {kind === "Paybill" ? <CopyValue label="Account number" value={accountRef} /> : null}
        <div className="flex items-center justify-between gap-2 border-t border-border py-2">
          <span className="text-xs text-muted-foreground">Amount</span>
          <Money amount={amount} className="text-base font-semibold" />
        </div>
      </div>
      <ol className="list-decimal space-y-0.5 pl-5 text-xs text-muted-foreground marker:tabular-nums">
        {steps.map((s) => (
          <li key={s}>{s}</li>
        ))}
      </ol>
      <Field data-invalid={tried && !!codeError}>
        <FieldLabel htmlFor={`mpesa-code-${kind}`}>M-Pesa code</FieldLabel>
        <div className="flex gap-2">
          <Input
            id={`mpesa-code-${kind}`}
            value={code}
            disabled={disabled || !!busy}
            maxLength={10}
            autoComplete="off"
            spellCheck={false}
            placeholder="SJK4L8P2QX"
            className="font-mono tracking-widest uppercase"
            aria-invalid={tried && !!codeError}
            onChange={(e) => setCode(e.target.value.toUpperCase().replace(/[^A-Z0-9]/g, ""))}
          />
          <Button variant="outline" disabled={disabled || !!busy} onClick={() => run("code")}>
            {busy === "code" ? <Spinner /> : null}
            Verify
          </Button>
        </div>
        {tried && codeError ? <FieldError>{codeError}</FieldError> : null}
      </Field>
      <Button variant="ghost" size="sm" className="-ml-2" disabled={disabled || !!busy} onClick={() => run("lookup")}>
        {busy === "lookup" ? <Spinner /> : <SearchIcon data-icon="inline-start" />}
        {kind === "Paybill" ? `Check for a payment to account ${accountRef}` : "Check the till for this amount"}
      </Button>
      {notFound ? (
        <p role="alert" className={cn("rounded-lg border px-3 py-2 text-xs", TONE_BOX.warning)}>
          No matching payment yet. Ask the customer to show the confirmation SMS, or check again in a moment.
        </p>
      ) : null}
    </div>
  );
}

type SendResult = "confirmed" | "failed" | "timeout";
type CheckResult = "confirmed" | "unpaid";

/** STK push: send a payment prompt to the customer's phone, then follow it through MpesaStatus. */
function StkPush({
  amount,
  defaultPhone,
  disabled,
  onSendPrompt,
  onCheckStatus,
  onConfirmed,
}: {
  amount: number;
  defaultPhone?: string;
  disabled?: boolean;
  /** Card 012: `amount` is what this push asks for (the part being paid by M-Pesa in a split). */
  onSendPrompt: (phone: string, amount: number) => Promise<{ state: SendResult; receiptNo?: string }>;
  onCheckStatus: (phone: string, amount: number) => Promise<{ state: CheckResult; receiptNo?: string }>;
  onConfirmed: (c: PaymentConfirmation) => void;
}) {
  const [phone, setPhone] = React.useState((defaultPhone ?? "").replace(/\D/g, "").slice(-9));
  const [state, setState] = React.useState<MpesaState | null>(null);
  const [receipt, setReceipt] = React.useState<string>();
  const run = React.useRef(0);
  const display = `+254 ${phone.slice(0, 3)} ${phone.slice(3, 6)} ${phone.slice(6)}`;
  const valid = /^[17]\d{8}$/.test(phone);

  const send = async () => {
    const id = ++run.current;
    setState("waiting");
    const r = await onSendPrompt(`254${phone}`, amount);
    if (id !== run.current) return; // cancelled or resent meanwhile
    setState(r.state);
    if (r.state === "confirmed") {
      setReceipt(r.receiptNo);
      onConfirmed({ method: "M-Pesa", channel: "STK push", reference: r.receiptNo ?? "", phone: `+254${phone}` });
    }
  };

  // After a timeout, ask M-Pesa before anything else: the customer may have paid late (IG-05).
  const check = async () => {
    const id = ++run.current;
    setState("checking");
    const r = await onCheckStatus(`254${phone}`, amount);
    if (id !== run.current) return;
    setState(r.state);
    if (r.state === "confirmed") {
      setReceipt(r.receiptNo);
      onConfirmed({ method: "M-Pesa", channel: "STK push", reference: r.receiptNo ?? "", phone: `+254${phone}` });
    }
  };

  if (state)
    return (
      <MpesaStatus
        state={state}
        phone={display}
        amount={amount}
        receiptNo={receipt}
        onCancel={() => {
          run.current++;
          setState(null);
        }}
        onResend={send}
        onCheck={check}
        onOtherMethod={() => setState(null)}
      />
    );

  return (
    <div className="space-y-3">
      <Field>
        <FieldLabel htmlFor="stk-phone">Customer phone</FieldLabel>
        <div onChange={(e) => setPhone((e.target as HTMLInputElement).value.replace(/\D/g, "").slice(-9))}>
          <PhoneInput id="stk-phone" defaultValue={phone} disabled={disabled} />
        </div>
        <FieldDescription>The customer gets a prompt for KES {amount.toFixed(2)} and enters their M-Pesa PIN.</FieldDescription>
      </Field>
      <Button className="w-full" disabled={disabled || !valid} onClick={send}>
        <SendIcon data-icon="inline-start" />
        Send payment prompt
      </Button>
    </div>
  );
}

/**
 * M-Pesa tender: STK push (prompt on the customer's phone), Paybill (business number + account number) or
 * Till (Buy Goods). Paybill and Till confirm with the customer's M-Pesa code or a lookup.
 * `onSendPrompt` and `onVerify` are the API calls; the defaults simulate them for previews.
 */
export function MpesaPayment({
  amount,
  accountRef,
  paybill = "174379",
  till = "5123456",
  defaultPhone = "712345678",
  disabled,
  // Previews: a number ending 0000 times out and turns out paid; ending 1111 times out unpaid.
  onSendPrompt = async (phone) => {
    await wait(2500);
    return /(0000|1111)$/.test(phone) ? { state: "timeout" } : { state: "confirmed", receiptNo: fakeReceipt() };
  },
  onCheckStatus = async (phone) => {
    await wait(1500);
    return phone.endsWith("0000") ? { state: "confirmed", receiptNo: fakeReceipt() } : { state: "unpaid" };
  },
  onVerify = async ({ code }) => {
    await wait(1200);
    return code ?? fakeReceipt();
  },
  onConfirmed = () => {},
  stkOnly = false,
}: {
  amount: number;
  /** Card 012: only the STK push tab, for a live till until Paybill and Till payments can be verified (M-Pesa C2B, Phase 2). */
  stkOnly?: boolean;
  /** Paybill account number: the sale's receipt number, so the payment matches the sale. */
  accountRef: string;
  paybill?: string;
  till?: string;
  defaultPhone?: string;
  disabled?: boolean;
  onSendPrompt?: (phone: string, amount: number) => Promise<{ state: SendResult; receiptNo?: string }>;
  /** Queries the STK request status; called after a timeout, before any resend. */
  onCheckStatus?: (phone: string, amount: number) => Promise<{ state: CheckResult; receiptNo?: string }>;
  /** Plan F: `amount` is what this tab is collecting, so a payment can be checked against it (C2B). */
  onVerify?: (q: { code?: string; accountRef: string; amount: number }) => Promise<string | null>;
  onConfirmed?: (c: PaymentConfirmation) => void;
}) {
  return (
    <Tabs defaultValue="stk" className="gap-3">
      {stkOnly ? null : (
        <TabsList className="grid w-full grid-cols-3">
          <TabsTrigger value="stk">STK push</TabsTrigger>
          <TabsTrigger value="paybill">Paybill</TabsTrigger>
          <TabsTrigger value="till">Till</TabsTrigger>
        </TabsList>
      )}
      <TabsContent value="stk">
        <StkPush amount={amount} defaultPhone={defaultPhone} disabled={disabled} onSendPrompt={onSendPrompt} onCheckStatus={onCheckStatus} onConfirmed={onConfirmed} />
      </TabsContent>
      <TabsContent value="paybill">
        <ManualMpesa kind="Paybill" amount={amount} number={paybill} accountRef={accountRef} disabled={disabled} onVerify={onVerify} onConfirmed={onConfirmed} />
      </TabsContent>
      <TabsContent value="till">
        <ManualMpesa kind="Till" amount={amount} number={till} accountRef={accountRef} disabled={disabled} onVerify={onVerify} onConfirmed={onConfirmed} />
      </TabsContent>
    </Tabs>
  );
}

/* ======================================= Card ========================================= */

type Brand = "Visa" | "Mastercard" | "Amex" | "Card";

export function cardBrand(digits: string): Brand {
  if (/^4/.test(digits)) return "Visa";
  if (/^(5[1-5]|2(2[2-9]|[3-6]\d|7[01]|720))/.test(digits)) return "Mastercard";
  if (/^3[47]/.test(digits)) return "Amex";
  return "Card";
}

/** Luhn checksum: catches mistyped card numbers before they reach the gateway. */
export function luhn(digits: string) {
  let sum = 0;
  for (let i = 0; i < digits.length; i++) {
    let d = Number(digits[digits.length - 1 - i]);
    if (i % 2 === 1) {
      d *= 2;
      if (d > 9) d -= 9;
    }
    sum += d;
  }
  return digits.length > 0 && sum % 10 === 0;
}

const groupCard = (d: string, brand: Brand) =>
  brand === "Amex" ? [d.slice(0, 4), d.slice(4, 10), d.slice(10, 15)].filter(Boolean).join(" ") : (d.match(/.{1,4}/g) ?? []).join(" ");

function expiryError(v: string) {
  const m = /^(\d{2})\/(\d{2})$/.exec(v);
  if (!m) return "Enter the expiry as MM/YY";
  const month = Number(m[1]);
  if (month < 1 || month > 12) return "Month must be 01 to 12";
  const now = new Date();
  const end = new Date(2000 + Number(m[2]), month, 1); // first day after the expiry month
  return end <= now ? "This card has expired" : "";
}

/** Card terminal: the terminal takes the card; the cashier records what it printed. No card number in the app. */
function TerminalCard({ amount, disabled, onConfirmed }: { amount: number; disabled?: boolean; onConfirmed: (c: PaymentConfirmation) => void }) {
  const [brand, setBrand] = React.useState<string>("");
  const [last4, setLast4] = React.useState("");
  const [approval, setApproval] = React.useState("");
  const [tried, setTried] = React.useState(false);
  const [done, setDone] = React.useState(false);
  const errs = {
    brand: brand ? "" : "Choose the card type",
    last4: /^\d{4}$/.test(last4) ? "" : "Enter the last 4 digits",
    approval: /^[A-Z0-9]{6}$/.test(approval) ? "" : "Enter the 6-character approval code",
  };
  const ok = !errs.brand && !errs.last4 && !errs.approval;

  if (done)
    return (
      <div role="status" className={cn("space-y-1 rounded-lg border p-3 text-sm", TONE_BOX.success)}>
        <p className="flex items-center gap-2 font-semibold">
          <CheckIcon className="size-4" /> Card payment recorded
        </p>
        <p className="text-xs">
          {brand} ending {last4}, approval {approval}.
        </p>
      </div>
    );

  return (
    <div className="space-y-3">
      <p className="flex gap-2 rounded-lg border border-border bg-muted/40 p-3 text-xs text-muted-foreground">
        <CreditCardIcon className="mt-0.5 size-4 shrink-0" aria-hidden />
        <span>
          Enter <span className="font-semibold text-foreground tabular-nums">KES {amount.toFixed(2)}</span> on the terminal and ask the customer to tap, insert or swipe. When it approves, record the
          slip details below.
        </span>
      </p>
      <FieldGroup className="gap-3">
        <div className="grid grid-cols-2 gap-3">
          <Field data-invalid={tried && !!errs.brand}>
            <FieldLabel>Card type</FieldLabel>
            <Select value={brand || null} onValueChange={(v) => setBrand(String(v ?? ""))} disabled={disabled}>
              <SelectTrigger className="w-full" aria-invalid={tried && !!errs.brand}>
                <SelectValue placeholder="Choose" />
              </SelectTrigger>
              <SelectContent>
                {["Visa", "Mastercard", "Amex", "Other"].map((b) => (
                  <SelectItem key={b} value={b}>
                    {b}
                  </SelectItem>
                ))}
              </SelectContent>
            </Select>
          </Field>
          <Field data-invalid={tried && !!errs.last4}>
            <FieldLabel htmlFor="term-last4">Last 4 digits</FieldLabel>
            <Input
              id="term-last4"
              inputMode="numeric"
              maxLength={4}
              placeholder="4242"
              className="tabular-nums tracking-widest"
              value={last4}
              disabled={disabled}
              aria-invalid={tried && !!errs.last4}
              onChange={(e) => setLast4(e.target.value.replace(/\D/g, "").slice(0, 4))}
            />
          </Field>
        </div>
        <Field data-invalid={tried && !!errs.approval}>
          <FieldLabel htmlFor="term-approval">Approval code</FieldLabel>
          <Input
            id="term-approval"
            maxLength={6}
            placeholder="A1B2C3"
            autoComplete="off"
            className="font-mono tracking-widest uppercase"
            value={approval}
            disabled={disabled}
            aria-invalid={tried && !!errs.approval}
            onChange={(e) => setApproval(e.target.value.toUpperCase().replace(/[^A-Z0-9]/g, "").slice(0, 6))}
          />
          <FieldDescription>Printed on the terminal slip as AUTH or APPR CODE.</FieldDescription>
        </Field>
        {tried && !ok ? <FieldError>{errs.brand || errs.last4 || errs.approval}</FieldError> : null}
      </FieldGroup>
      <Button
        variant="secondary"
        className="w-full"
        disabled={disabled}
        onClick={() => {
          setTried(true);
          if (!ok) return;
          setDone(true);
          onConfirmed({ method: "Card", channel: "Terminal", brand, last4, reference: approval });
        }}
      >
        Record card payment
      </Button>
    </div>
  );
}

/**
 * Card not present (phone or online orders). Laid out like a payment gateway's hosted fields: in production
 * the number, expiry and CVV inputs are the gateway's iframes, so the digits go straight to the gateway and
 * never reach the Medora API. Here they are local inputs; the CVV is never stored, only the last 4 survive.
 */
function CardNotPresent({
  amount,
  disabled,
  onCharge,
  onConfirmed,
}: {
  amount: number;
  disabled?: boolean;
  onCharge: (card: { brand: Brand; last4: string }) => Promise<{ approved: boolean; reference?: string; reason?: string }>;
  onConfirmed: (c: PaymentConfirmation) => void;
}) {
  const [number, setNumber] = React.useState("");
  const [expiry, setExpiry] = React.useState("");
  const [cvv, setCvv] = React.useState("");
  const [name, setName] = React.useState("");
  const [tried, setTried] = React.useState(false);
  const [busy, setBusy] = React.useState(false);
  const [result, setResult] = React.useState<{ approved: boolean; reference?: string; reason?: string; last4: string; brand: Brand } | null>(null);

  const brand = cardBrand(number);
  const cvvLen = brand === "Amex" ? 4 : 3;
  const errs = {
    number: number.length < (brand === "Amex" ? 15 : 13) ? "Enter the full card number" : !luhn(number) ? "This card number is not valid; check the digits" : "",
    expiry: expiryError(expiry),
    cvv: cvv.length === cvvLen ? "" : `Enter the ${cvvLen}-digit security code`,
    name: name.trim().length >= 2 ? "" : "Enter the name on the card",
  };
  const ok = !errs.number && !errs.expiry && !errs.cvv && !errs.name;

  const charge = async () => {
    setTried(true);
    if (!ok) return;
    setBusy(true);
    const last4 = number.slice(-4);
    const r = await onCharge({ brand, last4 });
    setBusy(false);
    // Card data is dropped as soon as the charge returns, whatever the outcome.
    setNumber("");
    setCvv("");
    setExpiry("");
    setTried(false);
    setResult({ ...r, last4, brand });
    if (r.approved) onConfirmed({ method: "Card", channel: "Card not present", brand, last4, reference: r.reference ?? "" });
  };

  if (result?.approved)
    return (
      <div role="status" className={cn("space-y-1 rounded-lg border p-3 text-sm", TONE_BOX.success)}>
        <p className="flex items-center gap-2 font-semibold">
          <CheckIcon className="size-4" /> Payment approved
        </p>
        <p className="text-xs">
          {result.brand} ending {result.last4}, reference {result.reference}.
        </p>
      </div>
    );

  return (
    <div className="space-y-3">
      {result && !result.approved ? (
        <p role="alert" className={cn("rounded-lg border px-3 py-2 text-xs", TONE_BOX.error)}>
          Declined: {result.reason ?? "the bank refused the payment"}. Enter the card again or use another method.
        </p>
      ) : null}
      <FieldGroup className="gap-3">
        <Field data-invalid={tried && !!errs.number}>
          <FieldLabel htmlFor="cnp-number">Card number</FieldLabel>
          <InputGroup>
            <InputGroupInput
              id="cnp-number"
              inputMode="numeric"
              autoComplete="cc-number"
              placeholder="1234 5678 9012 3456"
              className="tabular-nums tracking-wide"
              value={groupCard(number, brand)}
              disabled={disabled || busy}
              aria-invalid={tried && !!errs.number}
              onChange={(e) => {
                // Limit by the brand of the new digits, not the previous ones (Amex is 15, others 16).
                const d = e.target.value.replace(/\D/g, "");
                setNumber(d.slice(0, cardBrand(d) === "Amex" ? 15 : 16));
              }}
            />
            <InputGroupAddon align="inline-end">
              <span className={cn("rounded px-1.5 py-0.5 text-xs font-semibold", brand === "Card" ? "text-muted-foreground" : "bg-muted text-foreground")}>{brand === "Card" ? <CreditCardIcon className="size-4" /> : brand}</span>
            </InputGroupAddon>
          </InputGroup>
          {tried && errs.number ? <FieldError>{errs.number}</FieldError> : null}
        </Field>
        <div className="grid grid-cols-2 gap-3">
          <Field data-invalid={tried && !!errs.expiry}>
            <FieldLabel htmlFor="cnp-exp">Expiry</FieldLabel>
            <Input
              id="cnp-exp"
              inputMode="numeric"
              autoComplete="cc-exp"
              placeholder="MM/YY"
              className="tabular-nums"
              value={expiry}
              disabled={disabled || busy}
              aria-invalid={tried && !!errs.expiry}
              onChange={(e) => {
                const d = e.target.value.replace(/\D/g, "").slice(0, 4);
                setExpiry(d.length > 2 ? `${d.slice(0, 2)}/${d.slice(2)}` : d);
              }}
            />
            {tried && errs.expiry ? <FieldError>{errs.expiry}</FieldError> : null}
          </Field>
          <Field data-invalid={tried && !!errs.cvv}>
            <FieldLabel htmlFor="cnp-cvv">CVV</FieldLabel>
            <Input
              id="cnp-cvv"
              type="password"
              inputMode="numeric"
              autoComplete="cc-csc"
              placeholder={brand === "Amex" ? "4 digits" : "3 digits"}
              className="tabular-nums tracking-widest"
              value={cvv}
              disabled={disabled || busy}
              aria-invalid={tried && !!errs.cvv}
              onChange={(e) => setCvv(e.target.value.replace(/\D/g, "").slice(0, cvvLen))}
            />
            {tried && errs.cvv ? <FieldError>{errs.cvv}</FieldError> : null}
          </Field>
        </div>
        <Field data-invalid={tried && !!errs.name}>
          <FieldLabel htmlFor="cnp-name">Name on card</FieldLabel>
          <Input id="cnp-name" autoComplete="cc-name" value={name} disabled={disabled || busy} aria-invalid={tried && !!errs.name} onChange={(e) => setName(e.target.value)} />
          {tried && errs.name ? <FieldError>{errs.name}</FieldError> : null}
        </Field>
      </FieldGroup>
      <p className="flex items-start gap-1.5 text-xs text-muted-foreground">
        <LockIcon className="mt-0.5 size-3.5 shrink-0" aria-hidden />
        Card details go straight to the payment gateway. Medora keeps only the card type and last 4 digits; the CVV is never stored.
      </p>
      <Button variant="secondary" className="w-full" disabled={disabled || busy} onClick={charge}>
        {busy ? <Spinner /> : <LockIcon data-icon="inline-start" />}
        {busy ? "Charging" : `Charge KES ${amount.toFixed(2)}`}
      </Button>
    </div>
  );
}

/**
 * Card tender: the card terminal by default (the app records type, last 4 and approval code), or
 * card-not-present entry laid out as gateway hosted fields. `onCharge` is the gateway call; the default
 * simulates it (cards ending 0002 are declined, for previews).
 */
export function CardPayment({
  amount,
  disabled,
  onCharge = async ({ last4 }) => {
    await wait(1500);
    return last4 === "0002" ? { approved: false, reason: "insufficient funds" } : { approved: true, reference: `GW${Date.now().toString().slice(-8)}` };
  },
  onConfirmed = () => {},
}: {
  amount: number;
  disabled?: boolean;
  onCharge?: (card: { brand: Brand; last4: string }) => Promise<{ approved: boolean; reference?: string; reason?: string }>;
  onConfirmed?: (c: PaymentConfirmation) => void;
}) {
  return (
    <Tabs defaultValue="terminal" className="gap-3">
      <TabsList className="grid w-full grid-cols-2">
        <TabsTrigger value="terminal">
          <CreditCardIcon /> Card terminal
        </TabsTrigger>
        <TabsTrigger value="cnp">
          <SmartphoneIcon /> Card not present
        </TabsTrigger>
      </TabsList>
      <TabsContent value="terminal">
        <TerminalCard amount={amount} disabled={disabled} onConfirmed={onConfirmed} />
      </TabsContent>
      <TabsContent value="cnp">
        <CardNotPresent amount={amount} disabled={disabled} onCharge={onCharge} onConfirmed={onConfirmed} />
      </TabsContent>
    </Tabs>
  );
}
