import type { ComponentType } from 'react';
import { useCallback } from 'react';
import { ArrowLeftRight, ChevronLeft, X, Minus, ZoomIn, ZoomOut } from 'lucide-react';
import type { LayoutPanel, AppRegistryMap, PanelComponents, PanelComponentEntry } from './types';
import { useWorkspace } from './WorkspaceContext';
import { ZOOM_MIN, ZOOM_MAX, ZOOM_STEP } from './layout-utils';
import { Card } from '@/components/Card';
import { ErrorBoundary } from '@/components/ErrorBoundary';
import { AppPicker } from './AppPicker';
import { PanelErrorFallback } from './PanelErrorFallback';
import {
  ContextMenu,
  ContextMenuTrigger,
  ContextMenuContent,
  ContextMenuItem,
  ContextMenuSeparator,
} from '@/components/ui/context-menu';

type PanelSlotProps = {
  panel: LayoutPanel;
  registry: AppRegistryMap;
  components?: PanelComponents;
  interactive: boolean;
  locked: boolean;
  noHeader: boolean;
  isLastPanel: boolean;
  onSetApp: (panelId: string, appType: string | null) => void;
  onSplit: (panelId: string, direction: 'horizontal' | 'vertical') => void;
  onRemove: (panelId: string) => void;
};

type PanelContextMenuProps = {
  panelId: string;
  hasApp: boolean;
  isLastPanel: boolean;
  // Current content scale, or null when the app opted out of zoom (no zoom entries in the menu).
  zoom: number | null;
  onSplit: (panelId: string, direction: 'horizontal' | 'vertical') => void;
  onRemove: (panelId: string) => void;
  onClearApp: () => void;
  children: React.ReactNode;
};

const isPanelEntry = (v: PanelComponents[string]): v is PanelComponentEntry =>
  typeof v === 'object' && v !== null && 'component' in v;

const PanelContextMenu = ({
  panelId,
  hasApp,
  isLastPanel,
  zoom,
  onSplit,
  onRemove,
  onClearApp,
  children,
}: PanelContextMenuProps) => {
  const { swapSourceId, setSwapSourceId, maximizedPanelId, setMaximizedPanelId } = useWorkspace();
  const isMaximized = maximizedPanelId === panelId;

  return (
    <ContextMenu>
      <ContextMenuTrigger asChild>{children}</ContextMenuTrigger>
      <ContextMenuContent>
        <ContextMenuItem onClick={() => setMaximizedPanelId(isMaximized ? null : panelId)}>
          {isMaximized ? 'Restore' : 'Maximize'}
        </ContextMenuItem>
        <ContextMenuSeparator />
        <ContextMenuItem onClick={() => onSplit(panelId, 'horizontal')}>Split horizontal</ContextMenuItem>
        <ContextMenuItem onClick={() => onSplit(panelId, 'vertical')}>Split vertical</ContextMenuItem>
        {hasApp && <ContextMenuItem onClick={onClearApp}>Clear app</ContextMenuItem>}
        {!isLastPanel && (
          <ContextMenuItem className="text-red-500 focus:text-red-500" onClick={() => onRemove(panelId)}>
            Remove panel
          </ContextMenuItem>
        )}
        <ContextMenuSeparator />
        {swapSourceId ? (
          <ContextMenuItem onClick={() => setSwapSourceId(null)}>Cancel swap</ContextMenuItem>
        ) : (
          <ContextMenuItem onClick={() => setSwapSourceId(panelId)}>Swap with...</ContextMenuItem>
        )}
        {zoom !== null && <ZoomMenuItems panelId={panelId} zoom={zoom} />}
      </ContextMenuContent>
    </ContextMenu>
  );
};

const SwapOverlay = ({ panelId }: { panelId: string }) => {
  const { swapSourceId, onSwap } = useWorkspace();

  if (!swapSourceId || swapSourceId === panelId) return null;

  return (
    <div
      className="absolute inset-0 z-20 flex items-center justify-center bg-duck-teal/10 hover:bg-duck-teal/20 transition-colors cursor-pointer"
      onClick={() => onSwap(swapSourceId, panelId)}
    >
      <div className="flex items-center gap-2 rounded-lg bg-background/90 backdrop-blur-sm px-3 py-2 shadow-lg border border-duck-teal/30">
        <ArrowLeftRight className="h-4 w-4 text-duck-teal" />
        <span className="text-sm font-medium text-duck-teal">Swap here</span>
      </div>
    </div>
  );
};

