import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { ChevronDown } from 'lucide-react';
import { Button } from './Button';
import { KEYS, DOM_EVENTS } from './interactionConstants';

export interface OverflowMenuItem {
    /** Stable identity for the item (used as React key and focus target). */
    key: string;
    label: React.ReactNode;
    icon?: React.ReactNode;
    onSelect: () => void;
    disabled?: boolean;
    /** Tooltip — surfaces the reason on disabled items (native title attr). */
    title?: string;
    /** 'danger' tints the item red — for destructive actions like Delete. */
    variant?: 'default' | 'danger';
    /** Draw a divider above this item (e.g. to separate a destructive action). */
    dividerBefore?: boolean;
}

export interface OverflowMenuProps {
    items: OverflowMenuItem[];
    /** Trigger label. Defaults to "More". */
    label?: React.ReactNode;
    /** Which edge of the trigger the menu aligns to. Defaults to 'end' (right). */
    align?: 'start' | 'end';
    disabled?: boolean;
    /** Accessible name for the trigger; defaults to "More actions". */
    'aria-label'?: string;
    className?: string;
    /** Extra classes for the trigger button itself (e.g. a fixed toolbar height). */
    triggerClassName?: string;
}

const isEnabled = (item: OverflowMenuItem) => !item.disabled;

interface MenuRect { top: number; bottom: number; left: number; right: number; openUp: boolean }

/**
 * OverflowMenu — a "More ▾" trigger that collapses secondary actions into a
 * dropdown. Presentation-only: it renders the items it is given and calls
 * `onSelect`; it owns no action logic.
 *
 * The menu is portaled to document.body with fixed positioning (mirroring
 * SearchableSelect) so it escapes `overflow-hidden`/`overflow-auto` ancestors —
 * e.g. the sticky record-detail header lives inside a clipping card. It flips
 * above the trigger when there isn't room below, and repositions on scroll/resize.
 * Keyboard: Arrow/Home/End/Enter/Space/Escape/Tab with a roving tabindex.
 */
