"use client";

import React, { useCallback, useState, useEffect, useMemo, useRef } from "react";
import { CloudUploadOutlined, PictureOutlined } from "@ant-design/icons";
import { Position, NodeProps, Node, useReactFlow } from "@xyflow/react";
import { BaseNode } from "./BaseNode";
import { NodeHandle } from "./NodeHandle";
import { useWorkflowStore } from "@/store/workflowStore";
import { useNodeInlinePanelStore } from "@/store/nodeInlinePanelStore";
import { useGenerationPreferenceStore } from "@/store/generationPreferenceStore";
import { NanoBananaNodeData, SelectedModel } from "@/types";
import { ProviderModel } from "@/lib/providers/types";
import { ModelSearchDialog } from "@/components/modals/ModelSearchDialog";
import { useToast } from "@/components/Toast";
import { getImageDimensions, calculateMediaNodeSizeByShortSide } from "@/utils/nodeDimensions";
import { browseRegistry } from "@/utils/browseRegistry";
import { useAdaptiveImageSrc } from "@/hooks/useAdaptiveImageSrc";
import { downloadMedia } from "@/utils/downloadMedia";
import { useI18n } from "@/i18n";
import { MediaPreviewModal } from "@/components/MediaPreviewModal";
import { createPopiserverDefaultImageModel } from "@/store/utils/defaultImageModel";
import { POPI_PROVIDER_ID } from "@/lib/providerMode";
import { chooseModelFromList, toSelectedModel } from "@/utils/modelSelection";
import { useModelStore } from "@/store/modelStore";
import {
  readImageGenerationConfig,
} from "@/utils/generationConfig";
import { formatUserFacingError } from "@/utils/userFacingErrors";
import { SINGLE_INPUT_HANDLE_ID, SINGLE_OUTPUT_HANDLE_ID } from "@/utils/nodeHandles";
import { useSelectedNodeCount } from "@/hooks/useSelectedNodeCount";
import { NodeActionCapsule, type NodeActionCapsuleAction } from "./NodeActionCapsule";
import { SaveToAssetLibraryModal } from "@/components/SaveToAssetLibraryModal";
import { MediaCropOverlay, type MediaCropAspectRatio, type MediaCropConfirmPayload } from "@/components/media/MediaCropOverlay";
import { CropRatioSelect, MEDIA_EDIT_TOOL, MediaEditToolbar, type MediaEditTool, type MediaEditToolOption } from "@/components/media/MediaEditToolbar";
import { createCroppedImageBlob } from "@/components/media/imageCropUtils";
import { uploadBlobToGatewayMedia } from "@/utils/gatewayMediaUpload";
import { SplitGridCapsuleMenu, type SplitGridSelection } from "@/components/media/SplitGridCapsuleMenu";
import { createSplitGridImageNodes } from "@/utils/splitGridNodes";
import { createDerivedImageNode } from "@/utils/derivedImageNodes";
import { SplitGridSelectionOverlay } from "@/components/media/SplitGridSelectionOverlay";
import { SplitGridSelectionToolbar } from "@/components/media/SplitGridSelectionToolbar";

const IMAGE_LOAD_RETRY_DELAYS_MS = [600, 1600];
const IMAGE_MEDIA_EDIT_TOOLS: MediaEditToolOption[] = [
  { key: MEDIA_EDIT_TOOL.crop, label: "裁剪" },
];

function asImageDimensions(value: unknown): { width: number; height: number } | null {
  if (!value || typeof value !== "object") return null;
  const dimensions = value as { width?: unknown; height?: unknown };
  return typeof dimensions.width === "number" && typeof dimensions.height === "number"
    ? { width: dimensions.width, height: dimensions.height }
    : null;
}

function isSameSelectedModel(a: SelectedModel | undefined, b: SelectedModel | undefined): boolean {
  return Boolean(a?.provider && b?.provider && a.provider === b.provider && a.modelId === b.modelId);
}

type NanoBananaNodeType = Node<NanoBananaNodeData, "nanoBanana">;

