import { useCallback, useEffect, useRef } from 'react';
import { ResizablePanel, ResizablePanelGroup, ResizableHandle } from '@/components/ui/resizable';
import type { LayoutNode, AppRegistryMap, PanelComponents } from './types';
import { countPanels } from './layout-utils';
import { PanelSlot } from './PanelSlot';

type WorkspaceRendererProps = {
  layout: LayoutNode;
  registry: AppRegistryMap;
  components?: PanelComponents;
  interactive?: boolean;
  locked?: boolean;
  noHeader?: boolean;
  isMobile?: boolean;
  mobilePanelId?: string;
  onSetApp: (panelId: string, appType: string | null) => void;
  onSplit: (panelId: string, direction: 'horizontal' | 'vertical') => void;
  onRemove: (panelId: string) => void;
  onResized: (groupId: string, sizes: number[]) => void;
};

export const WorkspaceRenderer = ({
  layout,
  registry,
  components,
  interactive = false,
  locked = false,
  noHeader = false,
  isMobile = false,
  mobilePanelId,
  onSetApp,
  onSplit,
  onRemove,
  onResized,
}: WorkspaceRendererProps) => {
  const totalPanels = countPanels(layout);

  return (
    <div className="h-full w-full">
      <LayoutNodeRenderer
        node={layout}
        registry={registry}
        components={components}
        interactive={interactive}
        locked={locked}
        noHeader={noHeader}
        isMobile={isMobile}
        mobilePanelId={mobilePanelId}
        totalPanels={totalPanels}
        onSetApp={onSetApp}
        onSplit={onSplit}
        onRemove={onRemove}
        onResized={onResized}
      />
    </div>
  );
};

type LayoutNodeRendererProps = {
  node: LayoutNode;
  registry: AppRegistryMap;
  components?: PanelComponents;
  interactive: boolean;
  locked: boolean;
  noHeader: boolean;
  isMobile: boolean;
  mobilePanelId?: string;
  totalPanels: number;
  onSetApp: (panelId: string, appType: string | null) => void;
  onSplit: (panelId: string, direction: 'horizontal' | 'vertical') => void;
  onRemove: (panelId: string) => void;
  onResized: (groupId: string, sizes: number[]) => void;
};

/**
 * Given a group's direct children, find the one whose subtree contains `id` — the child itself if it is
 * the match, otherwise the ancestor it sits under. Not "the node with that id": the mobile collapse below
 * renders one direct child at a time, so what it needs back is the child to render, not the descendant.
 */
const findChildById = (
  children: { node: LayoutNode; size: number }[],
  id: string,
): { node: LayoutNode; size: number } | undefined => {
  for (const child of children) {
    if (child.node.id === id) return child;
    if (child.node.type === 'group') {
      const found = findChildById(child.node.children, id);
      if (found) return child;
    }
  }
  return undefined;
};

