"use client";

import React, { useCallback, useState, useEffect, useMemo, useRef } from "react";
import { CloudUploadOutlined, PictureOutlined } from "@ant-design/icons";
import { Position, NodeProps, Node } from "@xyflow/react";
import { BaseNode } from "./BaseNode";
import { NodeHandle } from "./NodeHandle";
import { useWorkflowStore } from "@/store/workflowStore";
import { getActiveNodeTool, useNodeToolStore, type NodeToolType } from "@/store/nodeToolStore";
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 { 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, type MediaEditFrameRect } from "@/components/media/MediaCropOverlay";
import { CropRatioSelect, MEDIA_EDIT_TOOL, MediaEditToolbar, type MediaEditTool, type MediaEditToolOption } from "@/components/media/MediaEditToolbar";
import { createMediaEditActionDropdown, getMediaEditMenuLabel, type MediaEditMenuTool } from "@/components/media/MediaEditActionDropdown";
import { MediaOutpaintingOverlay, type MediaOutpaintingFrameRect, type MediaOutpaintingParams } from "@/components/media/MediaOutpaintingOverlay";
import { MediaOutpaintingToolbar, type OutpaintingBatchSize } from "@/components/media/MediaOutpaintingToolbar";
import { SplitGridCapsuleMenu, type SplitGridSelection } from "@/components/media/SplitGridCapsuleMenu";
import { createSplitGridImageNodes } from "@/utils/splitGridNodes";
import { createPendingDerivedImageNode } from "@/utils/derivedImageNodes";
import { SplitGridSelectionOverlay } from "@/components/media/SplitGridSelectionOverlay";
import { SplitGridSelectionToolbar } from "@/components/media/SplitGridSelectionToolbar";
import { ImageEditSession } from "@/components/media/image-edit/ImageEditSession";
import { GridToolIcon, MultiAngleToolIcon } from "@/components/media/MediaToolbarIcons";
import { getAutoMediaElementClassName, getAutoMediaFrameClassName } from "./mediaAutoSize";
import { chooseHighDefinitionModel, createHighDefinitionImageNode, readHighDefinitionSourceImageDimensions } from "@/utils/highDefinitionNodes";
import { deriveOutpaintingOutputDimensions, OUTPAINTING_DEFAULT_PROMPT } from "@/utils/outpaintingNodes";
import { resolveMediaNodeCreationSizeFromDimensions } from "@/utils/nodeSizing";
import { toSelectedModel as providerModelToSelectedModel } from "@/utils/selectedModel";

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

function GenerateImageEditMenuIcon({ tool, className = "h-5 w-5" }: { tool: MediaEditMenuTool; className?: string }) {
  if (tool === "high-definition") {
    return (
      <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.8}>
        <rect x="4" y="5" width="16" height="14" rx="2" />
        <path strokeLinecap="round" strokeLinejoin="round" d="M8 15V9m0 3h4m4-3v6M12 9v6" />
      </svg>
    );
  }
  if (tool === "crop") {
    return (
      <svg className={className} 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>
    );
  }
  if (tool === "inpainting") {
    return (
      <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.8}>
        <path strokeLinecap="round" strokeLinejoin="round" d="m4 16 8.5-8.5a3 3 0 0 1 4.25 0l1.75 1.75a3 3 0 0 1 0 4.25L10 22H4v-6Z" />
        <path strokeLinecap="round" strokeLinejoin="round" d="m9 11 4 4M13 22h7" />
      </svg>
    );
  }
  return (
    <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.8}>
      <rect x="7" y="7" width="10" height="10" rx="1.5" />
      <path strokeLinecap="round" strokeLinejoin="round" d="M4 10V4h6M14 4h6v6M20 14v6h-6M10 20H4v-6" />
    </svg>
  );
}

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;
  renderInputHandle?: boolean;
}