export const OverflowMenu: React.FC<OverflowMenuProps> = ({
    items,
    label = 'More',
    align = 'end',
    disabled = false,
    'aria-label': ariaLabel = 'More actions',
    className = '',
    triggerClassName = '',
}) => {
    const [open, setOpen] = useState(false);
    const [activeIndex, setActiveIndex] = useState<number>(-1);
    const [rect, setRect] = useState<MenuRect | null>(null);
    const containerRef = useRef<HTMLDivElement>(null);
    const triggerRef = useRef<HTMLButtonElement>(null);
    const menuRef = useRef<HTMLDivElement>(null);
    const itemRefs = useRef<Array<HTMLButtonElement | null>>([]);

    // Recomputed each render — items is a fresh array from the parent, so
    // memoizing on it would never hit; the map is cheap.
    const enabledIndexes = items.reduce<number[]>((acc, item, i) => {
        if (isEnabled(item)) acc.push(i);
        return acc;
    }, []);
    const firstEnabled = enabledIndexes[0] ?? -1;
    const lastEnabled = enabledIndexes[enabledIndexes.length - 1] ?? -1;
    const hasEnabled = enabledIndexes.length > 0;

    const close = useCallback((returnFocus = true) => {
        setOpen(false);
        setActiveIndex(-1);
        if (returnFocus) triggerRef.current?.focus();
    }, []);

    // The menu opens even when every item is disabled: users must still be able
    // to discover which actions exist and read the disabled reasons (titles).
    const openMenu = (focus: 'first' | 'last') => {
        if (disabled || items.length === 0) return;
        setOpen(true);
        setActiveIndex(focus === 'first' ? firstEnabled : lastEnabled);
    };

    // Position the portaled menu relative to the trigger; recompute on scroll/resize.
    useLayoutEffect(() => {
        if (!open) { setRect(null); return; }
        const update = () => {
            const el = triggerRef.current;
            if (!el) return;
            const r = el.getBoundingClientRect();
            const estimated = Math.min(items.length * 40 + 12, 320);
            const spaceBelow = window.innerHeight - r.bottom;
            const openUp = spaceBelow < estimated && r.top > spaceBelow;
            setRect({
                top: r.bottom + 4,
                bottom: window.innerHeight - r.top + 4,
                left: r.left,
                right: window.innerWidth - r.right,
                openUp,
            });
        };
        update();
        window.addEventListener(DOM_EVENTS.SCROLL, update, true);
        window.addEventListener(DOM_EVENTS.RESIZE, update);
        return () => {
            window.removeEventListener(DOM_EVENTS.SCROLL, update, true);
            window.removeEventListener(DOM_EVENTS.RESIZE, update);
        };
    }, [open, items.length]);

    // Close on outside pointer-down — considers both the trigger and the portaled menu.
    useEffect(() => {
        if (!open) return;
        const handler = (e: MouseEvent) => {
            const target = e.target as Node;
            if (!containerRef.current?.contains(target) && !menuRef.current?.contains(target)) {
                close(false);
            }
        };
        document.addEventListener(DOM_EVENTS.MOUSE_DOWN, handler);
        return () => document.removeEventListener(DOM_EVENTS.MOUSE_DOWN, handler);
    }, [open, close]);

    // Keep the active item valid if `items` mutates while open (state-gated actions
    // can appear/disappear); otherwise focus can strand on an unmounted node.
    // All-disabled is NOT a close condition — the menu stays open (read-only)
    // so the disabled items remain discoverable; only an empty list closes it.
    useEffect(() => {
        if (!open) return;
        if (items.length === 0) { close(false); return; }
        if (hasEnabled && !enabledIndexes.includes(activeIndex)) setActiveIndex(firstEnabled);
        if (!hasEnabled && activeIndex !== -1) setActiveIndex(-1);
    }, [open, hasEnabled, activeIndex, firstEnabled, enabledIndexes, close, items.length]);

    // Move DOM focus to the active item once the menu is rendered and positioned.
    useEffect(() => {
        if (open && rect && activeIndex >= 0) itemRefs.current[activeIndex]?.focus();
    }, [open, rect, activeIndex]);

    const stepActive = (dir: 1 | -1) => {
        if (!hasEnabled) return;
        const pos = enabledIndexes.indexOf(activeIndex);
        const next = pos < 0
            ? (dir === 1 ? firstEnabled : lastEnabled)
            : enabledIndexes[(pos + dir + enabledIndexes.length) % enabledIndexes.length];
        setActiveIndex(next);
    };

    // Only arrow keys open from the trigger. Enter/Space are left to the native
    // button `click` (toggling via onClick); handling them here too would
    // double-activate — Space activates on keyup, so a keydown preventDefault
    // wouldn't cancel the ensuing click and the menu would open then close.
    const onTriggerKeyDown = (e: React.KeyboardEvent) => {
        if (e.key === KEYS.ARROW_DOWN) { e.preventDefault(); openMenu('first'); }
        else if (e.key === KEYS.ARROW_UP) { e.preventDefault(); openMenu('last'); }
        // With every item disabled, DOM focus never leaves the trigger (no roving
        // target), so Escape/Tab must close from here too — the menu's own handler
        // only fires when an item has focus. Tab keeps its default so focus moves
        // on instead of stranding the read-only menu open.
        else if (e.key === KEYS.ESCAPE && open) { e.preventDefault(); close(); }
        else if (e.key === KEYS.TAB && open) { close(false); }
    };

    const onMenuKeyDown = (e: React.KeyboardEvent) => {
        switch (e.key) {
            case KEYS.ARROW_DOWN: e.preventDefault(); stepActive(1); break;
            case KEYS.ARROW_UP: e.preventDefault(); stepActive(-1); break;
            case KEYS.HOME: e.preventDefault(); if (hasEnabled) setActiveIndex(firstEnabled); break;
            case KEYS.END: e.preventDefault(); if (hasEnabled) setActiveIndex(lastEnabled); break;
            case KEYS.ESCAPE: e.preventDefault(); close(); break;
            case KEYS.TAB: e.preventDefault(); close(); break;
            default: break;
        }
    };

    const selectItem = (item: OverflowMenuItem) => {
        if (item.disabled) return;
        close();
        item.onSelect();
    };

    if (items.length === 0) return null;

    const menuStyle: React.CSSProperties = { position: 'fixed', zIndex: 50 };
    if (rect) {
        if (align === 'end') menuStyle.right = rect.right; else menuStyle.left = rect.left;
        if (rect.openUp) menuStyle.bottom = rect.bottom; else menuStyle.top = rect.top;
    }

    return (
        <div ref={containerRef} className={`inline-flex ${className}`}>
            <Button
                ref={triggerRef}
                type="button"
                variant="secondary"
                size="sm"
                className={triggerClassName}
                disabled={disabled}
                onClick={() => (open ? close(false) : openMenu('first'))}
                onKeyDown={onTriggerKeyDown}
                aria-haspopup="menu"
                aria-expanded={open}
                aria-label={ariaLabel}
            >
                {label}
                <ChevronDown className="w-3.5 h-3.5" aria-hidden="true" />
            </Button>

            {open && rect && createPortal(
                <div
                    ref={menuRef}
                    role="menu"
                    aria-label={ariaLabel}
                    onKeyDown={onMenuKeyDown}
                    style={menuStyle}
                    className="min-w-[15rem] rounded-xl border border-gray-200 bg-white p-1.5 shadow-lg dark:border-gray-700 dark:bg-gray-800"
                >
                    {items.map((item, i) => (
                        <React.Fragment key={item.key}>
                            {item.dividerBefore && <div className="my-1.5 mx-2 h-px bg-gray-200 dark:bg-gray-700" role="separator" />}
                            <button
                                ref={el => { itemRefs.current[i] = el; }}
                                type="button"
                                role="menuitem"
                                tabIndex={i === activeIndex ? 0 : -1}
                                disabled={item.disabled}
                                title={item.title}
                                onClick={() => selectItem(item)}
                                className={[
                                    'flex w-full items-center gap-2.5 rounded-lg px-3 py-2 text-left text-sm',
                                    'focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500',
                                    'disabled:cursor-not-allowed disabled:opacity-50',
                                    item.variant === 'danger'
                                        ? 'text-red-600 hover:bg-red-50 [&_svg]:text-red-600 dark:text-red-400 dark:hover:bg-red-900/30 dark:[&_svg]:text-red-400'
                                        : 'text-gray-700 hover:bg-gray-50 [&_svg]:text-gray-500 dark:text-gray-200 dark:hover:bg-gray-700 dark:[&_svg]:text-gray-400',
                                ].join(' ')}
                            >
                                {item.icon && <span className="flex-none">{item.icon}</span>}
                                <span className="min-w-0 flex-1 truncate">{item.label}</span>
                            </button>
                        </React.Fragment>
                    ))}
                </div>,
                document.body,
            )}
        </div>
    );
};
