<script module lang="ts">
	/**
	 * The real card form is a CROSS-ORIGIN iframe (web.squarecdn.com) that does
	 * NOT inherit the page font or CSS — parent stylesheets cannot reach it; only
	 * the SDK `style` option can. This mirrors the app's shadcn Input (see
	 * ui/input/input.svelte + the tokens in app.css) so the iframe reads as the
	 * same input as the surrounding form instead of Square's default Helvetica
	 * Neue 16px. Selectors follow Square's CardClassSelectors schema; only the
	 * properties listed here are supported (fontSize is capped at 16px, and there
	 * is no per-field `inputs()` API).
	 */
	type SquareCardClassSelectors = Record<string, Record<string, string>>;

	const cardStyle: SquareCardClassSelectors = {
		input: {
			fontSize: '14px', // md:text-sm (text-base md:text-sm — desktop size)
			fontFamily: 'Inter, ui-sans-serif, system-ui, sans-serif',
			fontWeight: '400',
			color: 'oklch(0.129 0.042 264.695)', // --foreground
			backgroundColor: 'oklch(1 0 0)' // --background
		},
		'input::placeholder': { color: 'oklch(0.554 0.046 257.417)' }, // --muted-foreground
		'input.is-focus': { color: 'oklch(0.129 0.042 264.695)' },
		'input.is-error': { color: 'oklch(0.64 0.21 25)' }, // --destructive
		'.input-container': {
			borderColor: 'oklch(0.929 0.013 255.508)', // --input (=== --border)
			borderRadius: '8px' // rounded-md = calc(0.625rem - 2px) = --radius-md
		},
		'.input-container.is-focus': { borderColor: 'oklch(0.704 0.04 256.788)' }, // --ring
		'.input-container.is-error': { borderColor: 'oklch(0.64 0.21 25)' },
		'.message-text': { color: 'oklch(0.554 0.046 257.417)' },
		'.message-text.is-error': { color: 'oklch(0.64 0.21 25)' },
		'.message-icon': { color: 'oklch(0.554 0.046 257.417)' },
		'.message-icon.is-error': { color: 'oklch(0.64 0.21 25)' }
	};
</script>

