<!--
  Confirmation modal — replaces window.confirm/prompt for destructive
  actions so the dashboard can render the full context (counts, lists,
  warnings) in its own style.

  Usage:
    {#if showing}
      <ConfirmModal
        title="Delete app"
        variant="danger"
        confirmLabel="Delete app"
        confirmPhrase={app.slug}
        onConfirm={() => doDelete()}
        onCancel={() => (showing = false)}
      >
        <p>Body content — counts, lists, warnings.</p>
      </ConfirmModal>
    {/if}

  When `confirmPhrase` is set the confirm button stays disabled until
  the user types the phrase exactly — same pattern GitHub uses for
  irreversible repo deletes. Omit it for low-stakes confirmations.
-->
<script lang="ts">
	import type { Snippet } from 'svelte';

	type Variant = 'danger' | 'neutral';

	interface Props {
		title: string;
		children: Snippet;
		onConfirm: () => void | Promise<void>;
		onCancel: () => void;
		variant?: Variant;
		confirmLabel?: string;
		cancelLabel?: string;
		/** When set, the confirm button is disabled until the user types
		 *  this string exactly (case-sensitive). */
		confirmPhrase?: string;
		/** Shown above the confirm input. Defaults to a sensible message
		 *  that mentions the phrase. */
		confirmPhrasePrompt?: string;
		/** While true the buttons are disabled and the confirm label is
		 *  replaced with a "busy" form (e.g. "Deleting…"). */
		busy?: boolean;
		busyLabel?: string;
	}

	let {
		title,
		children,
		onConfirm,
		onCancel,
		variant = 'neutral',
		confirmLabel = 'Confirm',
		cancelLabel = 'Cancel',
		confirmPhrase,
		confirmPhrasePrompt,
		busy = false,
		busyLabel
	}: Props = $props();

	let typed = $state('');
	let phraseMatches = $derived(
		confirmPhrase === undefined || typed === confirmPhrase
	);

	function handleKeydown(event: KeyboardEvent) {
		if (event.key === 'Escape' && !busy) {
			event.preventDefault();
			onCancel();
		}
	}

	function handleBackdrop(event: MouseEvent) {
		// Only cancel when clicking the backdrop itself, not bubbled
		// clicks from the dialog content.
		if (event.target === event.currentTarget && !busy) {
			onCancel();
		}
	}

	async function handleConfirm() {
		if (!phraseMatches || busy) return;
		await onConfirm();
	}

	// Focus management: when the modal mounts, focus the slug-retype
	// input (if present) or the cancel button (so an accidental Enter
	// doesn't auto-confirm a destructive action).
	let inputRef = $state<HTMLInputElement | null>(null);
	let cancelRef = $state<HTMLButtonElement | null>(null);
	let dialogRef = $state<HTMLDivElement | null>(null);

	$effect(() => {
		if (inputRef) inputRef.focus();
		else if (cancelRef) cancelRef.focus();
	});

	// F-U-016: focus trap. The browser will otherwise Tab into the
	// underlying page; for a modal that's a strong accessibility and
	// keyboard-UX violation. Cycle focus within the dialog and reverse
	// on Shift+Tab.
	function handleTrapTab(event: KeyboardEvent) {
		if (event.key !== 'Tab' || !dialogRef) return;
		const focusables = dialogRef.querySelectorAll<HTMLElement>(
			'button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), a[href], [tabindex]:not([tabindex="-1"])'
		);
		if (focusables.length === 0) return;
		const first = focusables[0]!;
		const last = focusables[focusables.length - 1]!;
		const active = document.activeElement as HTMLElement | null;
		if (event.shiftKey && active === first) {
			event.preventDefault();
			last.focus();
		} else if (!event.shiftKey && active === last) {
			event.preventDefault();
			first.focus();
		}
	}
</script>

<svelte:window onkeydown={handleKeydown} />

<div
	class="backdrop"
	role="presentation"
	onclick={handleBackdrop}
>
	<div
		bind:this={dialogRef}
		class="dialog"
		class:danger={variant === 'danger'}
		role="dialog"
		aria-modal="true"
		aria-labelledby="confirm-title"
		onkeydown={handleTrapTab}
	>
		<h2 id="confirm-title">{title}</h2>
		<div class="body">
			{@render children()}
		</div>

		{#if confirmPhrase}
			<label class="phrase">
				<span>
					{confirmPhrasePrompt ?? `Type the slug to confirm:`}
					<code>{confirmPhrase}</code>
				</span>
				<input
					bind:this={inputRef}
					bind:value={typed}
					autocomplete="off"
					autocapitalize="off"
					autocorrect="off"
					spellcheck="false"
					disabled={busy}
				/>
			</label>
		{/if}

		<div class="actions">
			<button
				type="button"
				class="secondary"
				bind:this={cancelRef}
				onclick={onCancel}
				disabled={busy}
			>
				{cancelLabel}
			</button>
			<button
				type="button"
				class={variant === 'danger' ? 'danger' : ''}
				onclick={handleConfirm}
				disabled={!phraseMatches || busy}
			>
				{busy ? (busyLabel ?? `${confirmLabel}…`) : confirmLabel}
			</button>
		</div>
	</div>
</div>

<style>
	.backdrop {
		position: fixed;
		inset: 0;
		background: rgba(2, 6, 23, 0.7);
		display: flex;
		align-items: center;
		justify-content: center;
		padding: 1rem;
		z-index: 100;
	}

	.dialog {
		background: #0f172a;
		color: #e2e8f0;
		border: 1px solid #334155;
		border-radius: 0.5rem;
		padding: 1.5rem;
		width: 100%;
		max-width: 32rem;
		max-height: calc(100vh - 2rem);
		overflow-y: auto;
		box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.7);
	}

	.dialog.danger {
		border-color: #7f1d1d;
	}

	h2 {
		margin: 0 0 1rem;
		font-size: 1.125rem;
	}

	.dialog.danger h2 {
		color: #fca5a5;
	}

	.body {
		font-size: 0.9rem;
		line-height: 1.5;
		color: #cbd5e1;
	}

	.body :global(p) {
		margin: 0 0 0.75rem;
	}

	.body :global(p:last-child) {
		margin-bottom: 0;
	}

	.body :global(code) {
		background: #1e293b;
		padding: 0.1rem 0.3rem;
		border-radius: 0.25rem;
		font-size: 0.85em;
	}

	.body :global(strong) {
		color: #e2e8f0;
	}

	.body :global(ul) {
		margin: 0.5rem 0 0.75rem;
		padding-left: 1.25rem;
	}

	.body :global(.impact-list) {
		list-style: none;
		padding: 0;
		margin: 0.75rem 0;
		background: #1e293b;
		border-radius: 0.375rem;
		padding: 0.5rem 0.75rem;
		max-height: 12rem;
		overflow-y: auto;
	}

	.body :global(.impact-list li) {
		padding: 0.25rem 0;
		display: flex;
		justify-content: space-between;
		gap: 0.5rem;
	}

	.body :global(.impact-list li + li) {
		border-top: 1px solid #334155;
	}

	.body :global(.modal-error) {
		margin-top: 0.75rem;
		padding: 0.6rem 0.75rem;
		border: 1px solid #b91c1c;
		background: #450a0a;
		color: #fecaca;
		border-radius: 0.375rem;
		font-size: 0.85rem;
	}

	.body :global(.muted) {
		color: #64748b;
	}

	.phrase {
		display: flex;
		flex-direction: column;
		gap: 0.4rem;
		margin: 1rem 0 0;
		font-size: 0.85rem;
		color: #cbd5e1;
	}

	.phrase code {
		background: #1e293b;
		padding: 0.1rem 0.3rem;
		border-radius: 0.25rem;
		color: #fca5a5;
	}

	.phrase input {
		background: #0b1220;
		color: #e2e8f0;
		border: 1px solid #334155;
		border-radius: 0.375rem;
		padding: 0.5rem 0.75rem;
		font: inherit;
		font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
	}

	.phrase input:focus {
		outline: none;
		border-color: #38bdf8;
	}

	.actions {
		display: flex;
		justify-content: flex-end;
		gap: 0.5rem;
		margin-top: 1.5rem;
	}

	button {
		background: #38bdf8;
		color: #0b1220;
		border: none;
		padding: 0.5rem 1rem;
		border-radius: 0.375rem;
		font-weight: 600;
		font: inherit;
		font-weight: 600;
		cursor: pointer;
	}

	button.secondary {
		background: transparent;
		color: #94a3b8;
		border: 1px solid #334155;
	}

	button.danger {
		background: #7f1d1d;
		color: #fecaca;
	}

	button:disabled {
		opacity: 0.5;
		cursor: not-allowed;
	}
</style>
