'use client';

import React, {
  createContext,
  useCallback,
  useContext,
  useEffect,
  useId,
  useMemo,
  useState,
} from 'react';
import styles from './DrilldownStat.module.css';

type Accent = 'default' | 'success' | 'error' | 'warning';

interface GroupCtx {
  openId: string | null;
  setOpenId: (id: string | null) => void;
  register: (id: string, content: React.ReactNode) => void;
  unregister: (id: string) => void;
}
const Ctx = createContext<GroupCtx | null>(null);

function Group({ children }: { children: React.ReactNode }) {
  const [openId, setOpenIdState] = useState<string | null>(null);
  const [drawers, setDrawers] = useState<Map<string, React.ReactNode>>(new Map());
  // Drawers that have ever been opened stay mounted (hidden when not active),
  // so their internal state (e.g. fetched data) persists across open/close.
  const [everOpened, setEverOpened] = useState<Set<string>>(new Set());

  const register = useCallback((id: string, content: React.ReactNode) => {
    setDrawers((prev) => {
      // Bail when the same content ref is passed in (e.g. a parent's useMemo'd
      // children) so Group doesn't churn an extra render cycle per parent
      // re-render. JSX literals always allocate, so callers that want this
      // skip should wrap their drawer children with useMemo.
      if (prev.get(id) === content) return prev;
      const next = new Map(prev);
      next.set(id, content);
      return next;
    });
  }, []);

  const unregister = useCallback((id: string) => {
    setDrawers((prev) => {
      if (!prev.has(id)) return prev;
      const next = new Map(prev);
      next.delete(id);
      return next;
    });
    setOpenIdState((curr) => (curr === id ? null : curr));
  }, []);

  const setOpenId = useCallback((id: string | null) => {
    setOpenIdState(id);
    if (id !== null) {
      setEverOpened((prev) => (prev.has(id) ? prev : new Set(prev).add(id)));
    }
  }, []);

  // Memoize the context value so Stat consumers don't re-render every time
  // a drawer registers/unregisters. register, unregister, setOpenId are all
  // stable already; only openId actually changes user-visibly.
  const ctxValue = useMemo(
    () => ({ openId, setOpenId, register, unregister }),
    [openId, setOpenId, register, unregister]
  );

  return (
    <Ctx.Provider value={ctxValue}>
      <div className={styles.group}>
        <div className={styles.row}>{children}</div>
        {Array.from(drawers.entries())
          .filter(([id]) => everOpened.has(id))
          .map(([id, content]) => (
            <div
              key={id}
              className={styles.drawer}
              hidden={openId !== id}
              id={`drilldown-drawer-${id}`}
              role="region"
              aria-labelledby={`drilldown-stat-${id}`}
            >
              {content}
            </div>
          ))}
      </div>
    </Ctx.Provider>
  );
}

interface StatProps {
  label: string;
  value: number | string;
  accent?: Accent;
  interactive?: boolean;
  children?: React.ReactNode;
}

function Stat({ label, value, accent = 'default', interactive = true, children }: StatProps) {
  const id = useId();
  const ctx = useContext(Ctx);
  if (!ctx) throw new Error('DrilldownStat must be inside DrilldownStat.Group');
  const { register, unregister, openId, setOpenId } = ctx;
  const isOpen = openId === id;

  useEffect(() => {
    if (!interactive) return;
    register(id, children);
    return () => unregister(id);
  }, [id, children, interactive, register, unregister]);

  const toggle = useCallback(() => {
    setOpenId(isOpen ? null : id);
  }, [setOpenId, isOpen, id]);

  if (!interactive) {
    return (
      <div className={`${styles.stat} ${styles.staticStat}`} data-testid="drilldown-static-stat">
        <span className={`${styles.value} ${styles[accent]}`}>{value}</span>
        <span className={styles.label}>{label}</span>
      </div>
    );
  }

  return (
    <button
      type="button"
      id={`drilldown-stat-${id}`}
      className={`${styles.stat} ${isOpen ? styles.open : ''}`}
      onClick={toggle}
      aria-expanded={isOpen}
      aria-controls={`drilldown-drawer-${id}`}
    >
      <span className={`${styles.value} ${styles[accent]}`}>
        {value}
        <span className={styles.caret} aria-hidden="true">
          {isOpen ? '▲' : '▼'}
        </span>
      </span>
      <span className={styles.label}>{label}</span>
    </button>
  );
}

export const DrilldownStat = Object.assign(Stat, { Group });