const SwapSourceIndicator = ({ panelId }: { panelId: string }) => {
  const { swapSourceId, setSwapSourceId } = useWorkspace();

  if (swapSourceId !== panelId) return null;

  return (
    <div className="absolute inset-0 z-20 pointer-events-none ring-2 ring-inset ring-duck-teal/50 rounded-lg">
      <div className="absolute top-2 left-2 pointer-events-auto">
        <button
          type="button"
          onClick={() => setSwapSourceId(null)}
          className="flex items-center gap-1.5 rounded-md bg-duck-teal/90 px-2.5 py-1 text-xs font-medium text-white shadow cursor-pointer hover:bg-duck-teal"
        >
          <ArrowLeftRight className="h-3 w-3" />
          Swapping... click to cancel
        </button>
      </div>
    </div>
  );
};

const TrafficLights = ({
  panelId,
  isLastPanel,
  onRemove,
  onClearApp,
}: {
  panelId: string;
  isLastPanel: boolean;
  onRemove: (panelId: string) => void;
  onClearApp: () => void;
}) => {
  const { maximizedPanelId, setMaximizedPanelId } = useWorkspace();
  const isMaximized = maximizedPanelId === panelId;

  // The button says "Close panel" unless this is the only panel left, and it used to call `onClearApp`
  // either way — `isLastPanel` reached here only to choose the tooltip. So closing a panel from its own
  // chrome was impossible: the panel stayed, emptied, and the only working path was the context menu.
  // Worse than cosmetic now that identity lives in the layout — clearing the app drops the `config` that
  // named the panel's agent while the panel itself survives to be renamed by the next thing put in it.
  const handleClose = useCallback(() => {
    if (isLastPanel) onClearApp();
    else onRemove(panelId);
  }, [isLastPanel, onClearApp, onRemove, panelId]);

  const handleRestore = useCallback(() => {
    setMaximizedPanelId(null);
  }, [setMaximizedPanelId]);

  const handleMaximize = useCallback(() => {
    setMaximizedPanelId(panelId);
  }, [panelId, setMaximizedPanelId]);

  if (isMaximized) {
    return (
      <div className="flex items-center gap-1.5 shrink-0 ml-auto">
        <button
          type="button"
          onClick={handleRestore}
          className="group/btn h-3 w-3 rounded-full bg-[#febc2e] hover:brightness-90 transition-all cursor-pointer flex items-center justify-center"
          title="Restore"
        >
          <Minus
            className="h-2 w-2 text-[#5f4a00] opacity-0 group-hover/btn:opacity-100 transition-opacity"
            strokeWidth={3}
          />
        </button>
      </div>
    );
  }

  return (
    <div className="flex items-center gap-1.5 shrink-0 ml-auto">
      <button
        type="button"
        onClick={handleClose}
        className="group/btn h-3 w-3 rounded-full bg-[#ff5f57] hover:brightness-90 transition-all cursor-pointer flex items-center justify-center"
        title={isLastPanel ? 'Clear app' : 'Close panel'}
      >
        <X
          className="h-2 w-2 text-[#4a0002] opacity-0 group-hover/btn:opacity-100 transition-opacity"
          strokeWidth={3}
        />
      </button>
      <button
        type="button"
        onClick={handleMaximize}
        className="group/btn h-3 w-3 rounded-full bg-[#28c840] hover:brightness-90 transition-all cursor-pointer flex items-center justify-center"
        title="Maximize"
      >
        <svg
          viewBox="0 0 10 10"
          className="h-1.5 w-1.5 text-[#006500] opacity-0 group-hover/btn:opacity-100 transition-opacity"
        >
          <path d="M0 3.5L5 0L10 3.5V10H0Z" fill="currentColor" />
        </svg>
      </button>
    </div>
  );
};