export function GenerateImageNodeView({ id, data, selected, renderInputHandle = true }: GenerateImageNodeViewProps) {
  const nodeData = data;
  const { t } = useI18n();
  const storedImage = (data as NanoBananaNodeData & { image?: string | null; previewImage?: string | null }).image ?? null;
  const storedPreviewImage = (data as NanoBananaNodeData & { image?: string | null; previewImage?: string | null }).previewImage ?? null;
  const primaryImage = data.outputImage || storedImage;
  const primaryPreviewImage = data.previewImg || storedPreviewImage;
  const preferredDisplayImage = primaryPreviewImage || primaryImage;
  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 updateMediaNodeData = useWorkflowStore((state) => state.updateMediaNodeData);
  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 activeNodeTool = useNodeToolStore((state) => getActiveNodeTool(state, id));
  const switchNodeTool = useNodeToolStore((state) => state.switchTool);
  const closeNodeToolForNode = useNodeToolStore((state) => state.closeToolForNode);
  const updateImagePreference = useGenerationPreferenceStore((state) => state.updateImagePreference);
  const popiserverImageModels = useModelStore((state) => state.byKind.image);
  const highDefinitionModels = useModelStore((state) => state.byKind.highDefinition);
  const outpaintingModel = useModelStore((state) => state.byKind.outpainting[0] ?? null);
  const inpaintingModel = useModelStore((state) => state.byKind.inpainting?.[0] ?? null);
  const [isLoadingCarouselImage, setIsLoadingCarouselImage] = useState(false);
  const [isBrowseDialogOpen, setIsBrowseDialogOpen] = useState(false);
  const [isPreviewOpen, setIsPreviewOpen] = useState(false);
  const [isSaveToAssetLibraryOpen, setIsSaveToAssetLibraryOpen] = useState(false);
  const [activeEditMenuTool, setActiveEditMenuTool] = useState<MediaEditMenuTool>("crop");
  const [cropAspectRatio, setCropAspectRatio] = useState<MediaCropAspectRatio>("original");
  const [cropPayload, setCropPayload] = useState<MediaCropConfirmPayload | null>(null);
  const [cropFrameRect, setCropFrameRect] = useState<MediaEditFrameRect | null>(null);
  const [cropImageBoundsRect, setCropImageBoundsRect] = useState<MediaEditFrameRect | null>(null);
  const [outpaintingParams, setOutpaintingParams] = useState<MediaOutpaintingParams | null>(null);
  const [outpaintingFrameRect, setOutpaintingFrameRect] = useState<MediaOutpaintingFrameRect | null>(null);
  const [outpaintingImageBoundsRect, setOutpaintingImageBoundsRect] = useState<MediaOutpaintingFrameRect | null>(null);
  const [outpaintingBatchSize, setOutpaintingBatchSize] = useState<OutpaintingBatchSize>(1);
  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 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, {
        image: null,
        imageRef: undefined,
        assetId: undefined,
        previewImage: undefined,
        outputImage: null,
        previewImg: undefined,
        outputImageRef: undefined,
        outputImageStorageStatus: undefined,
        dimensions: null,
        imageHistory: [],
        selectedHistoryIndex: 0,
        derivedImage: undefined,
        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, {
        image: null,
        imageRef: undefined,
        assetId: undefined,
        previewImage: undefined,
        outputImage: null,
        previewImg: undefined,
        outputImageRef: undefined,
        outputImageStorageStatus: undefined,
        dimensions: null,
        imageHistory: [],
        selectedHistoryIndex: 0,
        derivedImage: undefined,
        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);

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

  const resetToolDraftState = useCallback(() => {
    setCropPayload(null);
    setCropFrameRect(null);
    setCropImageBoundsRect(null);
    setOutpaintingParams(null);
    setOutpaintingFrameRect(null);
    setOutpaintingImageBoundsRect(null);
    setGridSelection(null);
    setSelectedGridCellKeys(new Set());
  }, []);

  const openNodeTool = useCallback((tool: NodeToolType) => {
    resetToolDraftState();
    if (tool === "crop") setCropAspectRatio("original");
    if (tool === "outpainting") setOutpaintingBatchSize(1);
    switchNodeTool(id, tool);
  }, [id, resetToolDraftState, switchNodeTool]);

  const closeNodeTool = useCallback(() => {
    resetToolDraftState();
    closeNodeToolForNode(id);
  }, [closeNodeToolForNode, id, resetToolDraftState]);

  const handleConfirmCrop = useCallback((payload: MediaCropConfirmPayload) => {
    const currentNode = getNodeById(id);
    if (!currentNode) return;

    const currentPosition = currentNode.position ?? { x: 0, y: 0 };
    const currentWidth = typeof currentNode.style?.width === "number"
      ? currentNode.style.width
      : 300;
    const filename = `crop-${Date.now()}.png`;
    const dimensions = { width: payload.outputWidth, height: payload.outputHeight };
    const size = resolveMediaNodeCreationSizeFromDimensions(dimensions) ?? undefined;
    const nodeId = createPendingDerivedImageNode({
      sourceNodeId: id,
      position: {
        x: currentPosition.x + currentWidth + 220,
        y: currentPosition.y,
      },
      filename,
      dimensions,
      customTitle: t("imageInput.crop"),
      operation: "crop",
      task: {
        cropRect: payload.cropRect,
        outputWidth: payload.outputWidth,
        outputHeight: payload.outputHeight,
        aspectRatio: payload.aspectRatio,
      },
      size,
      addNode,
      onConnect,
      selectSingleNode,
    });
    closeNodeTool();
    void regenerateNode(nodeId);
  }, [addNode, closeNodeTool, getNodeById, id, onConnect, regenerateNode, selectSingleNode, t]);

  const handleStartGridSelection = useCallback((selection: SplitGridSelection) => {
    resetToolDraftState();
    switchNodeTool(id, "splitGrid");
    setGridSelection(selection);
    setSelectedGridCellKeys(new Set());
  }, [id, resetToolDraftState, switchNodeTool]);

  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(() => {
    closeNodeTool();
  }, [closeNodeTool]);

  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 handleCreateHighDefinitionNode = useCallback(async () => {
    const currentNode = getNodeById(id);
    const imageSource = primaryImage || displayImage;
    if (!currentNode || !imageSource) return;

    const currentPosition = currentNode.position ?? { x: 0, y: 0 };
    const currentWidth = typeof currentNode.style?.width === "number"
      ? currentNode.style.width
      : 300;
    const dimensions = await readHighDefinitionSourceImageDimensions(currentNode.data as { dimensions?: unknown }, imageSource);
    createHighDefinitionImageNode({
      sourceNodeId: id,
      position: {
        x: currentPosition.x + currentWidth + 220,
        y: currentPosition.y,
      },
      imageSource,
      dimensions,
      selectedModel: chooseHighDefinitionModel(highDefinitionModels),
      addNode,
      onConnect,
      selectSingleNode,
    });
  }, [addNode, displayImage, getNodeById, highDefinitionModels, id, primaryImage, onConnect, selectSingleNode]);

  const handleConfirmOutpainting = useCallback(async (params: MediaOutpaintingParams) => {
    const currentNode = getNodeById(id);
    const imageSource = primaryImage || displayImage;
    if (!currentNode || !imageSource || !outpaintingModel) return;

    const currentPosition = currentNode.position ?? { x: 0, y: 0 };
    const currentWidth = typeof currentNode.style?.width === "number"
      ? currentNode.style.width
      : 300;
    const sourceDimensions = await readHighDefinitionSourceImageDimensions(currentNode.data as { dimensions?: unknown }, imageSource);
    const dimensions = deriveOutpaintingOutputDimensions(sourceDimensions, params) ?? sourceDimensions;
    const size = resolveMediaNodeCreationSizeFromDimensions(dimensions) ?? undefined;
    const selectedModel = providerModelToSelectedModel(outpaintingModel);
    const nodeId = addNode("smartImage", {
      x: currentPosition.x + currentWidth + 220,
      y: currentPosition.y,
    }, {
      inputImages: [imageSource],
      inputPrompt: OUTPAINTING_DEFAULT_PROMPT,
      selectedModel,
      dimensions,
      batchSize: outpaintingBatchSize,
      parameters: {
        batchSize: outpaintingBatchSize,
      },
      extraTaskParams: params,
      filename: `outpainting-${Date.now()}.png`,
      customTitle: "扩图",
      derivedImage: {
        sourceNodeId: id,
        operation: "outpainting",
      },
      status: "idle",
      error: null,
    }, size);
    onConnect({
      source: id,
      sourceHandle: SINGLE_OUTPUT_HANDLE_ID,
      target: nodeId,
      targetHandle: SINGLE_INPUT_HANDLE_ID,
    });
    selectSingleNode(nodeId);
    closeNodeTool();
    await regenerateNode(nodeId);
  }, [
    addNode,
    closeNodeTool,
    displayImage,
    getNodeById,
    id,
    primaryImage,
    onConnect,
    outpaintingBatchSize,
    outpaintingModel,
    regenerateNode,
    selectSingleNode,
  ]);

  const startCropTool = useCallback(() => {
    openNodeTool("crop");
  }, [openNodeTool]);

  const startOutpaintingTool = useCallback(() => {
    openNodeTool("outpainting");
  }, [openNodeTool]);

  const startInpaintingTool = useCallback(() => {
    openNodeTool("inpainting");
  }, [openNodeTool]);

  const handleEditMenuPrimaryClick = useCallback((event: React.MouseEvent<HTMLButtonElement>) => {
    event.preventDefault();
    event.stopPropagation();
    if (activeEditMenuTool === "high-definition") {
      void handleCreateHighDefinitionNode();
      return;
    }
    if (activeEditMenuTool === "outpainting") {
      startOutpaintingTool();
      return;
    }
    if (activeEditMenuTool === "inpainting") {
      startInpaintingTool();
      return;
    }
    setActiveEditMenuTool("crop");
    startCropTool();
  }, [activeEditMenuTool, handleCreateHighDefinitionNode, startCropTool, startInpaintingTool, startOutpaintingTool]);

  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) {
      updateMediaNodeData(id, {
        outputImage: image,
        previewImg: imageItem.previewImg,
        dimensions: imageItem.dimensions ?? null,
        selectedHistoryIndex: newIndex,
        status: "idle",
        error: null,
      }, imageItem.dimensions ?? null);
    }
  }, [id, nodeData.imageHistory, nodeData.selectedHistoryIndex, isLoadingCarouselImage, updateMediaNodeData]);

  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) {
      updateMediaNodeData(id, {
        outputImage: image,
        previewImg: imageItem.previewImg,
        dimensions: imageItem.dimensions ?? null,
        selectedHistoryIndex: newIndex,
        status: "idle",
        error: null,
      }, imageItem.dimensions ?? null);
    }
  }, [id, nodeData.imageHistory, nodeData.selectedHistoryIndex, isLoadingCarouselImage, updateMediaNodeData]);

  // 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]);

  useEffect(() => {
    if (selected) return;
    closeNodeTool();
  }, [closeNodeTool, selected]);

  const selectedNodeCount = useSelectedNodeCount();
  const editableImage = primaryImage || displayImage;
  const downloadImage = nodeData.outputImage || editableImage;
  const overlayImageDimensions = cropImageDimensions || asImageDimensions(nodeData.dimensions);
  const showSelectedActions = Boolean(selected && selectedNodeCount === 1 && editableImage);
  const isCropping = activeNodeTool === "crop";
  const isOutpainting = activeNodeTool === "outpainting";
  const isInpainting = activeNodeTool === "inpainting";
  const isSplitGrid = activeNodeTool === "splitGrid";
  const isMultiAnglePanelOpen = activeNodeTool === "multiAngle";
  const renderedImageSrc = isCropping || isOutpainting || isInpainting ? editableImage : outputImageSrc;
  const selectedActions: NodeActionCapsuleAction[] = editableImage
    ? [
        createMediaEditActionDropdown({
          label: getMediaEditMenuLabel(activeEditMenuTool),
          icon: <GenerateImageEditMenuIcon tool={activeEditMenuTool} />,
          onClick: handleEditMenuPrimaryClick,
          items: [
            {
              key: "high-definition",
              icon: <GenerateImageEditMenuIcon tool="high-definition" className="h-4 w-4" />,
              label: t("imageInput.highDefinition"),
            },
            {
              key: "outpainting",
              icon: <GenerateImageEditMenuIcon tool="outpainting" className="h-4 w-4" />,
              label: "扩图",
              disabled: !outpaintingModel,
            },
            {
              key: "inpainting",
              icon: <GenerateImageEditMenuIcon tool="inpainting" className="h-4 w-4" />,
              label: "擦除",
              disabled: !inpaintingModel,
            },
            {
              key: "crop",
              icon: <GenerateImageEditMenuIcon tool="crop" className="h-4 w-4" />,
              label: t("imageInput.crop"),
            },
          ],
          menuOnClick: ({ key, domEvent }) => {
            domEvent.preventDefault();
            domEvent.stopPropagation();
            if (key === "high-definition") {
              setActiveEditMenuTool("high-definition");
              void handleCreateHighDefinitionNode();
              return;
            }
            if (key === "outpainting") {
              setActiveEditMenuTool("outpainting");
              startOutpaintingTool();
              return;
            }
            if (key === "inpainting") {
              setActiveEditMenuTool("inpainting");
              startInpaintingTool();
              return;
            }
            if (key === "crop") {
              setActiveEditMenuTool("crop");
              startCropTool();
            }
          },
        }),
        {
          key: "split-grid",
          icon: <GridToolIcon />,
          label: t("imageInput.splitGrid"),
          text: t("imageInput.splitGrid"),
          section: "feature",
          panel: ({ close }) => (
            <SplitGridCapsuleMenu
              busy={isSplitGridRunning}
              onSelect={(selection) => {
                close();
                handleStartGridSelection(selection);
              }}
            />
          ),
        },
        {
          key: "multi-angle",
          icon: <MultiAngleToolIcon />,
          label: t("imageInput.multiAngle"),
          text: t("imageInput.multiAngle"),
          section: "feature",
          onClick: (event) => {
            event.preventDefault();
            event.stopPropagation();
            switchNodeTool(id, "multiAngle");
          },
        },
        {
          key: "save-to-assets",
          icon: <CloudUploadOutlined className="text-[24px]" />,
          label: t("assetLibrary.saveToLibrary"),
          section: "basic",
          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"),
          section: "basic",
          onClick: (event) => {
            event.preventDefault();
            event.stopPropagation();
            if (downloadImage) {
              downloadMedia(downloadImage, "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"),
          section: "basic",
          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"),
          section: "basic",
          tone: "danger",
          onClick: (event) => {
            event.preventDefault();
            event.stopPropagation();
            void handleClearImage();
          },
        },
      ]
    : [];

  return (
    <>
    <BaseNode
      id={id}
      selected={selected}
      isExecuting={isRunning}
      hasError={nodeData.status === "error"}
      fullBleed
      autoSizeContent={Boolean(displayImage)}
      dataTutorial="generate-image-node"
    >
      {renderInputHandle && (
        <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 && !activeNodeTool && <NodeActionCapsule actions={selectedActions} />}

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

      <div
        className={displayImage ? getAutoMediaFrameClassName() : "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 ((renderedImageSrc === nodeData.outputImage || renderedImageSrc === storedImage) && image.naturalWidth > 0 && image.naturalHeight > 0) {
                  const dimensions = { width: image.naturalWidth, height: image.naturalHeight };
                  setCropImageDimensions(dimensions);
                  updateMediaNodeData(id, { dimensions }, dimensions);
                }
              }}
              onError={() => {
                if (primaryPreviewImage && displayImage === primaryPreviewImage && primaryImage && primaryImage !== primaryPreviewImage) {
                  setDisplayFallbackImage(primaryImage);
                  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={getAutoMediaElementClassName(overlayImageDimensions, "cursor-zoom-in")}
            />
            {isCropping && (
              <MediaCropOverlay
                kind="image"
                mediaElementRef={imageRef}
                naturalWidth={overlayImageDimensions?.width}
                naturalHeight={overlayImageDimensions?.height}
                aspectRatio={cropAspectRatio}
                onCropChange={setCropPayload}
                onFrameChange={setCropFrameRect}
                onImageBoundsChange={setCropImageBoundsRect}
              />
            )}
            {isOutpainting && (
              <MediaOutpaintingOverlay
                mediaElementRef={imageRef}
                naturalWidth={overlayImageDimensions?.width}
                naturalHeight={overlayImageDimensions?.height}
                onChange={setOutpaintingParams}
                onFrameChange={setOutpaintingFrameRect}
                onImageBoundsChange={setOutpaintingImageBoundsRect}
              />
            )}
            {isInpainting && editableImage && (
              <ImageEditSession
                tool="inpainting"
                nodeId={id}
                imageRef={imageRef}
                imageSource={editableImage}
                dimensions={overlayImageDimensions}
                models={{
                  outpainting: outpaintingModel,
                  inpainting: inpaintingModel,
                }}
                onClose={() => {
                  closeNodeTool();
                }}
              />
            )}
            {isCropping && (
              <MediaEditToolbar
                activeTool={MEDIA_EDIT_TOOL.crop}
                tools={IMAGE_MEDIA_EDIT_TOOLS}
                frameRect={cropFrameRect}
                imageBoundsRect={cropImageBoundsRect}
                onCancel={() => {
                  closeNodeTool();
                }}
                context={{
                  value: cropAspectRatio,
                  onChange: setCropAspectRatio,
                  onConfirm: () => {
                    if (cropPayload) void handleConfirmCrop(cropPayload);
                  },
                }}
                toolComponents={{
                  [MEDIA_EDIT_TOOL.crop]: CropRatioSelect,
                }}
              />
            )}
            {isOutpainting && (
              <MediaOutpaintingToolbar
                value={outpaintingParams}
                batchSize={outpaintingBatchSize}
                frameRect={outpaintingFrameRect}
                imageBoundsRect={outpaintingImageBoundsRect}
                onBatchSizeChange={setOutpaintingBatchSize}
                onCancel={() => {
                  closeNodeTool();
                }}
                onConfirm={() => {
                  if (outpaintingParams) void handleConfirmOutpainting(outpaintingParams);
                }}
              />
            )}
            {isSplitGrid && 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 && editableImage && (
      <MediaPreviewModal
        type="image"
        src={editableImage}
        downloadSrc={editableImage}
        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 };
