/**
 * Mod-card overflow menu — single document-level handler that opens
 * the kebab dropdown on entity cards that adopt the `.mod-*` markup.
 *
 * The menu portal-detaches to <body> when opened so that:
 *   - the card's `overflow: hidden` doesn't clip the dropdown,
 *   - and a hovered card's `transform: translateY(-2px)` doesn't turn
 *     `position: fixed` into "positioned relative to the card" (a
 *     well-known browser behaviour where `position: fixed` is rooted
 *     in the nearest transformed ancestor instead of the viewport).
 *
 * On close, the menu is re-inserted at its original DOM position so
 * subsequent reconciliations keep markup stable.
 *
 * Markup (emitted by `renderModMenu` in `mod-card.ts`):
 *
 *   <div class="mod-menu-wrap">
 *     <button class="mod-menu-btn" type="button" aria-haspopup="true" aria-expanded="false">⋯</button>
 *     <div class="mod-menu" role="menu">
 *       <button class="mod-menu__item" data-action="duplicate">…</button>
 *       <button class="mod-menu__item" data-action="hide">…</button>
 *       <div class="mod-menu__sep"></div>
 *       <button class="mod-menu__item mod-menu__item--danger" data-action="delete">…</button>
 *     </div>
 *   </div>
 *
 * Action handlers are attached via inline onclick on the menu items
 * themselves (matching the rest of the codebase's pattern).
 *
 * Initialised once from app.ts via `initModMenu()`.
 */

let _initialised = false;
let _controller: AbortController | null = null;

const MENU_VERTICAL_GAP = 4;
const MENU_VIEWPORT_MARGIN = 12;

interface OpenMenuInfo {
    wrap: HTMLElement;
    origParent: Element;
    origNext: Node | null;
}
const _openMenus = new Map<HTMLElement, OpenMenuInfo>();

function _restoreMenu(menu: HTMLElement, info: OpenMenuInfo): void {
    menu.classList.remove('is-open');
    menu.style.top = '';
    menu.style.right = '';
    menu.style.left = '';
    menu.style.transformOrigin = '';
    info.wrap.classList.remove('is-open');
    const btn = info.wrap.querySelector('.mod-menu-btn');
    if (btn) btn.setAttribute('aria-expanded', 'false');
    if (info.origParent.isConnected) {
        info.origParent.insertBefore(menu, info.origNext);
    } else {
        // Card was reconciled away while the menu was open — drop the
        // detached menu rather than reattach to a dead parent.
        menu.remove();
    }
}

function _closeAll(): void {
    _openMenus.forEach((info, menu) => _restoreMenu(menu, info));
    _openMenus.clear();
}

function _openMenu(wrap: HTMLElement): void {
    const btn = wrap.querySelector('.mod-menu-btn') as HTMLElement | null;
    const menu = wrap.querySelector('.mod-menu') as HTMLElement | null;
    if (!btn || !menu) return;

    // Portal the menu to <body> so transformed/overflow-clipping
    // ancestors don't capture or hide it. Track origin so we can
    // restore on close.
    const origParent = menu.parentElement!;
    const origNext = menu.nextSibling;
    document.body.appendChild(menu);
    _openMenus.set(menu, { wrap, origParent, origNext });

    wrap.classList.add('is-open');
    menu.classList.add('is-open');
    btn.setAttribute('aria-expanded', 'true');

    // Position after a paint so offsetHeight is measurable.
    requestAnimationFrame(() => _positionMenu(menu, btn));
}

/** Anchor the fixed-position menu to the kebab button. Opens downward
 *  by default; flips upward when there isn't enough room below the
 *  trigger (e.g. cards near the viewport bottom). */
function _positionMenu(menu: HTMLElement, btn: HTMLElement): void {
    const rect = btn.getBoundingClientRect();
    const menuH = menu.offsetHeight || 140;
    const spaceBelow = window.innerHeight - rect.bottom;

    if (spaceBelow < menuH + MENU_VIEWPORT_MARGIN) {
        // Open upward
        menu.style.top = `${rect.top - menuH - MENU_VERTICAL_GAP}px`;
        menu.style.transformOrigin = 'bottom right';
    } else {
        menu.style.top = `${rect.bottom + MENU_VERTICAL_GAP}px`;
        menu.style.transformOrigin = 'top right';
    }
    menu.style.right = `${window.innerWidth - rect.right}px`;
    menu.style.left = 'auto';
}

function _onClick(e: MouseEvent): void {
    const target = e.target as HTMLElement;
    const btn = target.closest('.mod-menu-btn') as HTMLElement | null;
    if (btn) {
        e.stopPropagation();
        const wrap = btn.closest('.mod-menu-wrap') as HTMLElement | null;
        if (!wrap) return;
        const wasOpen = wrap.classList.contains('is-open');
        _closeAll();
        if (!wasOpen) _openMenu(wrap);
        return;
    }

    // A click on a menu item: let the inline onclick fire, then close.
    const item = target.closest('.mod-menu__item') as HTMLElement | null;
    if (item) {
        // Use a short timeout so the inline onclick (e.g. a confirm prompt)
        // executes before the menu disappears under the user's pointer.
        setTimeout(_closeAll, 0);
        return;
    }

    // Click outside any menu — close all.
    if (!target.closest('.mod-menu')) _closeAll();
}

function _onKeydown(e: KeyboardEvent): void {
    if (e.key === 'Escape') _closeAll();
}

/**
 * Initialise the mod-menu system once at app boot.
 * Idempotent — calling more than once is a no-op.
 */
export function initModMenu(): void {
    if (_initialised) return;
    _initialised = true;
    _controller = new AbortController();
    const { signal } = _controller;
    document.addEventListener('click', _onClick, { signal });
    document.addEventListener('keydown', _onKeydown, { signal });
    // Close on scroll/resize so the fixed-position menu doesn't drift
    // away from its anchor.
    window.addEventListener('scroll', _closeAll, { passive: true, capture: true, signal });
    window.addEventListener('resize', _closeAll, { passive: true, signal });
}

/** Tear down listeners (used by tests / hot reload). */
export function disposeModMenu(): void {
    if (_controller) {
        _controller.abort();
        _controller = null;
    }
    _initialised = false;
    _closeAll();
}

/** Manually close any open menu — exposed for callers that need to
 *  dismiss a menu after a programmatic action (e.g. a confirm dialog
 *  closing). */
export function closeAllModMenus(): void {
    _closeAll();
}