// Locked screens get maximize only — no close, no clear. Green means "there is room to grow", amber
// means "this one is already filling the screen", the same way the two colours read on a mac window;
// `TrafficLights` below switches on the same pair.
const MaximizeButton = ({ panelId }: { panelId: string }) => {
  const { maximizedPanelId, setMaximizedPanelId } = useWorkspace();
  const isMaximized = maximizedPanelId === panelId;

  return (
    <div className="flex items-center gap-1.5 shrink-0 ml-auto">
      <button
        type="button"
        onClick={() => setMaximizedPanelId(isMaximized ? null : panelId)}
        className={`group/btn h-3 w-3 rounded-full hover:brightness-90 transition-all cursor-pointer flex items-center justify-center ${isMaximized ? 'bg-[#febc2e]' : 'bg-[#28c840]'}`}
        title={isMaximized ? 'Restore' : 'Maximize'}
      >
        {isMaximized ? (
          <Minus
            className="h-2 w-2 text-[#5f4a00] opacity-0 group-hover/btn:opacity-100 transition-opacity"
            strokeWidth={3}
          />
        ) : (
          <svg
            viewBox="0 0 10 10"
            className="h-1.5 w-1.5 text-[#006500] opacity-0 group-hover/btn:opacity-100 transition-opacity"
          >
            <path d="M0 3.5L5 0L10 3.5V10H0Z" fill="currentColor" />
          </svg>
        )}
      </button>
    </div>
  );
};

// Per-panel content scale. The percentage only appears once the panel is off 100%, so an untouched
// header looks exactly as it did before; the +/- buttons reveal on header hover. Clicking the
// percentage resets to 100%.
const ZoomControls = ({ panelId, zoom }: { panelId: string; zoom: number }) => {
  const { onSetZoom } = useWorkspace();
  const atDefault = zoom === 1;

  return (
    <div className="flex shrink-0 items-center gap-0.5">
      <button
        type="button"
        onClick={() => onSetZoom(panelId, zoom - ZOOM_STEP)}
        disabled={zoom <= ZOOM_MIN}
        title="Zoom out"
        className="cursor-pointer rounded p-1 opacity-0 transition-all group-hover/header:opacity-100 hover:bg-black/10 focus-visible:opacity-100 disabled:cursor-default disabled:opacity-0 disabled:group-hover/header:opacity-30"
      >
        <ZoomOut className="h-3.5 w-3.5" />
      </button>
      <button
        type="button"
        onClick={() => onSetZoom(panelId, 1)}
        title="Reset zoom"
        className={`w-9 cursor-pointer rounded px-1 text-center text-[11px] tabular-nums text-black/60 transition-all hover:bg-black/10 hover:text-black ${atDefault ? 'opacity-0 group-hover/header:opacity-100' : ''}`}
      >
        {Math.round(zoom * 100)}%
      </button>
      <button
        type="button"
        onClick={() => onSetZoom(panelId, zoom + ZOOM_STEP)}
        disabled={zoom >= ZOOM_MAX}
        title="Zoom in"
        className="cursor-pointer rounded p-1 opacity-0 transition-all group-hover/header:opacity-100 hover:bg-black/10 focus-visible:opacity-100 disabled:cursor-default disabled:opacity-0 disabled:group-hover/header:opacity-30"
      >
        <ZoomIn className="h-3.5 w-3.5" />
      </button>
    </div>
  );
};

const ZoomMenuItems = ({ panelId, zoom }: { panelId: string; zoom: number }) => {
  const { onSetZoom } = useWorkspace();

  return (
    <>
      <ContextMenuSeparator />
      <ContextMenuItem disabled={zoom >= ZOOM_MAX} onClick={() => onSetZoom(panelId, zoom + ZOOM_STEP)}>
        Zoom in
      </ContextMenuItem>
      <ContextMenuItem disabled={zoom <= ZOOM_MIN} onClick={() => onSetZoom(panelId, zoom - ZOOM_STEP)}>
        Zoom out
      </ContextMenuItem>
      <ContextMenuItem disabled={zoom === 1} onClick={() => onSetZoom(panelId, 1)}>
        Reset zoom
      </ContextMenuItem>
    </>
  );
};

