"use client";

import { useMemo, useState, useCallback, useEffect, useRef } from "react";
import { createPortal } from "react-dom";
import { Node } from "@xyflow/react";
import { useWorkflowStore, saveNanoBananaDefaults, useProviderApiKeys } from "@/store/workflowStore";
import { NodeType, NanoBananaNodeData, LLMGenerateNodeData, GenerateVideoNodeData, Generate3DNodeData, GenerateAudioNodeData, EaseCurveNodeData, ConditionalSwitchNodeData, AspectRatio, Resolution, ModelType, MODEL_DISPLAY_NAMES, ProviderType, SelectedModel, LLMProvider, LLMModelType, MatchMode, ConditionalSwitchRule } from "@/types";
import { ProviderModel, ModelCapability } from "@/lib/providers/types";
import { ModelSearchDialog } from "@/components/modals/ModelSearchDialog";
import { ModelParameters } from "./ModelParameters";
import { CubicBezierEditor } from "@/components/CubicBezierEditor";
import { deduplicatedFetch } from "@/utils/deduplicatedFetch";
import { evaluateRule } from "@/store/utils/ruleEvaluation";
import { EASING_PRESETS, getPresetBezier, getEasingBezier } from "@/lib/easing-presets";
import { getAllEasingNames, getEasingFunction } from "@/lib/easing-functions";
import { getModelPageUrl, getProviderDisplayName } from "@/utils/providerUrls";
import { useInlineParameters } from "@/hooks/useInlineParameters";

// List of node types that have configurable parameters
const CONFIGURABLE_NODE_TYPES: NodeType[] = [
  "nanoBanana",
  "generateVideo",
  "generate3d",
  "generateAudio",
  "llmGenerate",
  "easeCurve",
  "conditionalSwitch",
];

// Generation node types that can use inline parameters
const GENERATION_NODE_TYPES: NodeType[] = [
  "nanoBanana",
  "generateVideo",
  "generate3d",
  "generateAudio",
  "llmGenerate",
];