export interface GenerateImageNodeViewProps {
  id: string;
  data: NanoBananaNodeData;
  selected?: boolean;
}

export function GenerateImageNodeView({ id, data, selected }: GenerateImageNodeViewProps) {
  const nodeData = data;
  const { t } = useI18n();
  const preferredDisplayImage = data.previewImg || data.outputImage;
  const [imageRetryNonce, setImageRetryNonce] = useState(0);
  const [displayFallbackImage, setDisplayFallbackImage] = useState<string | null>(null);
  const displayImage = displayFallbackImage || preferredDisplayImage;
  const adaptiveOutputImage = useAdaptiveImageSrc(displayImage, id);
  const outputImageSrc = adaptiveOutputImage;
  const updateNodeData = useWorkflowStore((state) => state.updateNodeData);
  const addNode = useWorkflowStore((state) => state.addNode);
  const onConnect = useWorkflowStore((state) => state.onConnect);
  const getNodeById = useWorkflowStore((state) => state.getNodeById);
  const selectSingleNode = useWorkflowStore((state) => state.selectSingleNode);
  const openInlinePanel = useNodeInlinePanelStore((state) => state.openPanel);
  const updateImagePreference = useGenerationPreferenceStore((state) => state.updateImagePreference);
  const popiserverImageModels = useModelStore((state) => state.byKind.image);
  const [isLoadingCarouselImage, setIsLoadingCarouselImage] = useState(false);
  const [isBrowseDialogOpen, setIsBrowseDialogOpen] = useState(false);
  const [isPreviewOpen, setIsPreviewOpen] = useState(false);
  const [isSaveToAssetLibraryOpen, setIsSaveToAssetLibraryOpen] = useState(false);
  const [isCropping, setIsCropping] = useState(false);
  const [activeMediaTool, setActiveMediaTool] = useState<MediaEditTool>(MEDIA_EDIT_TOOL.crop);
  const [cropAspectRatio, setCropAspectRatio] = useState<MediaCropAspectRatio>("original");
  const [cropPayload, setCropPayload] = useState<MediaCropConfirmPayload | null>(null);
  const [isCropUploading, setIsCropUploading] = useState(false);
  const [isSplitGridRunning, setIsSplitGridRunning] = useState(false);
  const [gridSelection, setGridSelection] = useState<SplitGridSelection | null>(null);
  const [selectedGridCellKeys, setSelectedGridCellKeys] = useState<Set<string>>(() => new Set());
  const [cropImageDimensions, setCropImageDimensions] = useState<{ width: number; height: number } | null>(null);
  const imageRef = useRef<HTMLImageElement>(null);

  useEffect(() => {
    setDisplayFallbackImage(null);
    setImageRetryNonce(0);
    setCropImageDimensions(null);
  }, [preferredDisplayImage]);

  // Register browse callback for floating header button
  useEffect(() => {
    browseRegistry.register(id, () => setIsBrowseDialogOpen(true));
    return () => { browseRegistry.unregister(id); };
  }, [id]);

  const defaultSelectedModel = useMemo(
    () => nodeData.selectedModel ?? createPopiserverDefaultImageModel(),
    [nodeData.selectedModel]
  );
  const nodeConfig = useMemo(
    () => readImageGenerationConfig(nodeData, defaultSelectedModel),
    [defaultSelectedModel, nodeData]
  );
  const modelOptions = popiserverImageModels;

  // Keep imported nodes on the Popi gateway.
  useEffect(() => {
    if (!nodeData.selectedModel || nodeData.selectedModel.provider !== POPI_PROVIDER_ID) {
      const newSelectedModel = createPopiserverDefaultImageModel();
      updateNodeData(id, { selectedModel: newSelectedModel });
    }
  }, [id, nodeConfig, nodeData.selectedModel, updateNodeData]);

  useEffect(() => {
    if (modelOptions.length === 0) return;
    if (nodeConfig.selectedModel?.provider === POPI_PROVIDER_ID && nodeConfig.selectedModel.modelId) return;
    const selectedModel = chooseModelFromList(modelOptions, nodeConfig.selectedModel);
    if (!selectedModel || selectedModel.modelId === nodeConfig.selectedModel?.modelId) return;
    updateImagePreference({ model: selectedModel });
    updateNodeData(id, {
      selectedModel,
      parameters: {},
      modelSchemaRequestId: Date.now(),
    });
  }, [id, modelOptions, nodeConfig.selectedModel, nodeData, updateImagePreference, updateNodeData]);

  const { setNodes } = useReactFlow();

  const regenerateNode = useWorkflowStore((state) => state.regenerateNode);
  const isRunning = useWorkflowStore((state) => state.runningNodeIds.has(id));

  const handleRegenerate = useCallback(() => {
    regenerateNode(id);
  }, [id, regenerateNode]);

  const handleClearImage = useCallback(async () => {
    const history = nodeData.imageHistory || [];
    if (history.length === 0) {
      updateNodeData(id, {
        outputImage: null,
        previewImg: undefined,
        outputImageRef: undefined,
        outputImageStorageStatus: undefined,
        imageHistory: [],
        selectedHistoryIndex: 0,
        status: "idle",
        error: null,
      });
      return;
    }

    const currentIndex = Math.min(Math.max(nodeData.selectedHistoryIndex || 0, 0), history.length - 1);
    const nextHistory = history.filter((_, index) => index !== currentIndex);
    if (nextHistory.length === 0) {
      updateNodeData(id, {
        outputImage: null,
        previewImg: undefined,
        outputImageRef: undefined,
        outputImageStorageStatus: undefined,
        imageHistory: [],
        selectedHistoryIndex: 0,
        status: "idle",
        error: null,
      });
      return;
    }

    const nextIndex = Math.min(currentIndex, nextHistory.length - 1);
    const nextItem = nextHistory[nextIndex];
    setIsLoadingCarouselImage(true);
    const nextImage = nextItem.image ?? null;
    setIsLoadingCarouselImage(false);

    updateNodeData(id, {
      outputImage: nextImage,
      previewImg: nextItem.previewImg,
      outputImageRef: undefined,
      outputImageStorageStatus: nextImage
        ? /^https?:\/\//i.test(nextImage) ? "remote-only" : "localized"
        : undefined,
      imageHistory: nextHistory,
      selectedHistoryIndex: nextIndex,
      status: "idle",
      error: nextImage ? null : "Selected image could not be loaded",
    });
  }, [id, nodeData.imageHistory, nodeData.selectedHistoryIndex, updateNodeData]);

  const handleConfirmCrop = useCallback(async (payload: MediaCropConfirmPayload) => {
    const sourceImage = nodeData.outputImage || displayImage;
    if (!sourceImage) return;

    let blob: Blob | null = null;
    try {
      blob = await createCroppedImageBlob(sourceImage, payload);
    } catch {
      alert(t("imageInput.cropFailed"));
      return;
    }

    if (!blob) {
      alert(t("imageInput.cropFailed"));
      return;
    }

    setIsCropUploading(true);
    try {
      const filename = `crop-${Date.now()}.png`;
      const uploaded = await uploadBlobToGatewayMedia(blob, filename, "image/png", "image");
      const currentNode = getNodeById(id);
      const currentPosition = currentNode?.position ?? { x: 0, y: 0 };
      const currentWidth = typeof currentNode?.width === "number"
        ? currentNode.width
        : typeof currentNode?.style?.width === "number"
          ? currentNode.style.width
          : 300;
      createDerivedImageNode({
        sourceNodeId: id,
        position: {
          x: currentPosition.x + currentWidth + 220,
          y: currentPosition.y,
        },
        image: uploaded.url,
        previewImage: uploaded.previewUrl,
        filename,
        dimensions: { width: payload.outputWidth, height: payload.outputHeight },
        label: t("imageInput.crop"),
        operation: "crop",
        addNode,
        onConnect,
        selectSingleNode,
      });
      setIsCropping(false);
    } catch (error) {
      alert(error instanceof Error ? error.message : t("imageInput.cropFailed"));
    } finally {
      setIsCropUploading(false);
    }
  }, [addNode, displayImage, getNodeById, id, nodeData.outputImage, onConnect, selectSingleNode, t]);

  const handleStartGridSelection = useCallback((selection: SplitGridSelection) => {
    setIsCropping(false);
    setCropPayload(null);
    setGridSelection(selection);
    setSelectedGridCellKeys(new Set());
  }, []);

  const handleToggleGridCell = useCallback((cellKey: string) => {
    setSelectedGridCellKeys((current) => {
      const next = new Set(current);
      if (next.has(cellKey)) {
        next.delete(cellKey);
      } else {
        next.add(cellKey);
      }
      return next;
    });
  }, []);

  const handleCancelGridSelection = useCallback(() => {
    setGridSelection(null);
    setSelectedGridCellKeys(new Set());
  }, []);

  const handleConfirmSplitGrid = useCallback(async () => {
    const sourceNode = getNodeById(id);
    if (!sourceNode || !gridSelection || selectedGridCellKeys.size === 0) return;

    setIsSplitGridRunning(true);
    try {
      const createdNodeIds = createSplitGridImageNodes({
        sourceNode,
        rows: gridSelection.rows,
        cols: gridSelection.cols,
        selectedCellKeys: selectedGridCellKeys,
        addNode,
        onConnect,
        selectSingleNode,
      });
      handleCancelGridSelection();
      createdNodeIds.forEach((nodeId) => void regenerateNode(nodeId));
    } catch (error) {
      alert(t("workflow.splitGrid.failed", { error: error instanceof Error ? error.message : "Unknown error" }));
    } finally {
      setIsSplitGridRunning(false);
    }
  }, [addNode, getNodeById, gridSelection, handleCancelGridSelection, id, onConnect, regenerateNode, selectSingleNode, selectedGridCellKeys, t]);

  const handleCarouselPrevious = useCallback(async () => {
    const history = nodeData.imageHistory || [];
    if (history.length === 0 || isLoadingCarouselImage) return;

    const currentIndex = nodeData.selectedHistoryIndex || 0;
    const newIndex = currentIndex === 0 ? history.length - 1 : currentIndex - 1;
    const imageItem = history[newIndex];

    setIsLoadingCarouselImage(true);
    const image = imageItem.image ?? null;
    setIsLoadingCarouselImage(false);

    if (image) {
      updateNodeData(id, {
        outputImage: image,
        previewImg: imageItem.previewImg,
        selectedHistoryIndex: newIndex,
        status: "idle",
        error: null,
      });
    }
  }, [id, nodeData.imageHistory, nodeData.selectedHistoryIndex, isLoadingCarouselImage, updateNodeData]);

  const handleCarouselNext = useCallback(async () => {
    const history = nodeData.imageHistory || [];
    if (history.length === 0 || isLoadingCarouselImage) return;

    const currentIndex = nodeData.selectedHistoryIndex || 0;
    const newIndex = (currentIndex + 1) % history.length;
    const imageItem = history[newIndex];

    setIsLoadingCarouselImage(true);
    const image = imageItem.image ?? null;
    setIsLoadingCarouselImage(false);

    if (image) {
      updateNodeData(id, {
        outputImage: image,
        previewImg: imageItem.previewImg,
        selectedHistoryIndex: newIndex,
        status: "idle",
        error: null,
      });
    }
  }, [id, nodeData.imageHistory, nodeData.selectedHistoryIndex, isLoadingCarouselImage, updateNodeData]);

  // Handle model selection from browse dialog
  const handleBrowseModelSelect = useCallback((model: ProviderModel) => {
    const newSelectedModel = toSelectedModel(model);
    if (isSameSelectedModel(nodeConfig.selectedModel, newSelectedModel)) {
      updateNodeData(id, { modelSchemaRequestId: Date.now() });
    } else {
      updateImagePreference({ model: newSelectedModel });
      updateNodeData(id, {
        selectedModel: newSelectedModel,
        parameters: {},
        modelSchemaRequestId: Date.now(),
      });
    }
    setIsBrowseDialogOpen(false);
  }, [id, nodeConfig.selectedModel, nodeData, updateImagePreference, updateNodeData]);

  // Dynamic title based on selected model - just the model name
  const displayTitle = useMemo(() => {
    if (nodeConfig.selectedModel?.displayName && nodeConfig.selectedModel.modelId) {
      return nodeConfig.selectedModel.displayName;
    }
    if (nodeData.outputImage && nodeData.lastUsedModel?.displayName) {
      return nodeData.lastUsedModel.displayName;
    }
    return t("node.selectModel");
  }, [nodeConfig.selectedModel?.displayName, nodeConfig.selectedModel?.modelId, nodeData.lastUsedModel?.displayName, nodeData.outputImage, t]);
  const displayError = formatUserFacingError(nodeData.error, t, displayTitle);

  const hasCarouselImages = (nodeData.imageHistory || []).length > 1;

  // Track previous status to detect error transitions
  const prevStatusRef = useRef(nodeData.status);

  // Show toast when error occurs
  useEffect(() => {
    if (nodeData.status === "error" && prevStatusRef.current !== "error" && displayError) {
      useToast.getState().show(t("node.generationFailed"), "error", true, displayError);
    }
    prevStatusRef.current = nodeData.status;
  }, [nodeData.status, displayError, t]);

  // Auto-resize node when output image changes
  const prevOutputImageRef = useRef<string | null>(null);
  useEffect(() => {
    // Only resize when outputImage transitions from null/different to a new value
    if (!nodeData.outputImage || nodeData.outputImage === prevOutputImageRef.current) {
      prevOutputImageRef.current = nodeData.outputImage ?? null;
      return;
    }
    prevOutputImageRef.current = nodeData.outputImage;

    // Use requestAnimationFrame to avoid React Flow update conflicts
    requestAnimationFrame(() => {
      getImageDimensions(nodeData.outputImage!).then((dims) => {
        if (!dims) return;

        const aspectRatio = dims.width / dims.height;

        setNodes((nodes) =>
          nodes.map((node) => {
            if (node.id !== id) return node;

            const newSize = calculateMediaNodeSizeByShortSide(aspectRatio);

            return {
              ...node,
              width: newSize.width,
              height: newSize.height,
              style: { ...node.style, width: newSize.width, height: newSize.height },
            };
          })
        );
      });
    });
  }, [id, nodeData.outputImage, setNodes]);

  const selectedNodeCount = useSelectedNodeCount();
  const editableImage = displayImage;
  const overlayImageDimensions = cropImageDimensions || asImageDimensions(nodeData.dimensions);
  const showSelectedActions = Boolean(selected && selectedNodeCount === 1 && editableImage);
  const renderedImageSrc = isCropping ? editableImage : outputImageSrc;
  const selectedActions: NodeActionCapsuleAction[] = editableImage
    ? [
        {
          key: "crop",
          icon: (
            <svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.8}>
              <path strokeLinecap="round" strokeLinejoin="round" d="M6 3v12a3 3 0 0 0 3 3h12" />
              <path strokeLinecap="round" strokeLinejoin="round" d="M3 6h12a3 3 0 0 1 3 3v12" />
            </svg>
          ),
          label: t("imageInput.crop"),
          onClick: (event) => {
            event.preventDefault();
            event.stopPropagation();
            setActiveMediaTool(MEDIA_EDIT_TOOL.crop);
            setCropAspectRatio("original");
            setCropPayload(null);
            setIsCropping(true);
          },
        },
        {
          key: "split-grid",
          icon: (
            <svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.8}>
              <rect x="4" y="4" width="16" height="16" rx="2" />
              <path strokeLinecap="round" d="M9.33 4v16M14.67 4v16M4 9.33h16M4 14.67h16" />
            </svg>
          ),
          label: t("imageInput.splitGrid"),
          panel: ({ close }) => (
            <SplitGridCapsuleMenu
              busy={isSplitGridRunning}
              onSelect={(selection) => {
                close();
                handleStartGridSelection(selection);
              }}
            />
          ),
        },
        {
          key: "multi-angle",
          icon: (
            <svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.8}>
              <path strokeLinecap="round" strokeLinejoin="round" d="M4 12a8 8 0 0 1 16 0" />
              <path strokeLinecap="round" strokeLinejoin="round" d="M8 12a4 4 0 0 1 8 0" />
              <path strokeLinecap="round" strokeLinejoin="round" d="M12 12l5-5M12 12l-5 5" />
              <circle cx="12" cy="12" r="1.5" />
            </svg>
          ),
          label: t("imageInput.multiAngle"),
          onClick: (event) => {
            event.preventDefault();
            event.stopPropagation();
            openInlinePanel(id, "multiAngle");
          },
        },
        {
          key: "save-to-assets",
          icon: <CloudUploadOutlined className="text-[24px]" />,
          label: t("assetLibrary.saveToLibrary"),
          onClick: (event) => {
            event.preventDefault();
            event.stopPropagation();
            setIsSaveToAssetLibraryOpen(true);
          },
        },
        {
          key: "download",
          icon: (
            <svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.8}>
              <path strokeLinecap="round" strokeLinejoin="round" d="M12 4v11m0 0 4-4m-4 4-4-4M4 17v2h16v-2" />
            </svg>
          ),
          label: t("imageInput.download"),
          onClick: (event) => {
            event.preventDefault();
            event.stopPropagation();
            downloadMedia(editableImage, "image").catch(() => {});
          },
        },
        {
          key: "preview",
          icon: (
            <svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.8}>
              <path strokeLinecap="round" strokeLinejoin="round" d="M8 4H4v4M16 4h4v4M8 20H4v-4M20 16v4h-4" />
            </svg>
          ),
          label: t("imageInput.focusPreview"),
          onClick: (event) => {
            event.preventDefault();
            event.stopPropagation();
            setIsPreviewOpen(true);
          },
        },
        {
          key: "clear",
          icon: (
            <svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.8}>
              <path strokeLinecap="round" strokeLinejoin="round" d="M6 7h12M9 7V5h6v2m-8 0 1 13h8l1-13" />
            </svg>
          ),
          label: t("imageInput.remove"),
          tone: "danger",
          onClick: (event) => {
            event.preventDefault();
            event.stopPropagation();
            void handleClearImage();
          },
        },
      ]
    : [];

  return (
    <>
    <BaseNode
      id={id}
      selected={selected}
      isExecuting={isRunning}
      hasError={nodeData.status === "error"}
      fullBleed
      dataTutorial="generate-image-node"
    >
      <NodeHandle
        type="target"
        position={Position.Left}
        id={SINGLE_INPUT_HANDLE_ID}
        style={{ top: "50%", zIndex: 10 }}
        handleType="generic-input"
        data-tutorial="generate-text-input-handle"
        isConnectable={true}
      />
      <NodeHandle
        type="source"
        position={Position.Right}
        id={SINGLE_OUTPUT_HANDLE_ID}
        style={{ top: "50%", zIndex: 10 }}
        handleType="image"
      />

      {showSelectedActions && !isCropping && !gridSelection && <NodeActionCapsule actions={selectedActions} />}

      {gridSelection && (
        <SplitGridSelectionToolbar
          selectedCount={selectedGridCellKeys.size}
          busy={isSplitGridRunning}
          onCancel={handleCancelGridSelection}
          onConfirm={() => void handleConfirmSplitGrid()}
        />
      )}

      {isCropping && (
        <MediaEditToolbar
          activeTool={activeMediaTool}
          tools={IMAGE_MEDIA_EDIT_TOOLS}
          busy={isCropUploading}
          onCancel={() => setIsCropping(false)}
          context={{
            value: cropAspectRatio,
            onChange: setCropAspectRatio,
            onConfirm: () => {
              if (cropPayload) void handleConfirmCrop(cropPayload);
            },
          }}
          toolComponents={{
            [MEDIA_EDIT_TOOL.crop]: CropRatioSelect,
          }}
        />
      )}

      <div
        className="relative w-full h-full min-h-0 overflow-hidden rounded-lg"
        data-tutorial="generate-output-area"
      >
        {/* Preview area */}
        {displayImage ? (
          <>
            <img
              ref={imageRef}
              key={`${displayImage ?? "empty"}-${imageRetryNonce}`}
              src={renderedImageSrc ?? undefined}
              alt="Generated"
              onLoad={(event) => {
                setImageRetryNonce(0);
                const image = event.currentTarget;
                if (image.naturalWidth > 0 && image.naturalHeight > 0) {
                  setCropImageDimensions({ width: image.naturalWidth, height: image.naturalHeight });
                }
              }}
              onError={() => {
                if (nodeData.previewImg && displayImage === nodeData.previewImg && nodeData.outputImage && nodeData.outputImage !== nodeData.previewImg) {
                  setDisplayFallbackImage(nodeData.outputImage);
                  setImageRetryNonce(0);
                  return;
                }

                const retryIndex = imageRetryNonce;
                const retryDelay = IMAGE_LOAD_RETRY_DELAYS_MS[retryIndex];
                if (retryDelay !== undefined && displayImage && /^https?:\/\//i.test(displayImage)) {
                  window.setTimeout(() => {
                    setImageRetryNonce((current) => current === retryIndex ? current + 1 : current);
                  }, retryDelay);
                }
              }}
              onDoubleClick={(e) => {
                e.preventDefault();
                e.stopPropagation();
                setIsPreviewOpen(true);
              }}
              className="w-full h-full object-contain cursor-zoom-in"
            />
            {isCropping && (
              <MediaCropOverlay
                kind="image"
                mediaElementRef={imageRef}
                naturalWidth={overlayImageDimensions?.width}
                naturalHeight={overlayImageDimensions?.height}
                aspectRatio={cropAspectRatio}
                busy={isCropUploading}
                onCropChange={setCropPayload}
              />
            )}
            {gridSelection && (
              <SplitGridSelectionOverlay
                mediaElementRef={imageRef}
                naturalWidth={overlayImageDimensions?.width}
                naturalHeight={overlayImageDimensions?.height}
                rows={gridSelection.rows}
                cols={gridSelection.cols}
                selectedCellKeys={selectedGridCellKeys}
                busy={isSplitGridRunning}
                onToggleCell={handleToggleGridCell}
              />
            )}
            {/* Loading overlay for generation */}
            {nodeData.status === "loading" && (
              <div className="absolute inset-0 bg-neutral-900/70 flex items-center justify-center">
                <svg
                  className="w-6 h-6 animate-spin text-white"
                  fill="none"
                  viewBox="0 0 24 24"
                >
                  <circle
                    className="opacity-25"
                    cx="12"
                    cy="12"
                    r="10"
                    stroke="currentColor"
                    strokeWidth="3"
                  />
                  <path
                    className="opacity-75"
                    fill="currentColor"
                    d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
                  />
                </svg>
              </div>
            )}
            {/* Error overlay when generation failed */}
            {nodeData.status === "error" && (
              <div className="absolute inset-0 bg-red-900/40 flex flex-col items-center justify-center gap-1">
                <svg
                  className="w-6 h-6 text-white"
                  fill="none"
                  viewBox="0 0 24 24"
                  stroke="currentColor"
                  strokeWidth={2}
                >
                  <path strokeLinecap="round" strokeLinejoin="round" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
                </svg>
                <span className="text-white text-xs font-medium">{t("node.generationFailed")}</span>
                <span className="text-white/70 text-[10px]">See toast for details</span>
              </div>
            )}
            {/* Loading overlay for carousel navigation */}
            {isLoadingCarouselImage && (
              <div className="absolute inset-0 bg-neutral-900/50 flex items-center justify-center">
                <svg
                  className="w-4 h-4 animate-spin text-white"
                  fill="none"
                  viewBox="0 0 24 24"
                >
                  <circle
                    className="opacity-25"
                    cx="12"
                    cy="12"
                    r="10"
                    stroke="currentColor"
                    strokeWidth="3"
                  />
                  <path
                    className="opacity-75"
                    fill="currentColor"
                    d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
                  />
                </svg>
              </div>
            )}
            {/* Carousel controls - overlaid on image bottom */}
            {hasCarouselImages && (
              <div className="absolute bottom-0 left-0 right-0 flex items-center justify-center gap-2 py-1.5 bg-neutral-900/80">
                <button
                  onClick={handleCarouselPrevious}
                  disabled={isLoadingCarouselImage}
                  className="w-5 h-5 rounded hover:bg-white/10 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center text-white/70 hover:text-white transition-colors"
                  title="Previous image"
                >
                  <svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
                    <path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" />
                  </svg>
                </button>
                <span className="text-[10px] text-white/70 min-w-[32px] text-center">
                  {(nodeData.selectedHistoryIndex || 0) + 1} / {(nodeData.imageHistory || []).length}
                </span>
                <button
                  onClick={handleCarouselNext}
                  disabled={isLoadingCarouselImage}
                  className="w-5 h-5 rounded hover:bg-white/10 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center text-white/70 hover:text-white transition-colors"
                  title="Next image"
                >
                  <svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
                    <path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
                  </svg>
                </button>
              </div>
            )}
          </>
        ) : (
          <div className="w-full h-full min-h-[112px] bg-[var(--surface-1)] flex flex-col items-center justify-center">
            {nodeData.status === "loading" ? (
              <svg
                className="w-4 h-4 animate-spin text-[var(--text-muted)]"
                fill="none"
                viewBox="0 0 24 24"
              >
                <circle
                  className="opacity-25"
                  cx="12"
                  cy="12"
                  r="10"
                  stroke="currentColor"
                  strokeWidth="3"
                />
                <path
                  className="opacity-75"
                  fill="currentColor"
                  d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
                />
              </svg>
            ) : nodeData.status === "error" ? (
              <span className="text-[10px] text-red-400 text-center px-2">
                {displayError || "Failed"}
              </span>
            ) : (
              <PictureOutlined className="text-4xl text-[var(--text-disabled)]" />
            )}
          </div>
        )}
      </div>

    </BaseNode>

    {isPreviewOpen && nodeData.outputImage && (
      <MediaPreviewModal
        type="image"
        src={nodeData.outputImage}
        downloadSrc={nodeData.outputImage}
        alt="Generated preview"
        onClose={() => setIsPreviewOpen(false)}
      />
    )}
    <SaveToAssetLibraryModal
      open={isSaveToAssetLibraryOpen}
      mediaUrl={nodeData.outputImage || null}
      onClose={() => setIsSaveToAssetLibraryOpen(false)}
    />

    {/* Model browse dialog */}
    {isBrowseDialogOpen && (
      <ModelSearchDialog
        isOpen={isBrowseDialogOpen}
        onClose={() => setIsBrowseDialogOpen(false)}
        onModelSelected={handleBrowseModelSelect}
        initialCapabilityFilter="image"
      />
    )}
    </>
  );
}

export function GenerateImageNode({ id, data, selected }: NodeProps<NanoBananaNodeType>) {
  return <GenerateImageNodeView id={id} data={data} selected={selected} />;
}

/**
 * @deprecated Use `GenerateImageNode` instead. This alias is kept for backward compatibility
 * with existing workflows but will be removed in a future version.
 */
export { GenerateImageNode as NanoBananaNode };