const LayoutNodeRenderer = ({
  node,
  registry,
  components,
  interactive,
  locked,
  noHeader,
  isMobile,
  mobilePanelId,
  totalPanels,
  onSetApp,
  onSplit,
  onRemove,
  onResized,
}: LayoutNodeRendererProps) => {
  const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
  const pendingRef = useRef<(() => void) | null>(null);
  const mountedRef = useRef(false);

  // The timer had no cleanup at all, so it outlived the component and fired against whatever it had
  // captured. Flush rather than drop: `onResized` composes against current state now (WorkspaceView passes
  // an updater), so running it early is correct — and dropping it would lose a splitter drag made just
  // before navigating away, which the old no-cleanup version did at least persist.
  useEffect(
    () => () => {
      if (!debounceRef.current) return;
      clearTimeout(debounceRef.current);
      debounceRef.current = null;
      const pending = pendingRef.current;
      pendingRef.current = null;
      pending?.();
    },
    [],
  );

  const handleLayout = useCallback(
    (sizes: number[]) => {
      if (node.type !== 'group') return;
      if (!mountedRef.current) {
        mountedRef.current = true;
        return;
      }
      if (debounceRef.current) clearTimeout(debounceRef.current);
      const groupId = node.id;
      pendingRef.current = () => onResized(groupId, sizes);
      debounceRef.current = setTimeout(() => {
        debounceRef.current = null;
        const pending = pendingRef.current;
        pendingRef.current = null;
        pending?.();
      }, 500);
    },
    [node, onResized],
  );

  if (node.type === 'panel') {
    return (
      <PanelSlot
        panel={node}
        registry={registry}
        components={components}
        interactive={interactive}
        locked={locked}
        noHeader={noHeader}
        isLastPanel={totalPanels <= 1}
        onSetApp={onSetApp}
        onSplit={onSplit}
        onRemove={onRemove}
      />
    );
  }

  // Mobile: render only one panel at a time for horizontal groups
  if (isMobile && node.direction === 'horizontal' && node.children.length > 1) {
    const activeChild = (mobilePanelId && findChildById(node.children, mobilePanelId)) || node.children[0]!;
    return (
      <div className="h-full w-full">
        <LayoutNodeRenderer
          node={activeChild.node}
          registry={registry}
          components={components}
          interactive={interactive}
          locked={locked}
          noHeader={noHeader}
          isMobile={isMobile}
          mobilePanelId={mobilePanelId}
          totalPanels={totalPanels}
          onSetApp={onSetApp}
          onSplit={onSplit}
          onRemove={onRemove}
          onResized={onResized}
        />
      </div>
    );
  }

  // A vertical group holding a fit-to-content panel lays out as a flex column instead of a resizable
  // group: the fitting panel takes its natural height and the rest share what is left. There used to be a
  // second way in here — a `fixedHeight` in pixels declared on the app's registry entry — but no app has
  // ever declared one, so it only ever contributed `undefined` to this test.
  const hasFitChild =
    node.direction === 'vertical' && node.children.some((c) => c.node.type === 'panel' && c.node.fitContent);

  if (hasFitChild) {
    return (
      <div className="flex h-full w-full flex-col">
        {node.children.map((child) => {
          const fit = child.node.type === 'panel' && child.node.fitContent;
          return (
            <div key={child.node.id} className={fit ? 'shrink-0' : 'min-h-0 flex-1'}>
              <LayoutNodeRenderer
                node={child.node}
                registry={registry}
                components={components}
                interactive={interactive}
                locked={locked}
                noHeader={noHeader}
                isMobile={isMobile}
                mobilePanelId={mobilePanelId}
                totalPanels={totalPanels}
                onSetApp={onSetApp}
                onSplit={onSplit}
                onRemove={onRemove}
                onResized={onResized}
              />
            </div>
          );
        })}
      </div>
    );
  }

  return (
    <ResizablePanelGroup id={node.id} direction={node.direction} onLayout={handleLayout} className="h-full w-full">
      {node.children.map((child, i) => (
        <ChildEntry key={child.node.id} index={i}>
          <ResizablePanel id={child.node.id} order={i} defaultSize={child.size} minSize={5}>
            <div className="h-full w-full">
              <LayoutNodeRenderer
                node={child.node}
                registry={registry}
                components={components}
                interactive={interactive}
                locked={locked}
                noHeader={noHeader}
                isMobile={isMobile}
                mobilePanelId={mobilePanelId}
                totalPanels={totalPanels}
                onSetApp={onSetApp}
                onSplit={onSplit}
                onRemove={onRemove}
                onResized={onResized}
              />
            </div>
          </ResizablePanel>
        </ChildEntry>
      ))}
    </ResizablePanelGroup>
  );
};

type ChildEntryProps = {
  index: number;
  children: React.ReactNode;
};

/**
 * Every child but the first is preceded by a splitter handle. The `null` matters: returning
 * `<>{children}</>` for the first child put the panel in slot 0 of the fragment and the handle in slot 0
 * for everyone else, so a panel that became first — remove the leftmost of three, drag one away — found a
 * `ResizableHandle` where it used to be and React remounted it. Keeping two slots keeps the panel at
 * index 1 whatever happens around it. Measured in `WorkspaceRenderer.test.tsx`.
 */
const ChildEntry = ({ index, children }: ChildEntryProps) => (
  <>
    {index > 0 ? <ResizableHandle className="bg-transparent after:bg-transparent" /> : null}
    {children}
  </>
);