const MaximizeContextMenu = ({
  panelId,
  zoom,
  children,
}: {
  panelId: string;
  zoom: number | null;
  children: React.ReactNode;
}) => {
  const { maximizedPanelId, setMaximizedPanelId } = useWorkspace();
  const isMaximized = maximizedPanelId === panelId;

  return (
    <ContextMenu>
      <ContextMenuTrigger asChild>{children}</ContextMenuTrigger>
      <ContextMenuContent>
        <ContextMenuItem onClick={() => setMaximizedPanelId(isMaximized ? null : panelId)}>
          {isMaximized ? 'Restore' : 'Maximize'}
        </ContextMenuItem>
        {zoom !== null && <ZoomMenuItems panelId={panelId} zoom={zoom} />}
      </ContextMenuContent>
    </ContextMenu>
  );
};

// TODO: drag-to-reposition needs work (visual feedback, edge cases)
// const DragHandle = ({ panelId }: { panelId: string }) => {
//   const { setDragSourceId, dragSourceId } = useWorkspace();
//   const onPointerDown = useCallback((ev: React.PointerEvent) => { ev.preventDefault(); setDragSourceId(panelId); }, [panelId, setDragSourceId]);
//   if (dragSourceId) return null;
//   return (
//     <div className="absolute top-2 right-2 z-10 p-1.5 rounded-md bg-duck-dark/5 text-duck-dark/30 opacity-0 group-hover/panel:opacity-100 hover:!bg-duck-dark/10 hover:!text-duck-dark/60 transition-all cursor-grab" onPointerDown={onPointerDown}>
//       <GripVertical className="h-5 w-5" />
//     </div>
//   );
// };
//
// const DragSourceDimmer = ({ panelId }: { panelId: string }) => {
//   const { dragSourceId } = useWorkspace();
//   if (dragSourceId !== panelId) return null;
//   return <div className="absolute inset-0 z-20 rounded-lg bg-duck-dark/10 pointer-events-none" />;
// };

