import { cn } from '@/lib/cn';
import { cva, type VariantProps } from 'class-variance-authority';
import {
    createContext,
    forwardRef,
    useContext,
    useEffect,
    type SubmitEvent as ReactSubmitEvent,
    type HTMLAttributes,
    type ReactNode,
} from 'react';
import { createPortal } from 'react-dom';

/**
 * App-wide modal chrome — compound `Modal` + `Modal.Header` / `Modal.Body` /
 * `Modal.Footer`. Owns the overlay, portal mount, Esc-to-close, body-overflow
 * lock, the Card flex layout, fade overlays at the body's top/bottom, and the
 * sticky-region structure.
 *
 * Mirrors the pattern that ExportTemplateModal nailed first: `padding="none"`
 * Card, `overflow-hidden` so rounded corners clip the fades, `max-h-[90vh]`
 * cap, and `from-card to-transparent` gradient overlays in place of hard
 * dividers between header/body/footer.
 *
 * Forms: caller passes `onSubmit` and the root becomes a `<form>`. Otherwise
 * a `<div>`. `Modal.Body` always scrolls; `Modal.Footer` is optional (omit
 * for dialogs that don't take action like keyboard-shortcut help).
 *
 * Why compound, not props? The existing modals vary too much for an all-prop
 * API — BrowseTemplates wants a right-aligned Link in the header, ExportModal
 * wants a status region between body and footer, HelpDialog has no footer.
 * Compound children let each caller insert what they need without a prop
 * explosion.
 */

// Ref-counted body scroll lock. Stacked modals share this counter so the
// outermost modal's cleanup is what finally restores the original overflow
// — children's cleanups just decrement. The captured `originalOverflow` is
// the body's pre-lock value (typically `''`); if it was already `'hidden'`
// for some reason (a non-modal lock), we leave it alone after release.
let modalLockCount = 0;
let originalOverflow = '';
function acquireBodyScrollLock(): void {
    if (typeof document === 'undefined') return;
    if (modalLockCount === 0) {
        originalOverflow = document.body.style.overflow;
        document.body.style.overflow = 'hidden';
    }
    modalLockCount += 1;
}
function releaseBodyScrollLock(): void {
    if (typeof document === 'undefined') return;
    modalLockCount = Math.max(0, modalLockCount - 1);
    if (modalLockCount === 0) {
        document.body.style.overflow = originalOverflow;
    }
}

const modalCardVariants = cva(
    'animate-pop-in flex max-h-[90vh] w-full flex-col overflow-hidden rounded-xl border border-border bg-card shadow-xl',
    {
        variants: {
            size: {
                sm: 'max-w-sm',
                md: 'max-w-md',
                lg: 'max-w-2xl',
                xl: 'max-w-3xl',
                '2xl': 'max-w-5xl',
            },
        },
        defaultVariants: {
            size: 'md',
        },
    },
);

type ModalSize = NonNullable<VariantProps<typeof modalCardVariants>['size']>;

type ModalProps = {
    open: boolean;
    onClose: () => void;
    size?: ModalSize;
    /**
     * If provided, the modal root element is a `<form>` and submission flows
     * through this handler. Otherwise the root is a `<div>` and callers wire
     * forms inside `Modal.Body` as usual.
     */
    onSubmit?: (e: ReactSubmitEvent<HTMLFormElement>) => void;
    /**
     * When true, clicking the dimmed backdrop closes the modal. Default FALSE —
     * modals close only via their close button / Cancel / Esc, so an accidental
     * outside click never discards in-progress work. Opt a modal back in with
     * `closeOnBackdrop` if a quick dismiss is genuinely wanted.
     */
    closeOnBackdrop?: boolean;
    /**
     * When true, pressing Esc closes the modal. Default true.
     */
    closeOnEsc?: boolean;
    /**
     * Extra classes for the Card itself (rare — only when a modal needs a
     * one-off override like a slightly different max-h).
     */
    className?: string;
    'data-testid'?: string;
    'aria-labelledby'?: string;
    children: ReactNode;
};

type ModalContextValue = {
    onClose: () => void;
};

const ModalContext = createContext<ModalContextValue | null>(null);

function useModalContext(): ModalContextValue {
    const ctx = useContext(ModalContext);
    if (!ctx) {
        throw new Error('Modal subcomponents must be used inside <Modal>.');
    }
    return ctx;
}

