'use client';

import {useCallback, useEffect, useRef, useState} from 'react';
import {Avatar} from '@astryxdesign/core/Avatar';
import {Icon} from '@astryxdesign/core/Icon';
import {IconButton} from '@astryxdesign/core/IconButton';
import {Item} from '@astryxdesign/core/Item';
import {HStack, VStack} from '@astryxdesign/core/Layout';
import {layerAnimations} from '@astryxdesign/core/Layer';
import {usePopover} from '@astryxdesign/core/Popover';
import {useSession} from '@/features/auth/providers/SessionProvider';
import {ICONS} from '@/shared/utils/icons';
import type {AccountPanelId} from './account-menu';
import {useAccountMenu} from './AccountMenuProvider';
import {
  AccountRootPanel,
  AccountSubPanel,
  PANEL_ONE_WIDTH,
  PANEL_SURFACE,
  PANEL_TWO_MOTION,
  PANEL_TWO_WIDTH,
} from './AccountPanels';
import {useSidebar} from './SidebarProvider';
import {useAccountActions} from './useAccountActions';

/**
 * A cascading menu, not a dropdown.
 *
 * The distinction is behavioural, and it is the whole point: clicking a row in
 * panel one opens a SECOND panel beside the first and navigates nowhere. Both
 * panels stay up until the merchant either clicks a real destination in panel
 * two, clicks outside, or presses Escape. This is how a desktop application's
 * account menu behaves; the dropdown it replaces navigated on the first click
 * and dismissed itself before you had chosen anything.
 *
 * ── Why usePopover and not <Popover> ──────────────────────────────────────
 * <Popover> paints the surface — background, radius, shadow — on a wrapper
 * OUTSIDE the content it is given. One surface around two panels is exactly
 * the thing a cascade must not be: the fill would bridge the gap between them
 * and the pair would read as one wide card with a seam down it. `usePopover`
 * takes `hasSurface: false`, which <Popover> does not forward, so the layer
 * stays transparent and each panel paints its own card (see PANEL_SURFACE).
 *
 * Everything else the component gave us is still here, because it all lives in
 * the hook: CSS anchor positioning, the focus trap, native light dismiss and
 * Escape. The only thing hand-wired is the trigger, thirty lines below.
 *
 * ── Dismissal ────────────────────────────────────────────────────────────
 * Outside click and Escape both close EVERYTHING, panel two included — they
 * are the native popover's own light dismiss acting on the single layer that
 * holds both panels, so there is no state in which one panel outlives the
 * other. Panel two is reset on close rather than on open, so re-opening the
 * menu always starts at panel one.
 */

/**
 * 8px between the chip and the panel. --spacing-2 via the Tailwind bridge, and
 * it has to be a utility rather than a Stack prop because the element it
 * applies to is the layer itself, which useLayer renders.
 *
 * It wins over useLayer's own `margin: 0` on layer sorting alone: astryx.css
 * declares @layer astryx-base and Tailwind's utilities layer is declared after
 * it, so no !important is involved.
 */
const LAYER_OFFSET = 'mb-2';

/** The chip. Rounded like a nav item, and lit while its menu is open. */
const CHIP = 'rounded-lg transition-colors duration-150 cursor-pointer hover:bg-white/[0.06] p-2';

/**
 * The collapsed chip, sized to the rail rather than to IconButton's default.
 *
 * AppSideNav widens the collapsed rail to 84px and restyles its items to 44px
 * around 24px icons, but that rule is scoped to `.astryx-side-nav-item` and
 * this is an IconButton — it would otherwise sit at 32px under a column of
 * 44px targets. Both values are spacing tokens: size-11 is 44px, size-6 is 24.
 */
const CHIP_COLLAPSED = 'size-11 mx-auto [&_.astryx-avatar]:size-6';

/**
 * The account chip and its cascading menu, for the inline rail.
 *
 * Rendered in SideNav's `footer`, so it sits in the bottom-left corner at
 * every rail width. Below the rail breakpoint this is not mounted at all —
 * the drawer gets AccountSheetTrigger and the sheet instead.
 */