// Base 10 aspect ratios (all Gemini image models)
const BASE_ASPECT_RATIOS: AspectRatio[] = ["1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "21:9"];

// Extended 14 aspect ratios (Nano Banana 2 adds extreme ratios)
const EXTENDED_ASPECT_RATIOS: AspectRatio[] = ["1:1", "1:4", "1:8", "2:3", "3:2", "3:4", "4:1", "4:3", "4:5", "5:4", "8:1", "9:16", "16:9", "21:9"];

// Resolutions per model
const RESOLUTIONS_PRO: Resolution[] = ["1K", "2K", "4K"];
const RESOLUTIONS_NB2: Resolution[] = ["512", "1K", "2K", "4K"];

// Hardcoded Gemini image models
const GEMINI_IMAGE_MODELS: { value: ModelType; label: string }[] = [
  { value: "nano-banana", label: "Nano Banana" },
  { value: "nano-banana-2", label: "Nano Banana 2" },
  { value: "nano-banana-pro", label: "Nano Banana Pro" },
];

// LLM providers and models
const LLM_PROVIDERS: { value: LLMProvider; label: string }[] = [
  { value: "google", label: "Google" },
  { value: "openai", label: "OpenAI" },
  { value: "anthropic", label: "Anthropic" },
];

const LLM_MODELS: Record<LLMProvider, { value: LLMModelType; label: string }[]> = {
  google: [
    { value: "gemini-3-flash-preview", label: "Gemini 3 Flash" },
    { value: "gemini-2.5-flash", label: "Gemini 2.5 Flash" },
    { value: "gemini-3-pro-preview", label: "Gemini 3.0 Pro" },
    { value: "gemini-3.1-pro-preview", label: "Gemini 3.1 Pro" },
  ],
  openai: [
    { value: "gpt-4.1-mini", label: "GPT-4.1 Mini" },
    { value: "gpt-4.1-nano", label: "GPT-4.1 Nano" },
  ],
  anthropic: [
    { value: "claude-sonnet-4.5", label: "Claude Sonnet 4.5" },
    { value: "claude-haiku-4.5", label: "Claude Haiku 4.5" },
    { value: "claude-opus-4.6", label: "Claude Opus 4.6" },
  ],
};

// Image/video/audio/3d generation capabilities
const IMAGE_CAPABILITIES: ModelCapability[] = ["text-to-image", "image-to-image"];
const VIDEO_CAPABILITIES: ModelCapability[] = ["text-to-video", "image-to-video"];
const AUDIO_CAPABILITIES: ModelCapability[] = ["text-to-audio"];
const MODEL_3D_CAPABILITIES: ModelCapability[] = ["text-to-3d", "image-to-3d"];

// Easing names
const ALL_EASING_NAMES = getAllEasingNames();
const PRESET_NAMES = new Set(EASING_PRESETS);

// Generate SVG polyline for easing preview
function generateEasingPolyline(
  easingName: string,
  width: number,
  height: number,
  samples: number = 20
): string {
  const fn = getEasingFunction(easingName);
  return Array.from({ length: samples + 1 }, (_, i) => {
    const t = i / samples;
    const y = fn(t);
    return `${(t * width).toFixed(1)},${((1 - y) * height).toFixed(1)}`;
  }).join(" ");
}

/**
 * Fixed-position control panel on the right side of viewport
 * Displays controls for the currently selected node
 */
export function ControlPanel() {
  const selectedNode = useWorkflowStore((state) => {
    const selected = state.nodes.filter((n) => n.selected);
    return selected.length === 1 ? selected[0] : null;
  });
  const { inlineParametersEnabled } = useInlineParameters();

  // Check if the selected node is configurable
  const isConfigurable = selectedNode && CONFIGURABLE_NODE_TYPES.includes(selectedNode.type as NodeType);

  // If no single node selected or not configurable, hide panel
  if (!selectedNode || !isConfigurable) {
    return null;
  }

  // Check if this is a generation node
  const isGenerationNode = selectedNode &&
    GENERATION_NODE_TYPES.includes(selectedNode.type as NodeType);

  // Hide for generation nodes when inline parameters enabled
  if (isGenerationNode && inlineParametersEnabled) {
    return null;
  }

  return (
    <div className="fixed top-0 right-6 h-screen z-[90] flex items-center pointer-events-none">
      <div
        className="w-80 bg-neutral-800 border border-neutral-700 rounded-xl max-h-[80vh] overflow-y-auto pointer-events-auto transition-opacity duration-200 nowheel"
        style={{
          boxShadow: [
            '-1px 0 2px rgba(0,0,0,0.18)',
            '-2px 0 4px rgba(0,0,0,0.15)',
            '-4px 0 8px rgba(0,0,0,0.12)',
            '-8px 0 16px rgba(0,0,0,0.10)',
            '-16px 0 32px rgba(0,0,0,0.08)',
            '-32px 0 64px rgba(0,0,0,0.06)',
          ].join(', '),
        }}
      >
        <div className="p-4">
          {/* Header */}
          <h3 className="text-sm font-medium text-neutral-200">
            {getNodeTypeTitle(selectedNode.type as NodeType)}
          </h3>

          {/* Node-specific controls */}
          <div className="space-y-4 mt-4">
            {selectedNode.type === "nanoBanana" && (
              <GenerateImageControls node={selectedNode} />
            )}
            {selectedNode.type === "generateVideo" && (
              <GenerateVideoControls node={selectedNode} />
            )}
            {selectedNode.type === "generate3d" && (
              <Generate3DControls node={selectedNode} />
            )}
            {selectedNode.type === "generateAudio" && (
              <GenerateAudioControls node={selectedNode} />
            )}
            {selectedNode.type === "llmGenerate" && (
              <LLMControls node={selectedNode} />
            )}
            {selectedNode.type === "easeCurve" && (
              <EaseCurveControls node={selectedNode} />
            )}
            {selectedNode.type === "conditionalSwitch" && (
              <ConditionalSwitchControls node={selectedNode} />
            )}
          </div>
        </div>
      </div>
    </div>
  );
}

function getNodeTypeTitle(type: NodeType): string {
  const titles: Record<string, string> = {
    nanoBanana: "Generate Image Settings",
    generateVideo: "Generate Video Settings",
    generate3d: "Generate 3D Settings",
    generateAudio: "Generate Audio Settings",
    llmGenerate: "LLM Settings",
    easeCurve: "Ease Curve Settings",
    conditionalSwitch: "Conditional Switch Settings",
  };
  return titles[type] || "Settings";
}

// Generate Image Controls
function GenerateImageControls({ node }: { node: Node }) {
  const nodeData = node.data as NanoBananaNodeData;
  const updateNodeData = useWorkflowStore((state) => state.updateNodeData);
  const regenerateNode = useWorkflowStore((state) => state.regenerateNode);
  const isRunning = useWorkflowStore((state) => state.isRunning);
  const { replicateApiKey, falApiKey, kieApiKey, replicateEnabled, kieEnabled } = useProviderApiKeys();
  const [externalModels, setExternalModels] = useState<ProviderModel[]>([]);
  const [isLoadingModels, setIsLoadingModels] = useState(false);
  const [modelsFetchError, setModelsFetchError] = useState<string | null>(null);
  const [isBrowseDialogOpen, setIsBrowseDialogOpen] = useState(false);

  const currentProvider: ProviderType = nodeData.selectedModel?.provider || "gemini";

  // Get enabled providers
  const enabledProviders = useMemo(() => {
    const providers: { id: ProviderType; name: string }[] = [];
    providers.push({ id: "gemini", name: "Gemini" });
    providers.push({ id: "fal", name: "fal.ai" });
    if (replicateEnabled && replicateApiKey) {
      providers.push({ id: "replicate", name: "Replicate" });
    }
    if (kieEnabled && kieApiKey) {
      providers.push({ id: "kie", name: "Kie.ai" });
    }
    return providers;
  }, [replicateEnabled, replicateApiKey, kieEnabled, kieApiKey]);

  // Fetch models from external providers
  const fetchModels = useCallback(async () => {
    if (currentProvider === "gemini") {
      setExternalModels([]);
      setModelsFetchError(null);
      return;
    }

    setIsLoadingModels(true);
    setModelsFetchError(null);
    try {
      const capabilities = IMAGE_CAPABILITIES.join(",");
      const headers: HeadersInit = {};
      if (replicateApiKey) headers["X-Replicate-Key"] = replicateApiKey;
      if (falApiKey) headers["X-Fal-Key"] = falApiKey;
      if (kieApiKey) headers["X-Kie-Key"] = kieApiKey;

      const response = await deduplicatedFetch(`/api/models?provider=${currentProvider}&capabilities=${capabilities}`, { headers });
      if (response.ok) {
        const data = await response.json();
        setExternalModels(data.models || []);
        setModelsFetchError(null);
      } else {
        const errorData = await response.json().catch(() => ({}));
        const errorMsg = errorData.error || `Failed to load models (${response.status})`;
        setExternalModels([]);
        setModelsFetchError(errorMsg);
      }
    } catch (error) {
      console.error("Failed to fetch models:", error);
      setExternalModels([]);
      setModelsFetchError("Failed to load models. Check your connection.");
    } finally {
      setIsLoadingModels(false);
    }
  }, [currentProvider, replicateApiKey, falApiKey, kieApiKey]);

  useEffect(() => {
    fetchModels();
  }, [fetchModels]);

  const handleProviderChange = useCallback(
    (e: React.ChangeEvent<HTMLSelectElement>) => {
      const provider = e.target.value as ProviderType;

      if (provider === "gemini") {
        const newSelectedModel: SelectedModel = {
          provider: "gemini",
          modelId: nodeData.model || "nano-banana-pro",
          displayName: GEMINI_IMAGE_MODELS.find(m => m.value === (nodeData.model || "nano-banana-pro"))?.label || "Nano Banana Pro",
        };
        updateNodeData(node.id, { selectedModel: newSelectedModel, parameters: {} });
      } else {
        const newSelectedModel: SelectedModel = {
          provider,
          modelId: "",
          displayName: "Select model...",
        };
        updateNodeData(node.id, { selectedModel: newSelectedModel, parameters: {} });
      }
    },
    [node.id, nodeData.model, updateNodeData]
  );

  const handleExternalModelChange = useCallback(
    (e: React.ChangeEvent<HTMLSelectElement>) => {
      const modelId = e.target.value;
      const model = externalModels.find(m => m.id === modelId);
      if (model) {
        const newSelectedModel: SelectedModel = {
          provider: currentProvider,
          modelId: model.id,
          displayName: model.name,
          capabilities: model.capabilities,
        };
        updateNodeData(node.id, { selectedModel: newSelectedModel, parameters: {} });
      }
    },
    [node.id, currentProvider, externalModels, updateNodeData]
  );

  const handleModelChange = useCallback(
    (e: React.ChangeEvent<HTMLSelectElement>) => {
      const model = e.target.value as ModelType;
      updateNodeData(node.id, { model });
      saveNanoBananaDefaults({ model });

      const newSelectedModel: SelectedModel = {
        provider: "gemini",
        modelId: model,
        displayName: GEMINI_IMAGE_MODELS.find(m => m.value === model)?.label || model,
      };
      updateNodeData(node.id, { selectedModel: newSelectedModel });
    },
    [node.id, updateNodeData]
  );

  const handleAspectRatioChange = useCallback(
    (e: React.ChangeEvent<HTMLSelectElement>) => {
      const aspectRatio = e.target.value as AspectRatio;
      updateNodeData(node.id, { aspectRatio });
      saveNanoBananaDefaults({ aspectRatio });
    },
    [node.id, updateNodeData]
  );

  const handleResolutionChange = useCallback(
    (e: React.ChangeEvent<HTMLSelectElement>) => {
      const resolution = e.target.value as Resolution;
      updateNodeData(node.id, { resolution });
      saveNanoBananaDefaults({ resolution });
    },
    [node.id, updateNodeData]
  );

  const handleGoogleSearchToggle = useCallback(
    (e: React.ChangeEvent<HTMLInputElement>) => {
      const useGoogleSearch = e.target.checked;
      updateNodeData(node.id, { useGoogleSearch });
      saveNanoBananaDefaults({ useGoogleSearch });
    },
    [node.id, updateNodeData]
  );

  const handleImageSearchToggle = useCallback(
    (e: React.ChangeEvent<HTMLInputElement>) => {
      const useImageSearch = e.target.checked;
      updateNodeData(node.id, { useImageSearch });
      saveNanoBananaDefaults({ useImageSearch });
    },
    [node.id, updateNodeData]
  );

  const handleParametersChange = useCallback(
    (parameters: Record<string, unknown>) => {
      updateNodeData(node.id, { parameters });
    },
    [node.id, updateNodeData]
  );

  const handleBrowseModelSelect = useCallback((model: ProviderModel) => {
    const newSelectedModel: SelectedModel = {
      provider: model.provider,
      modelId: model.id,
      displayName: model.name,
      capabilities: model.capabilities,
    };
    updateNodeData(node.id, { selectedModel: newSelectedModel, parameters: {} });
    setIsBrowseDialogOpen(false);
  }, [node.id, updateNodeData]);

  const isGeminiProvider = currentProvider === "gemini";
  const currentModelId = isGeminiProvider ? (nodeData.selectedModel?.modelId || nodeData.model) : null;
  const supportsResolution = currentModelId === "nano-banana-pro" || currentModelId === "nano-banana-2";
  const aspectRatios = currentModelId === "nano-banana-2" ? EXTENDED_ASPECT_RATIOS : BASE_ASPECT_RATIOS;
  const resolutions = currentModelId === "nano-banana-2" ? RESOLUTIONS_NB2 : RESOLUTIONS_PRO;
  const hasExternalProviders = !!(replicateEnabled && replicateApiKey) || true;

  return (
    <>
      <div className="space-y-3">
        {/* Model name + provider with link — sits directly under title divider */}
        <div className="border-t border-neutral-700 pt-3">
          <div className="flex items-start gap-2">
            <div className="flex-1 min-w-0">
              <div className="text-sm text-neutral-100 truncate">
                {nodeData.selectedModel?.displayName || GEMINI_IMAGE_MODELS.find(m => m.value === nodeData.model)?.label || "Select model..."}
              </div>
              <div className="flex items-center gap-1 mt-0.5">
                <span className="text-[10px] text-neutral-500 truncate">
                  {enabledProviders.find(p => p.id === currentProvider)?.name || currentProvider}
                </span>
                {nodeData.selectedModel?.modelId && (
                  <a
                    href={getModelPageUrl(currentProvider, nodeData.selectedModel.modelId) || "#"}
                    target="_blank"
                    rel="noopener noreferrer"
                    className="text-neutral-500 hover:text-neutral-300 transition-colors"
                    title={`View on ${getProviderDisplayName(currentProvider)}`}
                    onClick={(e) => {
                      if (!getModelPageUrl(currentProvider, nodeData.selectedModel?.modelId || "")) {
                        e.preventDefault();
                      }
                    }}
                  >
                    <svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                      <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
                    </svg>
                  </a>
                )}
              </div>
            </div>
            <button
              onClick={() => setIsBrowseDialogOpen(true)}
              className="nodrag nopan shrink-0 px-3 py-1.5 text-xs bg-neutral-700 hover:bg-neutral-600 border border-neutral-600 rounded text-neutral-300 transition-colors"
            >
              Browse
            </button>
          </div>
        </div>

        {/* Gemini-specific controls */}
        {isGeminiProvider && (
          <>
            <div>
              <label className="block text-xs font-medium text-neutral-300 mb-1">Aspect Ratio</label>
              <select
                value={nodeData.aspectRatio || "1:1"}
                onChange={handleAspectRatioChange}
                className="nodrag nopan w-full px-2 py-1 text-xs bg-neutral-700 border border-neutral-600 rounded text-neutral-200 focus:outline-none focus:ring-1 focus:ring-blue-500"
              >
                {aspectRatios.map(ar => (
                  <option key={ar} value={ar}>{ar}</option>
                ))}
              </select>
            </div>

            {supportsResolution && (
              <div>
                <label className="block text-xs font-medium text-neutral-300 mb-1">Resolution</label>
                <select
                  value={nodeData.resolution || "1K"}
                  onChange={handleResolutionChange}
                  className="nodrag nopan w-full px-2 py-1 text-xs bg-neutral-700 border border-neutral-600 rounded text-neutral-200 focus:outline-none focus:ring-1 focus:ring-blue-500"
                >
                  {resolutions.map(res => (
                    <option key={res} value={res}>{res}</option>
                  ))}
                </select>
              </div>
            )}

            {(currentModelId === "nano-banana-pro" || currentModelId === "nano-banana-2") && (
              <div className="flex items-center">
                <input
                  type="checkbox"
                  id={`google-search-${node.id}`}
                  checked={nodeData.useGoogleSearch || false}
                  onChange={handleGoogleSearchToggle}
                  className="nodrag nopan w-3 h-3 text-blue-600 bg-neutral-700 border-neutral-600 rounded focus:ring-blue-500"
                />
                <label htmlFor={`google-search-${node.id}`} className="ml-2 text-xs text-neutral-300">
                  Google Search
                </label>
              </div>
            )}

            {currentModelId === "nano-banana-2" && (
              <div className="flex items-center">
                <input
                  type="checkbox"
                  id={`image-search-${node.id}`}
                  checked={nodeData.useImageSearch || false}
                  onChange={handleImageSearchToggle}
                  className="nodrag nopan w-3 h-3 text-blue-600 bg-neutral-700 border-neutral-600 rounded focus:ring-blue-500"
                />
                <label htmlFor={`image-search-${node.id}`} className="ml-2 text-xs text-neutral-300">
                  Image Search
                </label>
              </div>
            )}
          </>
        )}

        {/* External provider parameters */}
        {!isGeminiProvider && nodeData.selectedModel?.modelId && (
          <ModelParameters
            modelId={nodeData.selectedModel.modelId}
            provider={currentProvider}
            parameters={nodeData.parameters || {}}
            onParametersChange={handleParametersChange}
          />
        )}
      </div>

      <div className="flex justify-end">
        <button
          onClick={() => regenerateNode(node.id)}
          disabled={isRunning}
          className="nodrag nopan inline-flex items-center gap-1.5 px-3 py-1.5 text-xs bg-neutral-700 hover:bg-neutral-600 border border-neutral-600 rounded text-neutral-300 disabled:opacity-40 disabled:pointer-events-none transition-colors"
        >
          <svg className="w-3 h-3" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z" /></svg>
          {isRunning ? "Running..." : "Run"}
        </button>
      </div>

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

// Generate Video Controls
function GenerateVideoControls({ node }: { node: Node }) {
  const nodeData = node.data as GenerateVideoNodeData;
  const updateNodeData = useWorkflowStore((state) => state.updateNodeData);
  const regenerateNode = useWorkflowStore((state) => state.regenerateNode);
  const isRunning = useWorkflowStore((state) => state.isRunning);
  const [isBrowseDialogOpen, setIsBrowseDialogOpen] = useState(false);

  const currentProvider: ProviderType = nodeData.selectedModel?.provider || "fal";

  const handleParametersChange = useCallback(
    (parameters: Record<string, unknown>) => {
      updateNodeData(node.id, { parameters });
    },
    [node.id, updateNodeData]
  );

  const handleBrowseModelSelect = useCallback((model: ProviderModel) => {
    const newSelectedModel: SelectedModel = {
      provider: model.provider,
      modelId: model.id,
      displayName: model.name,
      capabilities: model.capabilities,
    };
    updateNodeData(node.id, { selectedModel: newSelectedModel, parameters: {} });
    setIsBrowseDialogOpen(false);
  }, [node.id, updateNodeData]);

  return (
    <>
      <div className="space-y-3">
        {/* Model name + provider with link */}
        <div className="border-t border-neutral-700 pt-3">
          <div className="flex items-start gap-2">
            <div className="flex-1 min-w-0">
              <div className="text-sm text-neutral-100 truncate">
                {nodeData.selectedModel?.displayName || "Select model..."}
              </div>
              <div className="flex items-center gap-1 mt-0.5">
                <span className="text-[10px] text-neutral-500 truncate">
                  {getProviderDisplayName(currentProvider)}
                </span>
                {nodeData.selectedModel?.modelId && (
                  <a
                    href={getModelPageUrl(currentProvider, nodeData.selectedModel.modelId) || "#"}
                    target="_blank"
                    rel="noopener noreferrer"
                    className="text-neutral-500 hover:text-neutral-300 transition-colors"
                    title={`View on ${getProviderDisplayName(currentProvider)}`}
                    onClick={(e) => {
                      if (!getModelPageUrl(currentProvider, nodeData.selectedModel?.modelId || "")) {
                        e.preventDefault();
                      }
                    }}
                  >
                    <svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                      <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
                    </svg>
                  </a>
                )}
              </div>
            </div>
            <button
              onClick={() => setIsBrowseDialogOpen(true)}
              className="nodrag nopan shrink-0 px-3 py-1.5 text-xs bg-neutral-700 hover:bg-neutral-600 border border-neutral-600 rounded text-neutral-300 transition-colors"
            >
              Browse
            </button>
          </div>
        </div>

        {nodeData.selectedModel?.modelId && (
          <ModelParameters
            modelId={nodeData.selectedModel.modelId}
            provider={currentProvider}
            parameters={nodeData.parameters || {}}
            onParametersChange={handleParametersChange}
          />
        )}
      </div>

      <div className="flex justify-end">
        <button
          onClick={() => regenerateNode(node.id)}
          disabled={isRunning}
          className="nodrag nopan inline-flex items-center gap-1.5 px-3 py-1.5 text-xs bg-neutral-700 hover:bg-neutral-600 border border-neutral-600 rounded text-neutral-300 disabled:opacity-40 disabled:pointer-events-none transition-colors"
        >
          <svg className="w-3 h-3" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z" /></svg>
          {isRunning ? "Running..." : "Run"}
        </button>
      </div>

      {isBrowseDialogOpen && (
        <ModelSearchDialog
          isOpen={isBrowseDialogOpen}
          onClose={() => setIsBrowseDialogOpen(false)}
          onModelSelected={handleBrowseModelSelect}
          initialCapabilityFilter="video"
        />
      )}
    </>
  );
}

// Generate 3D Controls
function Generate3DControls({ node }: { node: Node }) {
  const nodeData = node.data as Generate3DNodeData;
  const updateNodeData = useWorkflowStore((state) => state.updateNodeData);
  const regenerateNode = useWorkflowStore((state) => state.regenerateNode);
  const isRunning = useWorkflowStore((state) => state.isRunning);
  const [isBrowseDialogOpen, setIsBrowseDialogOpen] = useState(false);

  const currentProvider: ProviderType = nodeData.selectedModel?.provider || "fal";

  const handleParametersChange = useCallback(
    (parameters: Record<string, unknown>) => {
      updateNodeData(node.id, { parameters });
    },
    [node.id, updateNodeData]
  );

  const handleBrowseModelSelect = useCallback((model: ProviderModel) => {
    updateNodeData(node.id, {
      selectedModel: {
        provider: model.provider,
        modelId: model.id,
        displayName: model.name,
        capabilities: model.capabilities,
      },
      parameters: {}
    });
    setIsBrowseDialogOpen(false);
  }, [node.id, updateNodeData]);

  return (
    <>
      <div className="space-y-3">
        {/* Model name + provider with link */}
        <div className="border-t border-neutral-700 pt-3">
          <div className="flex items-start gap-2">
            <div className="flex-1 min-w-0">
              <div className="text-sm text-neutral-100 truncate">
                {nodeData.selectedModel?.displayName || "Select model..."}
              </div>
              <div className="flex items-center gap-1 mt-0.5">
                <span className="text-[10px] text-neutral-500 truncate">
                  {getProviderDisplayName(currentProvider)}
                </span>
                {nodeData.selectedModel?.modelId && (
                  <a
                    href={getModelPageUrl(currentProvider, nodeData.selectedModel.modelId) || "#"}
                    target="_blank"
                    rel="noopener noreferrer"
                    className="text-neutral-500 hover:text-neutral-300 transition-colors"
                    title={`View on ${getProviderDisplayName(currentProvider)}`}
                    onClick={(e) => {
                      if (!getModelPageUrl(currentProvider, nodeData.selectedModel?.modelId || "")) {
                        e.preventDefault();
                      }
                    }}
                  >
                    <svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                      <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
                    </svg>
                  </a>
                )}
              </div>
            </div>
            <button
              onClick={() => setIsBrowseDialogOpen(true)}
              className="nodrag nopan shrink-0 px-3 py-1.5 text-xs bg-neutral-700 hover:bg-neutral-600 border border-neutral-600 rounded text-neutral-300 transition-colors"
            >
              Browse
            </button>
          </div>
        </div>

        {nodeData.selectedModel?.modelId && (
          <ModelParameters
            modelId={nodeData.selectedModel.modelId}
            provider={currentProvider}
            parameters={nodeData.parameters || {}}
            onParametersChange={handleParametersChange}
          />
        )}
      </div>

      <div className="flex justify-end">
        <button
          onClick={() => regenerateNode(node.id)}
          disabled={isRunning}
          className="nodrag nopan inline-flex items-center gap-1.5 px-3 py-1.5 text-xs bg-neutral-700 hover:bg-neutral-600 border border-neutral-600 rounded text-neutral-300 disabled:opacity-40 disabled:pointer-events-none transition-colors"
        >
          <svg className="w-3 h-3" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z" /></svg>
          {isRunning ? "Running..." : "Run"}
        </button>
      </div>

      {isBrowseDialogOpen && (
        <ModelSearchDialog
          isOpen={isBrowseDialogOpen}
          onClose={() => setIsBrowseDialogOpen(false)}
          onModelSelected={handleBrowseModelSelect}
          initialCapabilityFilter="3d"
        />
      )}
    </>
  );
}

// Generate Audio Controls
function GenerateAudioControls({ node }: { node: Node }) {
  const regenerateNode = useWorkflowStore((state) => state.regenerateNode);
  const isRunning = useWorkflowStore((state) => state.isRunning);

  return (
    <div className="space-y-3">
      <div className="text-xs text-neutral-400">
        Audio generation settings
      </div>
      <div className="flex justify-end">
        <button
          onClick={() => regenerateNode(node.id)}
          disabled={isRunning}
          className="nodrag nopan inline-flex items-center gap-1.5 px-3 py-1.5 text-xs bg-neutral-700 hover:bg-neutral-600 border border-neutral-600 rounded text-neutral-300 disabled:opacity-40 disabled:pointer-events-none transition-colors"
        >
          <svg className="w-3 h-3" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z" /></svg>
          {isRunning ? "Running..." : "Run"}
        </button>
      </div>
    </div>
  );
}

// LLM Controls
function LLMControls({ node }: { node: Node }) {
  const nodeData = node.data as LLMGenerateNodeData;
  const updateNodeData = useWorkflowStore((state) => state.updateNodeData);
  const regenerateNode = useWorkflowStore((state) => state.regenerateNode);
  const isRunning = useWorkflowStore((state) => state.isRunning);

  const handleProviderChange = useCallback(
    (e: React.ChangeEvent<HTMLSelectElement>) => {
      const newProvider = e.target.value as LLMProvider;
      const firstModelForProvider = LLM_MODELS[newProvider][0].value;
      const updates: Partial<LLMGenerateNodeData> = {
        provider: newProvider,
        model: firstModelForProvider,
      };
      if (newProvider === "anthropic" && nodeData.temperature > 1) {
        updates.temperature = 1;
      }
      updateNodeData(node.id, updates);
    },
    [node.id, updateNodeData, nodeData.temperature]
  );

  const handleModelChange = useCallback(
    (e: React.ChangeEvent<HTMLSelectElement>) => {
      updateNodeData(node.id, { model: e.target.value as LLMModelType });
    },
    [node.id, updateNodeData]
  );

  const handleTemperatureChange = useCallback(
    (e: React.ChangeEvent<HTMLInputElement>) => {
      updateNodeData(node.id, { temperature: parseFloat(e.target.value) });
    },
    [node.id, updateNodeData]
  );

  const handleMaxTokensChange = useCallback(
    (e: React.ChangeEvent<HTMLInputElement>) => {
      updateNodeData(node.id, { maxTokens: parseInt(e.target.value, 10) });
    },
    [node.id, updateNodeData]
  );

  const provider = nodeData.provider || "google";
  const availableModels = LLM_MODELS[provider] || LLM_MODELS.google;

  return (
    <div className="space-y-3">
      <div>
        <label className="block text-xs font-medium text-neutral-300 mb-1">Provider</label>
        <select
          value={provider}
          onChange={handleProviderChange}
          className="nodrag nopan w-full px-2 py-1 text-xs bg-neutral-700 border border-neutral-600 rounded text-neutral-200 focus:outline-none focus:ring-1 focus:ring-blue-500"
        >
          {LLM_PROVIDERS.map(p => (
            <option key={p.value} value={p.value}>{p.label}</option>
          ))}
        </select>
      </div>

      <div>
        <label className="block text-xs font-medium text-neutral-300 mb-1">Model</label>
        <select
          value={nodeData.model || availableModels[0].value}
          onChange={handleModelChange}
          className="nodrag nopan w-full px-2 py-1 text-xs bg-neutral-700 border border-neutral-600 rounded text-neutral-200 focus:outline-none focus:ring-1 focus:ring-blue-500"
        >
          {availableModels.map(m => (
            <option key={m.value} value={m.value}>{m.label}</option>
          ))}
        </select>
      </div>

      <div>
        <label className="block text-xs font-medium text-neutral-300 mb-1">
          Temperature: {nodeData.temperature?.toFixed(2) || "0.70"}
        </label>
        <input
          type="range"
          min="0"
          max={provider === "anthropic" ? "1" : "2"}
          step="0.01"
          value={nodeData.temperature || 0.7}
          onChange={handleTemperatureChange}
          className="nodrag nopan w-full h-1 bg-neutral-700 rounded-lg appearance-none cursor-pointer accent-blue-500"
        />
      </div>

      <div>
        <label className="block text-xs font-medium text-neutral-300 mb-1">
          Max Tokens: {(nodeData.maxTokens || 2048).toLocaleString()}
        </label>
        <input
          type="range"
          min="256"
          max="16384"
          step="256"
          value={nodeData.maxTokens || 2048}
          onChange={handleMaxTokensChange}
          className="nodrag nopan w-full h-1 bg-neutral-700 rounded-lg appearance-none cursor-pointer accent-blue-500"
        />
      </div>

      <div className="flex justify-end">
        <button
          onClick={() => regenerateNode(node.id)}
          disabled={isRunning}
          className="nodrag nopan inline-flex items-center gap-1.5 px-3 py-1.5 text-xs bg-neutral-700 hover:bg-neutral-600 border border-neutral-600 rounded text-neutral-300 disabled:opacity-40 disabled:pointer-events-none transition-colors"
        >
          <svg className="w-3 h-3" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z" /></svg>
          {isRunning ? "Running..." : "Run"}
        </button>
      </div>
    </div>
  );
}

// Ease Curve Controls
function EaseCurveControls({ node }: { node: Node }) {
  const nodeData = node.data as EaseCurveNodeData;
  const updateNodeData = useWorkflowStore((state) => state.updateNodeData);
  const regenerateNode = useWorkflowStore((state) => state.regenerateNode);
  const isRunning = useWorkflowStore((state) => state.isRunning);
  const edges = useWorkflowStore((state) => state.edges);
  const removeEdge = useWorkflowStore((state) => state.removeEdge);
  const [showPresets, setShowPresets] = useState(false);
  const presetsButtonRef = useRef<HTMLButtonElement>(null);

  const inheritedEdge = useMemo(() => {
    return edges.find((e) => e.target === node.id && e.targetHandle === "easeCurve") || null;
  }, [edges, node.id]);

  const isInherited = !!inheritedEdge;

  const handleBreakInheritance = useCallback(() => {
    if (inheritedEdge) {
      removeEdge(inheritedEdge.id);
      updateNodeData(node.id, { inheritedFrom: null });
    }
  }, [inheritedEdge, removeEdge, node.id, updateNodeData]);

  const handleBezierChange = useCallback(
    (value: [number, number, number, number]) => {
      updateNodeData(node.id, { bezierHandles: value, easingPreset: null });
    },
    [node.id, updateNodeData]
  );

  const handleSelectEasing = useCallback(
    (name: string) => {
      updateNodeData(node.id, {
        easingPreset: name,
        bezierHandles: getEasingBezier(name),
      });
      setShowPresets(false);
    },
    [node.id, updateNodeData]
  );

  const handleDurationChange = useCallback(
    (e: React.ChangeEvent<HTMLInputElement>) => {
      const val = parseFloat(e.target.value);
      updateNodeData(node.id, { outputDuration: isNaN(val) ? 1.5 : Math.max(0.1, Math.min(30, val)) });
    },
    [node.id, updateNodeData]
  );

  const editorEasingCurve = useMemo(() => {
    if (!nodeData.easingPreset) return undefined;
    return generateEasingPolyline(nodeData.easingPreset, 100, 100, 50);
  }, [nodeData.easingPreset]);

  const presetThumbnails = useMemo(() => {
    return ALL_EASING_NAMES.map((name) => ({
      name,
      polyline: generateEasingPolyline(name, 36, 36),
      isPreset: PRESET_NAMES.has(name as any),
    }));
  }, []);

  return (
    <div className="space-y-3 relative">
      {isInherited && (
        <div className="absolute inset-0 flex flex-col items-center justify-center bg-neutral-900/80 backdrop-blur-sm rounded z-10">
          <p className="text-sm text-neutral-200 font-medium">Settings inherited</p>
          <p className="text-[11px] text-neutral-400 mt-1">Break connection to edit manually</p>
          <button
            className="nodrag nopan mt-3 px-3 py-1.5 bg-white/10 hover:bg-white/20 border border-white/20 rounded text-xs text-neutral-200 transition-colors"
            onClick={handleBreakInheritance}
          >
            Control manually
          </button>
        </div>
      )}
      <div>
        <div className="flex items-center justify-between mb-1">
          <label className="block text-xs font-medium text-neutral-300">Easing Function</label>
          <button
            ref={presetsButtonRef}
            onClick={() => setShowPresets(!showPresets)}
            className="nodrag nopan text-xs px-2 py-0.5 bg-neutral-700 hover:bg-neutral-600 border border-neutral-600 rounded text-neutral-300 transition-colors"
          >
            Presets
          </button>
        </div>
        <CubicBezierEditor
          value={nodeData.bezierHandles || [0.42, 0, 0.58, 1]}
          onChange={handleBezierChange}
          onCommit={handleBezierChange}
          easingCurve={editorEasingCurve}
        />
        {nodeData.easingPreset && (
          <div className="text-xs text-neutral-400 mt-1">
            Current: {nodeData.easingPreset}
          </div>
        )}
      </div>

      <div>
        <label className="block text-xs font-medium text-neutral-300 mb-1">
          Output Duration: {nodeData.outputDuration?.toFixed(1) || "1.5"}s
        </label>
        <input
          type="number"
          min="0.1"
          max="30"
          step="0.1"
          value={nodeData.outputDuration || 1.5}
          onChange={handleDurationChange}
          className="nodrag nopan w-full px-2 py-1 text-xs bg-neutral-700 border border-neutral-600 rounded text-neutral-200 focus:outline-none focus:ring-1 focus:ring-blue-500"
        />
      </div>

      <div className="flex justify-end">
        <button
          onClick={() => regenerateNode(node.id)}
          disabled={isRunning}
          className="nodrag nopan inline-flex items-center gap-1.5 px-3 py-1.5 text-xs bg-neutral-700 hover:bg-neutral-600 border border-neutral-600 rounded text-neutral-300 disabled:opacity-40 disabled:pointer-events-none transition-colors"
        >
          <svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" /></svg>
          {isRunning ? "Applying..." : "Apply"}
        </button>
      </div>

      {showPresets && typeof document !== 'undefined' && createPortal(
        <div
          className="fixed z-[100] bg-neutral-800 border border-neutral-600 rounded-lg shadow-xl p-2 max-h-[60vh] overflow-y-auto nowheel"
          style={{
            top: presetsButtonRef.current?.getBoundingClientRect().bottom || 0,
            right: window.innerWidth - (presetsButtonRef.current?.getBoundingClientRect().left || 0),
            width: 280,
          }}
        >
          <div className="grid grid-cols-4 gap-1">
            {presetThumbnails.map(({ name, polyline }) => (
              <button
                key={name}
                onClick={() => handleSelectEasing(name)}
                className="nodrag nopan p-1 bg-neutral-900 hover:bg-neutral-700 rounded flex flex-col items-center gap-1 transition-colors"
                title={name}
              >
                <svg width="36" height="36" viewBox="0 0 36 36" className="flex-shrink-0">
                  <polyline
                    points={polyline}
                    fill="none"
                    stroke="#a3a3a3"
                    strokeWidth="1.5"
                  />
                </svg>
                <span className="text-[8px] text-neutral-400 text-center break-words w-full">
                  {name}
                </span>
              </button>
            ))}
          </div>
        </div>,
        document.body
      )}
    </div>
  );
}

// Conditional Switch Controls
function ConditionalSwitchControls({ node }: { node: Node }) {
  const nodeData = node.data as ConditionalSwitchNodeData;
  const updateNodeData = useWorkflowStore((state) => state.updateNodeData);
  const [editingId, setEditingId] = useState<string | null>(null);

  const handleRuleValueChange = useCallback(
    (ruleId: string, newValue: string) => {
      const updatedRules = nodeData.rules.map((rule) =>
        rule.id === ruleId ? { ...rule, value: newValue } : rule
      );
      updateNodeData(node.id, { rules: updatedRules, evaluationPaused: false });
    },
    [node.id, nodeData.rules, updateNodeData]
  );

  const handleModeChange = useCallback(
    (ruleId: string, newMode: MatchMode) => {
      const updatedRules = nodeData.rules.map((rule) =>
        rule.id === ruleId ? { ...rule, mode: newMode } : rule
      );
      updateNodeData(node.id, { rules: updatedRules, evaluationPaused: false });
    },
    [node.id, nodeData.rules, updateNodeData]
  );

  const handleLabelEdit = useCallback(
    (ruleId: string, newLabel: string) => {
      const updatedRules = nodeData.rules.map((rule) =>
        rule.id === ruleId ? { ...rule, label: newLabel } : rule
      );
      updateNodeData(node.id, { rules: updatedRules });
      setEditingId(null);
    },
    [node.id, nodeData.rules, updateNodeData]
  );

  const handleDelete = useCallback(
    (ruleId: string) => {
      if (nodeData.rules.length <= 1) return;
      const updatedRules = nodeData.rules.filter((rule) => rule.id !== ruleId);
      updateNodeData(node.id, { rules: updatedRules });
    },
    [node.id, nodeData.rules, updateNodeData]
  );

  const handleAddRule = useCallback(() => {
    const newRule: ConditionalSwitchRule = {
      id: "rule-" + Math.random().toString(36).slice(2, 9),
      value: "",
      mode: "contains",
      label: `Rule ${nodeData.rules.length + 1}`,
      isMatched: false,
    };
    updateNodeData(node.id, { rules: [...nodeData.rules, newRule] });
  }, [node.id, nodeData.rules, updateNodeData]);

  return (
    <div className="space-y-2">
      {nodeData.rules.map((rule, index) => (
        <div key={rule.id} className="border border-neutral-600 rounded p-2 space-y-2">
          <div className="flex items-center justify-between">
            <input
              type="text"
              value={editingId === rule.id ? rule.label : rule.label || `Rule ${index + 1}`}
              onChange={(e) => handleLabelEdit(rule.id, e.target.value)}
              onFocus={() => setEditingId(rule.id)}
              onBlur={() => setEditingId(null)}
              className="nodrag nopan flex-1 px-1 py-0.5 text-xs bg-transparent border-none text-neutral-200 focus:outline-none"
            />
            {nodeData.rules.length > 1 && (
              <button
                onClick={() => handleDelete(rule.id)}
                className="nodrag nopan text-neutral-500 hover:text-red-400"
                title="Delete rule"
              >
                <svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
                  <path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
                </svg>
              </button>
            )}
          </div>

          <select
            value={rule.mode}
            onChange={(e) => handleModeChange(rule.id, e.target.value as MatchMode)}
            className="nodrag nopan w-full px-2 py-1 text-xs bg-neutral-700 border border-neutral-600 rounded text-neutral-200 focus:outline-none focus:ring-1 focus:ring-blue-500"
          >
            <option value="exact">Exact match</option>
            <option value="contains">Contains</option>
            <option value="starts-with">Starts with</option>
            <option value="ends-with">Ends with</option>
          </select>

          <input
            type="text"
            value={rule.value}
            onChange={(e) => handleRuleValueChange(rule.id, e.target.value)}
            placeholder="Enter match value"
            className="nodrag nopan w-full px-2 py-1 text-xs bg-neutral-700 border border-neutral-600 rounded text-neutral-200 focus:outline-none focus:ring-1 focus:ring-blue-500"
          />

          {rule.isMatched !== undefined && (
            <div className="flex items-center gap-1">
              <div className={`w-2 h-2 rounded-full ${rule.isMatched ? 'bg-green-500' : 'bg-neutral-600'}`} />
              <span className="text-xs text-neutral-400">
                {rule.isMatched ? 'Matched' : 'Not matched'}
              </span>
            </div>
          )}
        </div>
      ))}

      <button
        onClick={handleAddRule}
        className="nodrag nopan w-full px-2 py-1 text-xs bg-neutral-700 hover:bg-neutral-600 border border-neutral-600 rounded text-neutral-300 transition-colors"
      >
        + Add Rule
      </button>
    </div>
  );
}