<script lang="ts">
	import { onMount, onDestroy } from 'svelte';
	import CardEntryUnavailable from './CardEntryUnavailable.svelte';
	import type MockCardForm from './MockCardForm.svelte';
	import { getSquarePayments, isSquareConfigured, isSquareMock } from '$lib/square/square';

	/**
	 * Billing contact passed to Square's tokenize() verificationDetails for
	 * Strong Customer Authentication (SCA). Only fields we already hold are
	 * included; omit the object entirely when nothing is available.
	 */
	export interface SquareVerificationContact {
		givenName?: string;
		familyName?: string;
		email?: string;
	}

	/** Result of a tokenize-with-verification call. */
	export interface TokenizeWithVerificationResult {
		nonce: string;
		verificationToken: string | null;
	}

	/** Square Web Payments `card.tokenize()` verification details shape. */
	interface SquareVerificationDetails {
		amount: string;
		billingContact?: SquareVerificationContact;
		intent: string;
		currencyCode: string;
		customerInitiated: boolean;
		sellerKeyedIn: boolean;
	}

	interface Props {
		/** Disable the form while a payment is processing. */
		disabled?: boolean;
		/** Called when the card form finishes (re)initializing; true = ready to tokenize. */
		onReady?: (ready: boolean) => void;
	}

	let { disabled = false, onReady = () => {} }: Props = $props();

	let containerEl = $state<HTMLDivElement | null>(null);
	let cardInstance: unknown | null = null;
	let mockForm = $state<MockCardForm | null>(null);
	let ready = $state(false);
	let initError = $state<string | null>(null);

	// Unique per instance so two mounted card forms never share an element id
	// (e.g. the deposit step + the pay-early modal on the same page). This app
	// is a pure SPA (no SSR/hydration), so a random id cannot mismatch.
	let uniqueId = $state(`square-card-${crypto.randomUUID()}`);

	async function init() {
		if (isSquareMock()) {
			// Local dev mock: no SDK load, no iframe. MockCardForm handles everything.
			return;
		}
		if (!isSquareConfigured()) {
			initError = 'not_configured';
			return;
		}
		try {
			const payments = (await getSquarePayments()) as {
				card: (options?: { style?: SquareCardClassSelectors }) => Promise<{
					attach: (selector: string) => Promise<void>;
					tokenize: (
						verificationDetails?: SquareVerificationDetails
					) => Promise<{
						status: string;
						token?: string;
						verificationResult?: { token?: string };
						errors?: Array<{ message?: string; code?: string }>;
					}>;
					destroy: () => void;
				}>;
			};
			// The iframe only honours styling passed via the SDK `style` option.
			// card.configure({ style: cardStyle }) could re-apply it later if we
			// ever need to restyle the already-attached form — no need to call it
			// at init.
			const card = await payments.card({ style: cardStyle });
			await card.attach(`#${uniqueId}`);
			cardInstance = card;
			ready = true;
			initError = null;
		} catch (err) {
			console.error('Square card form init failed:', err);
			initError = 'init_failed';
		}
	}

	onMount(() => {
		init();
	});

	onDestroy(() => {
		const card = cardInstance as { destroy?: () => void } | null;
		card?.destroy?.();
		cardInstance = null;
		ready = false;
	});

	$effect(() => {
		if (!isSquareMock()) {
			onReady(ready && !disabled);
		}
	});

	/** Tokenizes the entered card. Returns the cnon:xxx nonce, throws with a user-facing message. */
	export async function tokenize(): Promise<string> {
		if (isSquareMock()) {
			if (!mockForm) {
				throw new Error('Card form is not ready — please wait a moment and try again');
			}
			return mockForm.tokenize();
		}
		const card = cardInstance as {
			tokenize: (
				verificationDetails?: SquareVerificationDetails
			) => Promise<{
				status: string;
				token?: string;
				verificationResult?: { token?: string };
				errors?: Array<{ message?: string; code?: string }>;
			}>;
		} | null;
		if (!card) {
			throw new Error('Card form is not ready — please wait a moment and try again');
		}
		const result = await card.tokenize();
		if (result.status === 'OK' && result.token) {
			return result.token;
		}
		const detail =
			result.errors
				?.map((e) => e.message || e.code)
				.filter(Boolean)
				.join(', ') || 'Card details are incomplete';
		throw new Error(detail);
	}

	/**
	 * Tokenizes the entered card together with SCA verification details for a
	 * card-not-present charge. UK merchants must run Strong Customer
	 * Authentication for most online payments — without verificationDetails,
	 * Square rejects in-scope cards with CARD_DECLINED_VERIFICATION_REQUIRED.
	 *
	 * Returns BOTH the card nonce and the verification token, which the caller
	 * must send to the backend as `verification_token` alongside
	 * `new_card_token`/`card_token`.
	 *
	 * @param amount  The amount that WILL be charged, in pence (minor units).
	 *                Square requires this to match the eventual payment amount.
	 *                It is sent to Square as a MAJOR-units decimal string (e.g.
	 *                5000 pence → "50.00" for £50.00) per the W3C
	 *                valid-decimal-monetary-value standard — sending pence as an
	 *                integer string ("5000") would make Square's 3DS bind a
	 *                100×-too-large amount and fail SCA.
	 * @param contact Optional billing contact (name/email we already hold).
	 */
	export async function tokenizeWithVerification(
		amount: number,
		contact?: SquareVerificationContact
	): Promise<TokenizeWithVerificationResult> {
		if (isSquareMock()) {
			if (!mockForm) {
				throw new Error('Card form is not ready — please wait a moment and try again');
			}
			return mockForm.tokenizeWithVerification(amount, contact);
		}
		const card = cardInstance as {
			tokenize: (
				verificationDetails: SquareVerificationDetails
			) => Promise<{
				status: string;
				token?: string;
				verificationResult?: { token?: string };
				errors?: Array<{ message?: string; code?: string }>;
			}>;
		} | null;
		if (!card) {
			throw new Error('Card form is not ready — please wait a moment and try again');
		}
		const verificationDetails: SquareVerificationDetails = {
			// Square expects a MAJOR-units decimal string (W3C valid-decimal-
			// monetary-value), e.g. "50.00" for £50.00 — NOT the minor-unit
			// integer ("5000"), which would bind a 100×-too-large 3DS amount.
			amount: (amount / 100).toFixed(2),
			intent: 'CHARGE',
			currencyCode: 'GBP',
			customerInitiated: true,
			sellerKeyedIn: false
		};
		if (contact && (contact.givenName || contact.familyName || contact.email)) {
			verificationDetails.billingContact = contact;
		}
		const result = await card.tokenize(verificationDetails);
		if (result.status === 'OK' && result.token) {
			// In the current tokenize-with-verification flow the returned nonce
			// (result.token) is ALREADY the 3DS-verified token — Square binds the
			// SCA challenge to this exact amount, so charging it as
			// `new_card_token`/`card_token` is sufficient. `verificationResult`
			// only exists on the deprecated verifyBuyer() flow; we still read it
			// defensively since the backend accepts an explicit verification_token.
			return {
				nonce: result.token,
				verificationToken: result.verificationResult?.token ?? null
			};
		}
		const detail =
			result.errors
				?.map((e) => e.message || e.code)
				.filter(Boolean)
				.join(', ') || 'Card details are incomplete';
		throw new Error(detail);
	}
</script>

{#if isSquareMock()}
	{#await import('./MockCardForm.svelte')}
		<div class="flex h-9 items-center text-sm text-gray-400">Loading card form...</div>
	{:then { default: MockCardFormCtor }}
		<MockCardFormCtor bind:this={mockForm} {disabled} {onReady} />
	{/await}
{:else if initError === 'not_configured'}
	<CardEntryUnavailable />
{:else if initError === 'init_failed'}
	<CardEntryUnavailable
		message="The secure card form failed to load. Please try again or use a saved card."
	/>
{:else}
	<div id={uniqueId} bind:this={containerEl}></div>
{/if}
