<script lang="ts">
    import { onMount } from 'svelte';
    import type { Snippet } from 'svelte';
    import X from '@lucide/svelte/icons/x';

    let {
        open,
        title,
        onClose,
        children,
        footer,
        size = 'md',
        closeOnBackdrop = false,
        testid
    }: {
        open: boolean;
        title: string;
        onClose: () => void;
        children: Snippet;
        footer?: Snippet;
        size?: 'sm' | 'md' | 'lg';
        /**
         * Whether clicking the dim backdrop closes the modal. Off by
         * default — forms with unsaved input would discard typed data
         * on a misclick. Opt-in for confirm dialogs and read-only
         * popovers.
         */
        closeOnBackdrop?: boolean;
        testid?: string;
    } = $props();

    let dialog: HTMLDivElement | undefined = $state();

    // Track previous focus so we can restore it on close — a basic
    // requirement for any focus-trapping modal.
    let previouslyFocused: HTMLElement | null = null;

    $effect(() => {
        if (open) {
            previouslyFocused = document.activeElement as HTMLElement | null;
            // Defer until the dialog mounts.
            queueMicrotask(() => dialog?.focus());
        } else if (previouslyFocused) {
            previouslyFocused.focus();
            previouslyFocused = null;
        }
    });

    function focusable(): HTMLElement[] {
        if (!dialog) return [];
        // Standard set of "tab can land here" elements, minus those
        // disabled or with `tabindex=-1`. Sufficient for our forms.
        const selector = [
            'a[href]',
            'button:not([disabled])',
            'input:not([disabled]):not([type="hidden"])',
            'select:not([disabled])',
            'textarea:not([disabled])',
            '[tabindex]:not([tabindex="-1"])'
        ].join(',');
        return Array.from(dialog.querySelectorAll<HTMLElement>(selector));
    }

    function onKeydown(e: KeyboardEvent) {
        if (!open) return;
        if (e.key === 'Escape') {
            e.stopPropagation();
            onClose();
            return;
        }
        if (e.key === 'Tab') {
            // Wrap focus inside the dialog so Tab/Shift+Tab don't
            // escape to the background page.
            const items = focusable();
            if (items.length === 0) {
                e.preventDefault();
                dialog?.focus();
                return;
            }
            const first = items[0];
            const last = items[items.length - 1];
            const active = document.activeElement as HTMLElement | null;
            if (e.shiftKey) {
                if (active === first || !dialog?.contains(active)) {
                    e.preventDefault();
                    last.focus();
                }
            } else if (active === last) {
                e.preventDefault();
                first.focus();
            }
        }
    }

    onMount(() => {
        document.addEventListener('keydown', onKeydown);
        return () => document.removeEventListener('keydown', onKeydown);
    });

    function onBackdropClick(e: MouseEvent) {
        if (!closeOnBackdrop) return;
        if (e.target === e.currentTarget) onClose();
    }
</script>

{#if open}
    <div
        class="backdrop"
        onclick={onBackdropClick}
        role="presentation"
        data-testid={testid ? `${testid}-backdrop` : undefined}
    >
        <div
            class="dialog size-{size}"
            role="dialog"
            aria-modal="true"
            aria-labelledby="modal-title"
            tabindex="-1"
            bind:this={dialog}
            data-testid={testid}
        >
            <header class="header">
                <h2 id="modal-title">{title}</h2>
                <button
                    type="button"
                    class="close"
                    onclick={onClose}
                    aria-label="Close"
                    title="Close"
                    data-testid={testid ? `${testid}-close` : undefined}
                >
                    <X size={18} aria-hidden="true" />
                </button>
            </header>
            <div class="body">{@render children()}</div>
            {#if footer}
                <footer class="footer">{@render footer()}</footer>
            {/if}
        </div>
    </div>
{/if}

<style>
    .backdrop {
        position: fixed;
        inset: 0;
        background: rgba(0, 0, 0, 0.4);
        display: flex;
        align-items: center;
        justify-content: center;
        padding: var(--space-4);
        z-index: var(--z-modal);
    }

    .dialog {
        background: var(--surface);
        color: var(--text);
        border: 1px solid var(--border);
        border-radius: var(--radius-lg);
        box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
        max-height: 90vh;
        display: flex;
        flex-direction: column;
        width: 100%;
        outline: none;
    }

    .size-sm {
        max-width: 24rem;
    }
    .size-md {
        max-width: 32rem;
    }
    .size-lg {
        max-width: 48rem;
    }

    .header {
        display: flex;
        align-items: center;
        justify-content: space-between;
        padding: var(--space-3) var(--space-4);
        border-bottom: 1px solid var(--border);
    }

    .header h2 {
        margin: 0;
        font-size: var(--font-lg);
    }

    .close {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        width: 32px;
        height: 32px;
        padding: 0;
        background: transparent;
        color: var(--text-muted);
        border: 1px solid transparent;
        border-radius: var(--radius-sm);
    }

    .close:hover {
        background: var(--surface-elevated);
        color: var(--text);
    }

    .body {
        padding: var(--space-4);
        overflow-y: auto;
    }

    .footer {
        padding: var(--space-3) var(--space-4);
        border-top: 1px solid var(--border);
        display: flex;
        gap: var(--space-2);
        justify-content: flex-end;
    }
</style>