export const PanelSlot = ({
  panel,
  registry,
  components,
  interactive,
  locked,
  noHeader,
  isLastPanel,
  onSetApp,
  onSplit,
  onRemove,
}: PanelSlotProps) => {
  const { maximizedPanelId, transitioningPanelId, isMobile, onMobileBack } = useWorkspace();
  const isMaximized = maximizedPanelId === panel.id;

  const rawPanelComponent = components?.[panel.id];
  const panelEntry = rawPanelComponent && isPanelEntry(rawPanelComponent) ? rawPanelComponent : null;
  const PanelComponent = panelEntry
    ? panelEntry.component
    : (rawPanelComponent as ComponentType<{ panelId: string }> | undefined);

  const entry = panel.appType ? registry[panel.appType] : null;
  const AppComponent = PanelComponent ?? entry?.component;

  // Resolve header, provider, onClose from PanelComponentEntry or registry
  const HeaderComponent = panelEntry?.header ?? entry?.header;
  const ProviderComponent = panelEntry?.provider ?? entry?.provider;
  const onClose = panelEntry?.onClose;

  // null = this app opted out of zoom, so no control and no menu entries.
  const zoom = entry?.zoomable === false ? null : (panel.zoom ?? 1);

  const contextMenu = interactive
    ? locked
      ? (content: React.ReactNode) => (
          <MaximizeContextMenu panelId={panel.id} zoom={zoom}>
            {content}
          </MaximizeContextMenu>
        )
      : (content: React.ReactNode) => (
          <PanelContextMenu
            panelId={panel.id}
            hasApp={!!AppComponent}
            isLastPanel={isLastPanel}
            zoom={zoom}
            onSplit={onSplit}
            onRemove={onRemove}
            onClearApp={() => onSetApp(panel.id, null)}
          >
            {content}
          </PanelContextMenu>
        )
    : (content: React.ReactNode) => <>{content}</>;

  // One app throwing must not take the workspace with it — the other panels are still live, and on a
  // dashboard left running overnight they are the result. Keyed on the panel's identity, so swapping the
  // app or clearing it is itself a recovery. A locked layout offers no "clear": it isn't the user's to edit.
  const guardApp = (node: React.ReactNode) => (
    <ErrorBoundary
      resetKeys={[panel.id, panel.appType]}
      fallback={({ error, reset }) => (
        <PanelErrorFallback
          error={error}
          reset={reset}
          onClearApp={interactive && !locked ? () => onSetApp(panel.id, null) : null}
        />
      )}
    >
      {node}
    </ErrorBoundary>
  );

  const overlays =
    interactive && !locked ? (
      <>
        <SwapOverlay panelId={panel.id} />
        <SwapSourceIndicator panelId={panel.id} />
      </>
    ) : null;

  if (!AppComponent) {
    if (!interactive || locked) {
      return (
        <div className="h-full w-full p-1">
          <div className="h-full w-full rounded-lg border-3 border-duck-teal/50" />
        </div>
      );
    }

    return contextMenu(
      <div data-panel-id={panel.id} className="group/panel relative h-full w-full p-1">
        <Card className="relative h-full w-full flex flex-col items-center justify-center gap-3 p-4">
          <AppPicker registry={registry} onSelect={(type) => onSetApp(panel.id, type)} />
        </Card>
        {overlays}
      </div>,
    );
  }

  if (entry?.transparent) {
    return contextMenu(
      <div data-panel-id={panel.id} className="group/panel relative h-full w-full p-1">
        <div className="@container relative h-full w-full overflow-hidden">
          {guardApp(<AppComponent panelId={panel.id} />)}
        </div>
        {overlays}
      </div>,
    );
  }

  // All apps get a chrome header — a custom HeaderComponent, or the registry's icon and name. The default
  // used to be a component declared here, which makes it a new component *type* on every render: React
  // cannot match it to the previous one, so the header subtree unmounted and remounted continuously. It is
  // harmless while the default is stateless, and stops being harmless the moment it isn't. Elements, not
  // types, are the thing to build inside a render.
  const header = HeaderComponent ? (
    <HeaderComponent panelId={panel.id} />
  ) : entry ? (
    <>
      <entry.icon className="h-3.5 w-3.5 shrink-0" />
      <span className="text-xs font-medium truncate flex-1">{entry.name}</span>
    </>
  ) : null;

  const trafficLights =
    interactive && !isMobile ? (
      locked ? (
        <MaximizeButton panelId={panel.id} />
      ) : (
        <TrafficLights
          panelId={panel.id}
          isLastPanel={isLastPanel}
          onRemove={onRemove}
          onClearApp={() => onSetApp(panel.id, null)}
        />
      )
    ) : null;

  const mobileBackButton =
    isMobile && onMobileBack ? (
      <button
        type="button"
        onClick={onMobileBack}
        className="flex items-center gap-0.5 text-xs font-medium text-black/70 hover:text-black transition-colors cursor-pointer shrink-0"
      >
        <ChevronLeft className="h-4 w-4" />
        Back
      </button>
    ) : null;

  const zoomControls =
    interactive && !isMobile && zoom !== null ? <ZoomControls panelId={panel.id} zoom={zoom} /> : null;

  const headerContent = (
    <div className="group/header shrink-0 flex items-center gap-2 px-3 py-1.5 border-b border-black/10 text-black font-semibold">
      {mobileBackButton}
      {header}
      {!mobileBackButton && onClose && (
        <button onClick={onClose} className="p-1 rounded hover:bg-black/10 transition-colors cursor-pointer">
          <X className="h-3.5 w-3.5" />
        </button>
      )}
      {(zoomControls || trafficLights) && (
        // One shrink-to-fit container so TrafficLights' own ml-auto has no free space left to claim —
        // two competing auto margins would split the header's slack and strand the zoom control mid-row.
        <div className="ml-auto flex shrink-0 items-center gap-2">
          {zoomControls}
          {trafficLights}
        </div>
      )}
    </div>
  );

  const headerBar = interactive ? (
    locked ? (
      <MaximizeContextMenu panelId={panel.id} zoom={zoom}>
        {headerContent}
      </MaximizeContextMenu>
    ) : (
      <PanelContextMenu
        panelId={panel.id}
        hasApp={!!AppComponent}
        isLastPanel={isLastPanel}
        zoom={zoom}
        onSplit={onSplit}
        onRemove={onRemove}
        onClearApp={() => onSetApp(panel.id, null)}
      >
        {headerContent}
      </PanelContextMenu>
    )
  ) : (
    headerContent
  );

  // CSS `zoom`, not `transform: scale`. A transform paints the content at a different size without
  // re-laying it out, so the panel keeps the geometry it had at 100% and anything sized against the
  // viewport or a percentage lands in the wrong place — visible immediately in Chat, whose composer is
  // pinned to the bottom of a box the transform no longer agrees with. `zoom` scales the used lengths
  // instead: children reflow, `h-full` and percentages still resolve to the panel, and rem-based
  // Tailwind text sizes scale too (a container font-size would not — rem is root-relative). Percentages
  // resolve across the zoom boundary, so the wrapper still fills the panel exactly at any factor.
  //
  // @container sits on the zoomed element on purpose: at 1.4 a 700px panel measures 500 layout px, so
  // container queries collapse the same way they would in a genuinely narrower panel. That is the point
  // of the control — bigger content, same layout rules.
  const body = (
    <div className="flex-1 min-h-0">
      <Card className="h-full w-full overflow-hidden p-0 rounded-none border-0 shadow-none">
        <div className="@container h-full w-full" style={zoom !== null && zoom !== 1 ? { zoom } : undefined}>
          {guardApp(<AppComponent panelId={panel.id} />)}
        </div>
      </Card>
    </div>
  );

  const inner = ProviderComponent ? (
    <ProviderComponent panelId={panel.id}>
      {!noHeader && headerBar}
      {body}
    </ProviderComponent>
  ) : (
    <>
      {!noHeader && headerBar}
      {body}
    </>
  );

  // Maximize is a CSS state toggle on THE SAME element — no portal, no remount — so the panel's content
  // (scroll position, media playback, in-flight state) is preserved exactly across maximize/restore.
  // It fills the content REGION (starting below the global Header), not the whole viewport: the panel is
  // confined to the layout's `absolute z-2` content stacking context, which sits under the fixed nav
  // Header (z-10) — so a full-viewport overlay would hide the panel's own header/Restore behind the nav.
  // Clearing the header keeps it visible + the nav accessible. (Maximize only happens in the dashboard
  // shell; previews/mobile no-op it, so the header offset is always the right reference.)
  // Denser glass than a tiled panel, not a darker one. Panel chrome — header text, icons, the zoom
  // control — is black throughout, so the old near-opaque dark fill left every one of them unreadable
  // the moment you maximised. This is the nav Header's exact recipe (`rgba(255,255,255,0.25)` over a
  // 0.35 border), which is the app's established "raised above the rest" surface and is already proven
  // to carry black text.
  const maximizedStyle = { backgroundColor: 'rgba(255, 255, 255, 0.25)', borderColor: 'rgba(255, 255, 255, 0.35)' };
  const normalStyle = noHeader
    ? { backgroundColor: 'rgba(255, 255, 255, 0.12)', borderColor: 'rgba(0, 0, 0, 0.25)' }
    : { backgroundColor: 'rgba(255, 255, 255, 0.12)', borderColor: 'rgba(255, 255, 255, 0.2)' };
  const panelStyle = {
    ...(isMaximized ? maximizedStyle : normalStyle),
    ...(transitioningPanelId === panel.id ? { viewTransitionName: `panel-${panel.id}` } : {}),
  } as React.CSSProperties;

  return (
    <div data-panel-id={panel.id} className="group/panel relative h-full w-full p-1">
      <div
        className={
          isMaximized
            ? 'fixed inset-x-2 bottom-2 top-[56px] z-50 overflow-hidden rounded-lg border backdrop-blur-xl p-2 flex flex-col shadow-2xl md:top-[68px]'
            : 'relative h-full w-full overflow-hidden rounded-lg border backdrop-blur-xl p-2 flex flex-col'
        }
        style={panelStyle}
      >
        {inner}
      </div>
      {overlays}
    </div>
  );
};