export function AccountMenu() {
  const {isCollapsed} = useSidebar();
  const {user} = useSession();
  const {locale, setLocale} = useAccountMenu();
  const [activePanel, setActivePanel] = useState<AccountPanelId | null>(null);

  // Set only when a branch is opened from the keyboard, and consumed by the
  // effect below. A ref, not state: it must not cause a render of its own, and
  // the effect that reads it already runs on the render `activePanel` causes.
  const wantsSubFocus = useRef(false);
  const subPanelRef = useRef<HTMLElement>(null);

  const {hide, isOpen, render, toggle, triggerProps, triggerRef} =
    usePopover({
      dialogLabel: 'Account menu',
      // See the header note. This is the reason the hook is used directly.
      hasSurface: false,
      // Reset on CLOSE rather than on open, and from the hide callback rather
      // than an effect watching isOpen: this runs for every way the menu can
      // go away — light dismiss, Escape, a chosen row — and never schedules a
      // second render pass the way `useEffect(() => setState())` would.
      onHide: () => setActivePanel(null),
    });

  const close = useCallback(() => hide(), [hide]);
  const handleLeaf = useAccountActions(close);

  // Mouse users are already looking at panel two; keyboard users are not, and
  // leaving focus on the branch row would make them tab through the rest of
  // panel one to reach the panel they just asked for.
  useEffect(() => {
    if (!activePanel || !wantsSubFocus.current) return;
    wantsSubFocus.current = false;
    subPanelRef.current?.querySelector('button')?.focus();
  }, [activePanel]);

  const handleBranch = useCallback(
    (panel: AccountPanelId, viaKeyboard: boolean) => {
      wantsSubFocus.current = viaKeyboard;
      // Re-selecting the open branch closes panel two, so the same key that
      // opened it also puts it away.
      setActivePanel((current) => (current === panel ? null : panel));
    },
    [],
  );

  const label = user?.name ?? 'Account';

  return (
    <>
      {isCollapsed ? (
        <IconButton
          ref={triggerRef}
          variant="ghost"
          label={label}
          // The avatar draws its own initials from the name; a signed-out visit
          // falls back to Avatar's person glyph rather than someone else's
          // letter.
          icon={
            <Avatar name={user?.name || undefined} size="md" tooltip={false} />
          }
          className={CHIP_COLLAPSED}
          onClick={toggle}
          {...triggerProps}
          data-testid="account-menu-trigger"
        />
      ) : (
        <Item
          ref={triggerRef}
          // role="button" rather than Item's default: given a role, Item skips
          // the inner <button> it would otherwise wrap the label in and puts
          // the handler on the root — which is the element the ARIA below has
          // to describe, and the element the popover is anchored to. With the
          // inner button in play those would be two different nodes.
          role="button"
          tabIndex={0}
          onClick={toggle}
          // A native <button> synthesises click from Enter and Space;
          // role="button" does not, so it is spelled out.
          onKeyDown={(event) => {
            if (event.key === 'Enter' || event.key === ' ') {
              event.preventDefault();
              toggle();
            }
          }}
          {...triggerProps}
          label={label}
          description={user?.email ?? 'Not signed in'}
          labelLines={1}
          descriptionLines={1}
          density="balanced"
          className={CHIP}
          // isHighlighted, not isSelected: Item turns isSelected into
          // aria-current on a root that has no role permitting aria-selected,
          // and "current item" is not what an open menu means. aria-expanded
          // above already says it; this only has to look lit.
          isHighlighted={isOpen}
          startContent={
            <Avatar name={user?.name || undefined} size="sm" tooltip={false} />
          }
          // The switcher glyph, not a chevron: this opens a panel of its own,
          // it does not expand in place.
          endContent={
            <Icon icon={ICONS.switcher} size="sm" color="secondary" />
          }
          data-testid="account-menu-trigger"
        />
      )}

      {render(
        // vAlign="end" keeps both cards' bottom edges flush with each other.
        // The layer's own bottom edge is pinned to the chip, so a panel two
        // that is taller than panel one grows upward instead of hanging below
        // the trigger.
        <HStack gap={2} vAlign="end">
          <VStack
            width={PANEL_ONE_WIDTH}
            padding={2}
            className={PANEL_SURFACE}
            data-testid="account-panel-root"
          >
            <AccountRootPanel
              activePanel={activePanel}
              onBranch={handleBranch}
              onLeaf={handleLeaf}
            />
          </VStack>

          {activePanel ? (
            <VStack
              // Keyed on the panel so moving between branches re-runs the
              // entrance instead of swapping the rows in place.
              key={activePanel}
              ref={subPanelRef}
              width={PANEL_TWO_WIDTH}
              padding={2}
              className={`${PANEL_SURFACE} ${PANEL_TWO_MOTION}`}
              data-testid="account-panel-sub"
            >
              <AccountSubPanel
                panel={activePanel}
                locale={locale}
                onLocale={setLocale}
                onLeaf={handleLeaf}
              />
            </VStack>
          ) : null}
        </HStack>,
        {
          placement: 'above',
          alignment: 'start',
          className: LAYER_OFFSET,
          xstyle: layerAnimations.above,
        },
      )}
    </>
  );
}
