import { useCallback, useEffect, useMemo, useState } from 'react';
import type { ProcessDiagramProps } from './types';
import type { DiagramNode as DiagramNodeType } from '../../api/queries/diagrams';
import { useDiagramData } from './useDiagramData';
import { useZoomPan } from './useZoomPan';
import { useToolbarHover, NodeToolbar } from './NodeToolbar';
import { DiagramNode } from './DiagramNode';
import { DiagramEdge } from './DiagramEdge';
import { CompoundNode } from './CompoundNode';
import { ErrorSection } from './ErrorSection';
import { ZoomControls } from './ZoomControls';
import { Minimap } from './Minimap';
import { isCompoundType } from './node-colors';
import styles from './ProcessDiagram.module.css';

const PADDING = 40;

/** Types that support drill-down — double-click navigates to the target route */
const DRILLDOWN_TYPES = new Set(['DIRECT', 'SEDA', 'TO', 'TO_DYNAMIC']);

export function ProcessDiagram({
  application,
  routeId,
  direction = 'LR',
  selectedNodeId,
  onNodeSelect,
  onNodeAction,
  nodeConfigs,
  knownRouteIds,
  endpointRouteMap,
  className,
  diagramLayout,
  executionOverlay,
  iterationState,
  onIterationChange,
  centerOnNodeId,
  latencyHeatmap,
}: ProcessDiagramProps) {
  // Route stack for drill-down navigation
  const [routeStack, setRouteStack] = useState<string[]>([routeId]);

  // Reset stack when the external routeId prop changes
  useEffect(() => {
    setRouteStack([routeId]);
  }, [routeId]);

  const currentRouteId = routeStack[routeStack.length - 1];
  const isDrilledDown = currentRouteId !== routeId;

  // Keep overlay active when drilled down — the execution tree includes
  // sub-route processors as nested children, so their IDs are in the overlay map.
  const overlayActive = !!executionOverlay;
  const effectiveOverlay = executionOverlay;

  // Only use the pre-fetched diagramLayout for the root route.
  const effectiveLayout = isDrilledDown ? undefined : diagramLayout;

  const { sections, totalWidth, totalHeight, isLoading, error } = useDiagramData(
    application, currentRouteId, direction, effectiveLayout,
  );

  // Build a set of node IDs that should count as "traversed" for edge coloring.
  // This includes ENDPOINT nodes (always traversed) and compound nodes where
  // any descendant was executed (so edges into/out of DO_TRY etc. are green).
  const traversedNodeIds = useMemo(() => {
    const ids = new Set<string>();
    if (!overlayActive || !effectiveOverlay || !sections.length) return ids;

    // ENDPOINTs are always traversed
    for (const section of sections) {
      for (const node of section.nodes) {
        if (node.type === 'ENDPOINT' && node.id) ids.add(node.id);
      }
    }

    // Mark compound nodes as traversed if any descendant was executed
    function hasExecutedDescendant(node: DiagramNodeType): boolean {
      if (effectiveOverlay!.has(node.id)) return true;
      if (node.children) {
        for (const child of node.children) {
          if (hasExecutedDescendant(child)) return true;
        }
      }
      return false;
    }
    function markCompounds(nodes: DiagramNodeType[]) {
      for (const node of nodes) {
        if (isCompoundType(node.type) && node.children?.length && hasExecutedDescendant(node)) {
          ids.add(node.id);
        }
        if (node.children) markCompounds(node.children);
      }
    }
    for (const section of sections) {
      markCompounds(section.nodes);
    }

    return ids;
  }, [overlayActive, effectiveOverlay, sections]);

  const zoom = useZoomPan();
  const toolbar = useToolbarHover();

  const contentWidth = totalWidth + PADDING * 2;
  const contentHeight = totalHeight + PADDING * 2;

  // Reset to 100% at top-left when route changes
  useEffect(() => {
    if (totalWidth > 0 && totalHeight > 0) {
      zoom.resetView();
    }
  }, [totalWidth, totalHeight, currentRouteId]); // eslint-disable-line react-hooks/exhaustive-deps

  // Center on a specific node when centerOnNodeId changes
  useEffect(() => {
    if (!centerOnNodeId || sections.length === 0) return;
    const node = findNodeById(sections, centerOnNodeId);
    if (!node) return;
    const container = zoom.containerRef.current;
    if (!container) return;
    // Compute the node center in diagram coordinates
    const nodeX = (node.x ?? 0) + (node.width ?? 160) / 2;
    const nodeY = (node.y ?? 0) + (node.height ?? 40) / 2;
    // Find which section the node is in to add its offsetY
    let sectionOffsetY = 0;
    for (const section of sections) {
      const found = findNodeInSection(section.nodes, centerOnNodeId);
      if (found) { sectionOffsetY = section.offsetY; break; }
    }
    const adjustedY = nodeY + sectionOffsetY;
    // Pan so the node center is at the viewport center
    const cw = container.clientWidth;
    const ch = container.clientHeight;
    const scale = zoom.state.scale;
    zoom.panTo(
      cw / 2 - nodeX * scale,
      ch / 2 - adjustedY * scale,
    );
  }, [centerOnNodeId]); // eslint-disable-line react-hooks/exhaustive-deps

  // Resolve execution state for a node. ENDPOINT nodes (the route's "from:")
  // don't appear in the processor execution tree, but should be marked as
  // COMPLETED when the route executed (i.e., overlay has any entries).
  const getNodeExecutionState = useCallback(
    (nodeId: string | undefined, nodeType: string | undefined) => {
      if (!nodeId || !effectiveOverlay) return undefined;
      const state = effectiveOverlay.get(nodeId);
      if (state) return state;
      // Synthesize COMPLETED for ENDPOINT nodes when overlay is active
      if (nodeType === 'ENDPOINT' && effectiveOverlay.size > 0) {
        return { status: 'COMPLETED' as const, durationMs: 0, hasTraceData: false };
      }
      return undefined;
    },
    [effectiveOverlay],
  );

  const handleNodeClick = useCallback(
    (nodeId: string) => {
      // Suppress click if the pointer gesture was a drag (pan)
      if (zoom.didPan.current) return;
      onNodeSelect?.(nodeId);
    },
    [onNodeSelect, zoom.didPan],
  );

  const handleNodeDoubleClick = useCallback(
    (nodeId: string) => {
      const node = findNodeById(sections, nodeId);
      if (!node || !DRILLDOWN_TYPES.has(node.type ?? '')) return;

      // Try static endpointUri first, then runtime-resolved URI from execution overlay
      const staticUri = node.endpointUri;
      const runtimeUri = executionOverlay?.get(nodeId)?.resolvedEndpointUri;

      function normalize(uri: string): string {
        return uri.split('?')[0];
      }

      function tryResolve(uri: string | undefined): string | undefined {
        if (!uri) return undefined;
        return endpointRouteMap?.get(normalize(uri));
      }

      const resolved = tryResolve(staticUri) ?? tryResolve(runtimeUri);
      if (resolved) {
        onNodeSelect?.('');
        setRouteStack(prev => [...prev, resolved]);
      }
    },
    [sections, onNodeSelect, endpointRouteMap, executionOverlay],
  );

  const handleBreadcrumbClick = useCallback(
    (index: number) => {
      onNodeSelect?.('');
      setRouteStack(prev => prev.slice(0, index + 1));
    },
    [onNodeSelect],
  );

  const handleNodeAction = useCallback(
    (nodeId: string, action: import('./types').NodeAction) => {
      if (action === 'inspect') onNodeSelect?.(nodeId);
      onNodeAction?.(nodeId, action);
    },
    [onNodeSelect, onNodeAction],
  );

  const handleKeyDown = useCallback(
    (e: React.KeyboardEvent) => {
      if (e.key === 'Escape') {
        if (routeStack.length > 1) {
          setRouteStack(prev => prev.slice(0, -1));
        } else {
          onNodeSelect?.('');
        }
        return;
      }
      zoom.onKeyDown(e, contentWidth, contentHeight);
    },
    [onNodeSelect, zoom, contentWidth, contentHeight, routeStack.length],
  );

  if (isLoading) {
    return (
      <div className={`${styles.container} ${className ?? ''}`}>
        <div className={styles.loading}>Loading diagram...</div>
      </div>
    );
  }

  if (error) {
    return (
      <div className={`${styles.container} ${className ?? ''}`}>
        <div className={styles.error}>Failed to load diagram</div>
      </div>
    );
  }

  if (sections.length === 0) {
    return (
      <div className={`${styles.container} ${className ?? ''}`}>
        <div className={styles.loading}>No diagram data available</div>
      </div>
    );
  }

  const mainSection = sections[0];
  const handlerSections = sections.slice(1);

  return (
    <div
      ref={zoom.containerRef}
      className={`${styles.container} ${className ?? ''}`}
    >
      {/* Breadcrumb bar — only shown when drilled down */}
      {routeStack.length > 1 && (
        <div className={styles.breadcrumbs}>
          {routeStack.map((route, i) => (
            <span key={i} className={styles.breadcrumbItem}>
              {i > 0 && <span className={styles.breadcrumbSep}>/</span>}
              {i < routeStack.length - 1 ? (
                <button
                  className={styles.breadcrumbLink}
                  onClick={() => handleBreadcrumbClick(i)}
                >
                  {route}
                </button>
              ) : (
                <span className={styles.breadcrumbCurrent}>{route}</span>
              )}
            </span>
          ))}
        </div>
      )}

      <svg
        ref={zoom.svgRef}
        className={styles.svg}
        onPointerDown={zoom.onPointerDown}
        onPointerMove={zoom.onPointerMove}
        onPointerUp={zoom.onPointerUp}
        onKeyDown={handleKeyDown}
        tabIndex={0}
        onClick={() => onNodeSelect?.('')}
      >
        <defs>
          <marker
            id="arrowhead"
            markerWidth="8"
            markerHeight="6"
            refX="7"
            refY="3"
            orient="auto"
          >
            <polygon points="0 0, 8 3, 0 6" fill="var(--text-muted)" />
          </marker>
          <marker
            id="arrowhead-green"
            markerWidth="8"
            markerHeight="6"
            refX="7"
            refY="3"
            orient="auto"
          >
            <polygon points="0 0, 8 3, 0 6" fill="var(--success)" />
          </marker>
        </defs>

        <g style={{ transform: zoom.transform, transformOrigin: '0 0' }}>
          {/* Main section top-level edges (not inside compounds) */}
          <g className="edges">
            {mainSection.edges.filter(e => topLevelEdge(e, mainSection.nodes)).map((edge, i) => {
              const sourceHasState = effectiveOverlay?.has(edge.sourceId) || traversedNodeIds.has(edge.sourceId);
              const targetHasState = effectiveOverlay?.has(edge.targetId) || traversedNodeIds.has(edge.targetId);
              const isTraversed = effectiveOverlay
                ? (!!sourceHasState && !!targetHasState)
                : undefined;
              return (
                <DiagramEdge key={`${edge.sourceId}-${edge.targetId}-${i}`} edge={edge} traversed={isTraversed} />
              );
            })}
          </g>

          {/* Main section nodes */}
          <g className="nodes">
            {mainSection.nodes.map(node => {
              if (isCompoundType(node.type) && node.children && node.children.length > 0) {
                return (
                  <CompoundNode
                    key={node.id}
                    node={node}
                    edges={mainSection.edges}
                    selectedNodeId={selectedNodeId}
                    hoveredNodeId={toolbar.hoveredNodeId}
                    nodeConfigs={nodeConfigs}
                    executionOverlay={effectiveOverlay}
                    overlayActive={overlayActive}
                    iterationState={iterationState}
                    onIterationChange={onIterationChange}
                    latencyHeatmap={latencyHeatmap}
                    onNodeClick={handleNodeClick}
                    onNodeDoubleClick={handleNodeDoubleClick}
                    onNodeEnter={toolbar.onNodeEnter}
                    onNodeLeave={toolbar.onNodeLeave}
                  />
                );
              }
              return (
                <DiagramNode
                  key={node.id}
                  node={node}
                  isHovered={toolbar.hoveredNodeId === node.id}
                  isSelected={selectedNodeId === node.id}
                  config={node.id ? nodeConfigs?.get(node.id) : undefined}
                  executionState={getNodeExecutionState(node.id, node.type)}
                  overlayActive={overlayActive}
                  heatmapEntry={node.id ? latencyHeatmap?.get(node.id) : undefined}
                  onClick={() => node.id && handleNodeClick(node.id)}
                  onDoubleClick={() => node.id && handleNodeDoubleClick(node.id)}
                  onMouseEnter={() => node.id && toolbar.onNodeEnter(node.id)}
                  onMouseLeave={toolbar.onNodeLeave}
                />
              );
            })}
          </g>

          {/* Toolbar rendered as HTML overlay below */}

          {/* Handler sections (completion, then error) */}
          {handlerSections.map((section, i) => (
            <ErrorSection
              key={`handler-${i}`}
              section={section}
              totalWidth={totalWidth}
              selectedNodeId={selectedNodeId}
              hoveredNodeId={toolbar.hoveredNodeId}
              nodeConfigs={nodeConfigs}
              executionOverlay={executionOverlay}
              overlayActive={overlayActive}
              iterationState={iterationState}
              onIterationChange={onIterationChange}
              onNodeClick={handleNodeClick}
              onNodeDoubleClick={handleNodeDoubleClick}
              onNodeEnter={toolbar.onNodeEnter}
              onNodeLeave={toolbar.onNodeLeave}
            />
          ))}
        </g>
      </svg>

      {/* Node toolbar — HTML overlay, fixed size regardless of zoom */}
      {toolbar.hoveredNodeId && onNodeAction && (() => {
        const hNode = findNodeById(sections, toolbar.hoveredNodeId!);
        if (!hNode) return null;
        const nodeCenter = (hNode.x ?? 0) + (hNode.width ?? 160) / 2;
        const nodeTop = hNode.y ?? 0;
        const screenX = nodeCenter * zoom.state.scale + zoom.state.translateX;
        const screenY = nodeTop * zoom.state.scale + zoom.state.translateY;
        return (
          <NodeToolbar
            nodeId={toolbar.hoveredNodeId!}
            screenX={screenX}
            screenY={screenY}
            onAction={handleNodeAction}
            onMouseEnter={toolbar.onToolbarEnter}
            onMouseLeave={toolbar.onToolbarLeave}
            nodeConfig={nodeConfigs?.get(toolbar.hoveredNodeId!)}
          />
        );
      })()}

      {/* Heatmap tooltip — HTML overlay below hovered node */}
      {toolbar.hoveredNodeId && latencyHeatmap && !onNodeAction && (() => {
        const hNode = findNodeById(sections, toolbar.hoveredNodeId!);
        const entry = hNode ? latencyHeatmap.get(hNode.id) : undefined;
        if (!hNode || !entry) return null;
        const nodeCenter = (hNode.x ?? 0) + (hNode.width ?? 160) / 2;
        const nodeBottom = (hNode.y ?? 0) + (hNode.height ?? 40);
        const screenX = nodeCenter * zoom.state.scale + zoom.state.translateX;
        const screenY = nodeBottom * zoom.state.scale + zoom.state.translateY;
        const fmtMs = (v: number) => v.toLocaleString(undefined, { maximumFractionDigits: 3 });
        return (
          <div className={styles.heatmapTooltip} style={{ left: screenX, top: screenY }}>
            <div className={styles.heatmapTooltipTitle}>
              {hNode.id}{entry.processorType ? ` (${entry.processorType})` : ''}
            </div>
            <div className={styles.heatmapTooltipGrid}>
              <span className={styles.heatmapTooltipLabel}>Avg</span>
              <span className={styles.heatmapTooltipValue}>{fmtMs(entry.avgDurationMs)}ms</span>
              <span className={styles.heatmapTooltipLabel}>P99</span>
              <span className={styles.heatmapTooltipValue}>{fmtMs(entry.p99DurationMs)}ms</span>
              <span className={styles.heatmapTooltipLabel}>% Time</span>
              <span className={styles.heatmapTooltipValue}>{entry.pctOfRoute.toFixed(1)}%</span>
              {entry.totalCount != null && (<>
                <span className={styles.heatmapTooltipLabel}>Invocations</span>
                <span className={styles.heatmapTooltipValue}>{entry.totalCount.toLocaleString()}</span>
              </>)}
              {entry.errorRate != null && (<>
                <span className={styles.heatmapTooltipLabel}>Error Rate</span>
                <span className={styles.heatmapTooltipValue}>{(entry.errorRate * 100).toFixed(2)}%</span>
              </>)}
            </div>
          </div>
        );
      })()}

      <Minimap
        sections={sections}
        totalWidth={totalWidth}
        totalHeight={totalHeight}
        scale={zoom.state.scale}
        translateX={zoom.state.translateX}
        translateY={zoom.state.translateY}
        containerWidth={zoom.containerRef.current?.clientWidth ?? 0}
        containerHeight={zoom.containerRef.current?.clientHeight ?? 0}
        onPan={zoom.panTo}
        executionOverlay={effectiveOverlay}
      />

      <ZoomControls
        onZoomIn={zoom.zoomIn}
        onZoomOut={zoom.zoomOut}
        onFitToView={() => zoom.fitToView(contentWidth, contentHeight)}
        scale={zoom.state.scale}
      />
    </div>
  );
}