function ModalRoot({
    open,
    onClose,
    size = 'md',
    onSubmit,
    closeOnBackdrop = false,
    closeOnEsc = true,
    className,
    children,
    ...rest
}: ModalProps) {
    // Body scroll lock + Esc binding. Keep both inside this single effect so
    // they activate/deactivate atomically — a modal that locks scroll but
    // doesn't unlock on close, or vice versa, is the classic regression.
    //
    // Lock state is ref-counted across all open modals so stacked modals
    // (e.g. a confirm modal on top of a browse modal) restore overflow
    // correctly. Without the counter, each modal saved its parent's
    // current value — and the child's saved `'hidden'` would re-apply on
    // cleanup, leaving the page un-scrollable after navigation.
    useEffect(() => {
        if (!open) return;
        acquireBodyScrollLock();
        const onKey = (e: KeyboardEvent) => {
            if (e.key === 'Escape' && closeOnEsc) onClose();
        };
        window.addEventListener('keydown', onKey);
        return () => {
            window.removeEventListener('keydown', onKey);
            releaseBodyScrollLock();
        };
    }, [open, onClose, closeOnEsc]);

    if (!open || typeof document === 'undefined') return null;

    const cardClass = cn(modalCardVariants({ size }), className);

    // The form-vs-div decision happens at the inner Card level so the outer
    // backdrop never accidentally submits when the user mousedowns on a form
    // control. Card is the flex container that holds Modal.Header / Body /
    // Footer regions.
    const cardInner = (
        <ModalContext.Provider value={{ onClose }}>
            {children}
        </ModalContext.Provider>
    );

    return createPortal(
        <div
            className="animate-fade-in fixed inset-0 z-50 flex items-center justify-center bg-black/40 px-4"
            onMouseDown={(e) => {
                if (e.target === e.currentTarget && closeOnBackdrop) onClose();
            }}
            data-testid={rest['data-testid']}
        >
            {onSubmit ? (
                <form
                    onSubmit={onSubmit}
                    className={cardClass}
                    role="dialog"
                    aria-modal="true"
                    aria-labelledby={rest['aria-labelledby']}
                >
                    {cardInner}
                </form>
            ) : (
                <div
                    className={cardClass}
                    role="dialog"
                    aria-modal="true"
                    aria-labelledby={rest['aria-labelledby']}
                >
                    {cardInner}
                </div>
            )}
        </div>,
        document.body,
    );
}

type ModalHeaderProps = {
    /** Tiny uppercase-ish label above the title (e.g. "Blank", "Template"). */
    eyebrow?: ReactNode;
    /** Optional class override applied to the eyebrow span — used by Delete
     *  modal where the eyebrow is destructive red. */
    eyebrowClassName?: string;
    title: ReactNode;
    /** Optional sentence below the title. Plain text or arbitrary node. */
    subtitle?: ReactNode;
    /**
     * Right-aligned slot next to the title block. Used for things like the
     * BrowseTemplates modal's "Open Templates page →" link, or a close button
     * on dialogs that don't have a Cancel button in the footer.
     */
    actions?: ReactNode;
    /** Override id used to wire `aria-labelledby` to the title. */
    titleId?: string;
} & Omit<HTMLAttributes<HTMLDivElement>, 'title'>;

function ModalHeader({
    eyebrow,
    eyebrowClassName,
    title,
    subtitle,
    actions,
    titleId,
    className,
    ...rest
}: ModalHeaderProps) {
    return (
        <div
            className={cn(
                'flex shrink-0 items-start justify-between gap-3 px-6 pb-3 pt-6',
                className,
            )}
            {...rest}
        >
            <div className="min-w-0">
                {eyebrow && (
                    <span
                        className={cn(
                            'text-[11px] font-medium text-muted-foreground',
                            eyebrowClassName,
                        )}
                    >
                        {eyebrow}
                    </span>
                )}
                <h3
                    id={titleId}
                    className={cn(
                        'font-sans text-2xl font-semibold tracking-tight text-foreground',
                        eyebrow ? 'mt-2' : undefined,
                    )}
                >
                    {title}
                </h3>
                {subtitle && (
                    <p className="mt-1 text-sm text-muted-foreground">
                        {subtitle}
                    </p>
                )}
            </div>
            {actions && <div className="shrink-0">{actions}</div>}
        </div>
    );
}

type ModalBodyProps = HTMLAttributes<HTMLDivElement> & {
    /**
     * When false, suppresses the top + bottom fade overlays. Most callers want
     * them; opt out for bodies where the fade visibly clashes with content
     * (e.g. an embedded chart that paints over the gradient).
     */
    fade?: boolean;
};

const ModalBody = forwardRef<HTMLDivElement, ModalBodyProps>(
    ({ fade = true, className, children, ...rest }, ref) => {
        return (
            <div className="relative flex min-h-0 flex-1 flex-col">
                <div
                    ref={ref}
                    className={cn(
                        'sleek-scroll flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto px-6 py-3',
                        className,
                    )}
                    {...rest}
                >
                    {children}
                </div>
                {fade && (
                    <>
                        <div
                            aria-hidden
                            className="pointer-events-none absolute inset-x-0 top-0 h-6 bg-gradient-to-b from-card to-transparent"
                        />
                        <div
                            aria-hidden
                            className="pointer-events-none absolute inset-x-0 bottom-0 h-6 bg-gradient-to-t from-card to-transparent"
                        />
                    </>
                )}
            </div>
        );
    },
);
ModalBody.displayName = 'Modal.Body';

type ModalFooterProps = HTMLAttributes<HTMLDivElement>;

function ModalFooter({ className, children, ...rest }: ModalFooterProps) {
    return (
        <div
            className={cn(
                'flex shrink-0 items-center justify-end gap-2 bg-card px-6 pb-5 pt-3',
                className,
            )}
            {...rest}
        >
            {children}
        </div>
    );
}

export const Modal = Object.assign(ModalRoot, {
    Header: ModalHeader,
    Body: ModalBody,
    Footer: ModalFooter,
});

// Re-export for callers that need to nest a custom close-button or read the
// context (e.g. a sub-component inside Modal.Body wanting `onClose`).
export { useModalContext };