function findNodeById(
  sections: import('./types').DiagramSection[],
  nodeId: string,
): DiagramNodeType | undefined {
  for (const section of sections) {
    for (const node of section.nodes) {
      if (node.id === nodeId) return node;
      if (node.children) {
        const found = findInChildren(node.children, nodeId);
        if (found) return found;
      }
    }
  }
  return undefined;
}

function findInChildren(
  nodes: DiagramNodeType[],
  nodeId: string,
): DiagramNodeType | undefined {
  for (const n of nodes) {
    if (n.id === nodeId) return n;
    if (n.children) {
      const found = findInChildren(n.children, nodeId);
      if (found) return found;
    }
  }
  return undefined;
}

function findNodeInSection(
  nodes: DiagramNodeType[],
  nodeId: string,
): boolean {
  return !!findInChildren(nodes, nodeId) || nodes.some(n => n.id === nodeId);
}

function topLevelEdge(
  edge: import('../../api/queries/diagrams').DiagramEdge,
  nodes: DiagramNodeType[],
): boolean {
  const compoundChildIds = new Set<string>();
  for (const n of nodes) {
    if (n.children && n.children.length > 0) {
      collectDescendantIds(n.children, compoundChildIds);
    }
  }
  return !compoundChildIds.has(edge.sourceId) && !compoundChildIds.has(edge.targetId);
}

function collectDescendantIds(nodes: DiagramNodeType[], set: Set<string>) {
  for (const n of nodes) {
    if (n.id) set.add(n.id);
    if (n.children) collectDescendantIds(n.children, set);
  }
}
