"use client";

import { ReactNode, useRef, useLayoutEffect } from "react";
import { Node, useReactFlow } from "@xyflow/react";
import { useWorkflowStore } from "@/store/workflowStore";
import { isPanningRef, isDraggingNodeRef } from "@/components/WorkflowCanvas";
import { NodeGenerationComposer } from "@/components/composer/NodeGenerationComposer";
import { useCanvasPreviewMode } from "@/components/CanvasPreviewModeContext";
import { useI18n } from "@/i18n";
import { isPopiProviderMode } from "@/lib/providerMode";
import type { NodeType, WorkflowNode, WorkflowNodeData } from "@/types";
import { useInlineParameters } from "@/hooks/useInlineParameters";
import { browseRegistry } from "@/utils/browseRegistry";
import { isSmartAudioGenerateMode } from "@/utils/smartAudioMode";
import { isSmartImageGenerateMode } from "@/utils/smartImageMode";
import { isSmartVideoGenerateMode } from "@/utils/smartVideoMode";
import { FloatingNodeHeader } from "./FloatingNodeHeader";
import { useNodeHeaderContext } from "./NodeHeaderContext";
import { getNodeHeaderMediaMeta } from "./nodeHeaderMedia";
import { getEffectiveNodeHeaderModel, getNodeHeaderTitle } from "./nodeHeaderTitle";

const DEFAULT_NODE_DIMENSION = 300;

interface BaseNodeProps {
  id: string;
  children: ReactNode;
  selected?: boolean;
  isExecuting?: boolean;
  hasError?: boolean;
  className?: string;
  contentClassName?: string;
  autoSizeContent?: boolean;
  minWidth?: number;
  minHeight?: number;
  /** When true, node has no background/border — content fills the entire node area */
  fullBleed?: boolean;
  /** When true, bottom corners lose rounding so the selection ring connects to the settings panel below */
  settingsExpanded?: boolean;
  /** Settings panel rendered outside the bordered area so it shares the node's full width */
  settingsPanel?: ReactNode;
  /** Floating UI rendered outside clipped node content, for selected-node panels. */
  floatingPanel?: ReactNode;
  /** Tutorial identifier for highlighting */
  dataTutorial?: string;
}

/**
 * Read a node's effective width or height from persisted style first, falling
 * back locally without mutating imported workflow data.
 */
function getNodeDimension(node: Node, axis: "width" | "height"): number {
  return (
    (node.style?.[axis] as number) ??
    (node.measured?.[axis] as number) ??
    DEFAULT_NODE_DIMENSION
  );
}

/**
 * Apply dimensions to persisted node style. Business sizing is stored in
 * `style.width/height`; React Flow can keep its own measured/top-level fields.
 */
function applyNodeDimensions(node: Node, width: number, height: number): Node {
  return {
    ...node,
    style: { ...node.style, width, height },
  };
}

export function BaseNode({
  id,
  children,
  selected = false,
  isExecuting = false,
  hasError = false,
  className = "",
  contentClassName,
  autoSizeContent = false,
  minWidth = 180,
  minHeight = 100,
  fullBleed = false,
  settingsExpanded = false,
  settingsPanel,
  floatingPanel,
  dataTutorial,
}: BaseNodeProps) {
  const { t } = useI18n();
  const isPreviewMode = useCanvasPreviewMode();
  const currentNodeIds = useWorkflowStore((state) => state.currentNodeIds);
  const setHoveredNodeId = useWorkflowStore((state) => state.setHoveredNodeId);
  const node = useWorkflowStore((state) => state.nodes.find((candidate) => candidate.id === id) as WorkflowNode | undefined);
  const edges = useWorkflowStore((state) => state.edges);
  const updateNodeData = useWorkflowStore((state) => state.updateNodeData);
  const { onExpandNode } = useNodeHeaderContext();
  const { inlineParametersEnabled } = useInlineParameters();
  const isCurrentlyExecuting = currentNodeIds.includes(id);
  const { setNodes } = useReactFlow();

  const settingsPanelRef = useRef<HTMLDivElement>(null);
  const contentRef = useRef<HTMLDivElement>(null);
  const trackedSettingsHeightRef = useRef(0);
  const isAnimatingRef = useRef(false);
  const animationTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);

  // Adjust node height when settings expand or collapse
  useLayoutEffect(() => {
    // Cancel any pending animation timeout from a previous toggle (handles rapid toggling)
    if (animationTimeoutRef.current) {
      clearTimeout(animationTimeoutRef.current);
      animationTimeoutRef.current = null;
    }

    const contentEl = contentRef.current;
    const ANIMATION_MS = 160;

    if (!settingsExpanded && trackedSettingsHeightRef.current > 0) {
      // --- COLLAPSE ---
      const heightToRemove = trackedSettingsHeightRef.current;
      trackedSettingsHeightRef.current = 0;
      isAnimatingRef.current = true;

      // Lock content height for the full animation duration
      if (contentEl) {
        contentEl.style.height = contentEl.offsetHeight + "px";
      }

      setNodes((nodes) =>
        nodes.map((node) => {
          if (node.id !== id) return node;
          const currentHeight = getNodeDimension(node, "height");
          const newHeight = Math.max(minHeight, currentHeight - heightToRemove);
          return {
            ...applyNodeDimensions(node, getNodeDimension(node, "width"), newHeight),
            data: { ...node.data, _settingsPanelHeight: 0 },
          };
        })
      );

      animationTimeoutRef.current = setTimeout(() => {
        isAnimatingRef.current = false;
        if (contentEl) contentEl.style.height = "";
      }, ANIMATION_MS);
    } else if (settingsExpanded && settingsPanel) {
      // --- EXPAND ---
      // Lock the content wrapper rigid so flex can't redistribute space as the
      // settings panel grows. Without this, flex-1 + min-h-0 lets the wrapper
      // shrink between CSS transition frames and the ResizeObserver setNodes catch-up.
      isAnimatingRef.current = true;

      if (contentEl) {
        const wrapperEl = contentEl.parentElement as HTMLElement | null;
        if (wrapperEl) {
          wrapperEl.style.flex = "none";
          wrapperEl.style.height = wrapperEl.offsetHeight + "px";
        }
      }

      animationTimeoutRef.current = setTimeout(() => {
        isAnimatingRef.current = false;

        // Apply the final panel height in one shot, then unlock the wrapper.
        // Subtract any previously saved panel height to avoid double-counting
        // on workflow reload (saved node height already includes panel).
        const finalHeight = trackedSettingsHeightRef.current;
        if (finalHeight > 0) {
          setNodes((nodes) =>
            nodes.map((node) => {
              if (node.id !== id) return node;
              const savedPanelHeight = typeof (node.data as Record<string, unknown>)?._settingsPanelHeight === "number"
                ? (node.data as Record<string, unknown>)._settingsPanelHeight as number
                : 0;
              const heightToAdd = finalHeight - savedPanelHeight;
              const currentHeight = getNodeDimension(node, "height");
              const newHeight = Math.max(minHeight, currentHeight + heightToAdd);
              return {
                ...applyNodeDimensions(node, getNodeDimension(node, "width"), newHeight),
                data: { ...node.data, _settingsPanelHeight: finalHeight },
              };
            })
          );
        }

        if (contentEl) {
          const wrapperEl = contentEl.parentElement as HTMLElement | null;
          if (wrapperEl) {
            wrapperEl.style.flex = "";
            wrapperEl.style.height = "";
          }
        }
      }, ANIMATION_MS);
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [settingsExpanded]);

  // ResizeObserver to track dynamic settings panel height changes (e.g., model param count changes)
  useLayoutEffect(() => {
    if (!settingsExpanded || !settingsPanel) return;
    const panelEl = settingsPanelRef.current;
    if (!panelEl) return;

    const observer = new ResizeObserver((entries) => {
      for (const entry of entries) {
        const newPanelHeight = entry.contentRect.height;
        if (newPanelHeight === 0) continue;
        const delta = newPanelHeight - trackedSettingsHeightRef.current;
        if (Math.abs(delta) < 2) continue; // Ignore sub-pixel changes

        trackedSettingsHeightRef.current = newPanelHeight;

        // During animation, just track the height — skip setNodes to avoid
        // multiple re-renders. The expand timeout will apply one final update.
        if (isAnimatingRef.current) continue;

        // Lock content height to prevent image flicker during resize
        const contentEl = contentRef.current;
        if (contentEl) {
          contentEl.style.height = contentEl.offsetHeight + "px";
        }

        setNodes((nodes) =>
          nodes.map((node) => {
            if (node.id !== id) return node;
            const currentHeight = getNodeDimension(node, "height");
            const newHeight = Math.max(minHeight, currentHeight + delta);
            return {
              ...applyNodeDimensions(node, getNodeDimension(node, "width"), newHeight),
              data: { ...node.data, _settingsPanelHeight: newPanelHeight },
            };
          })
        );

        // Release locked height after layout settles
        requestAnimationFrame(() => {
          if (contentEl) {
            contentEl.style.height = "";
          }
        });
      }
    });

    observer.observe(panelEl);
    return () => observer.disconnect();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [settingsExpanded, settingsPanel]);

  // Cleanup animation timeout on unmount
  useLayoutEffect(() => {
    return () => {
      if (animationTimeoutRef.current) {
        clearTimeout(animationTimeoutRef.current);
      }
    };
  }, []);

  const hasExpandedSettings = settingsExpanded && settingsPanel;
  const nodeTitle = node ? getNodeHeaderTitle(node, edges, t) : "";
  const mediaMeta = node ? getNodeHeaderMediaMeta(node, nodeTitle, edges) : null;
  const customTitle = typeof node?.data?.customTitle === "string" ? node.data.customTitle : undefined;
  const effectiveModel = node ? getEffectiveNodeHeaderModel(node, edges) : undefined;
  const nodeData = node?.data as WorkflowNodeData | undefined;
  const showBrowse = Boolean(node && inlineParametersEnabled && (
    (node.type === "smartImage" && isSmartImageGenerateMode(node.id, nodeData, edges)) ||
    (node.type === "smartAudio" && isSmartAudioGenerateMode(node.id, nodeData, edges)) ||
    (node.type === "smartVideo" && isSmartVideoGenerateMode(node.id, nodeData, edges)) ||
    node.type === "nanoBanana" ||
    node.type === "generateVideo" ||
    node.type === "generate3d" ||
    node.type === "generateAudio"
  ));
  const browseAction = showBrowse ? (
    <button
      onClick={() => browseRegistry.open(id)}
      className="nodrag nopan text-[10px] py-0.5 px-1.5 bg-neutral-700 hover:bg-neutral-600 border border-neutral-600 rounded text-neutral-300 transition-colors"
    >
      {t("common.browse")}
    </button>
  ) : undefined;
  const showHeader = Boolean(node && node.type !== "group" && !isPreviewMode);
  const header = showHeader ? (
    <FloatingNodeHeader
      id={id}
      type={node!.type as NodeType}
      isInLockedGroup={Boolean((node!.data as Record<string, unknown>)?.isInLockedGroup)}
      selected={selected}
      title={nodeTitle}
      mediaMeta={mediaMeta}
      customTitle={customTitle}
      provider={isPopiProviderMode() ? undefined : effectiveModel?.provider}
      headerAction={browseAction}
      onCustomTitleChange={(nodeId, title) => updateNodeData(nodeId, { customTitle: title || undefined })}
      onExpandNode={onExpandNode}
    />
  ) : null;

  return (
    <div
      className={hasExpandedSettings
        ? `relative flex flex-col w-full h-full overflow-visible rounded-lg bg-[var(--surface-2)] text-[var(--text-primary)] shadow-[var(--shadow-node)] ${selected ? "ring-2 ring-[var(--node-selected-ring)] shadow-[var(--node-selected-shadow)]" : ""}`
        : "contents"}
      data-tutorial={hasExpandedSettings ? dataTutorial : undefined}
    >
      <div
        className={`
          ${autoSizeContent ? "h-full w-full" : hasExpandedSettings ? "flex-1 min-h-0 w-full" : "h-full w-full"} flex flex-col overflow-visible relative
          ${fullBleed
            ? `${settingsExpanded ? "rounded-t-lg border-b-0" : "rounded-lg"} border border-[var(--border-subtle)] bg-[var(--surface-2)] text-[var(--text-primary)] shadow-[var(--shadow-node)]`
            : `bg-[var(--surface-2)] text-[var(--text-primary)] ${settingsExpanded ? "rounded-t-lg border-b-0" : "rounded-lg"} shadow-[var(--shadow-node)] border`}
          ${fullBleed ? "" : (isCurrentlyExecuting || isExecuting ? "border-blue-500 ring-1 ring-blue-500/20" : "border-[var(--border-subtle)]")}
          ${fullBleed ? "" : (hasError ? "border-red-500" : "")}
          ${fullBleed && selected && !settingsExpanded ? "ring-2 ring-[var(--node-selected-ring)] shadow-[var(--node-selected-shadow)]" : ""}
          ${!fullBleed && selected && !settingsExpanded ? "border-[var(--node-selected-border)] ring-2 ring-[var(--node-selected-ring)] shadow-[var(--node-selected-shadow)]" : ""}
          ${!fullBleed && selected && settingsExpanded ? "border-[var(--node-selected-border)]" : ""}
          ${className}
        `}
        data-tutorial={!hasExpandedSettings ? dataTutorial : undefined}
        onMouseEnter={(e) => {
          if (isPreviewMode) return;
          if (e.buttons !== 0 || isPanningRef.current || isDraggingNodeRef.current) return;
          setHoveredNodeId(id);
        }}
        onMouseLeave={(e) => {
          if (isPreviewMode) return;
          if (e.buttons !== 0 || isPanningRef.current || isDraggingNodeRef.current) return;
          setHoveredNodeId(null);
        }}
      >
        {header}
        <div ref={contentRef} style={{ contain: "layout style" }} className={contentClassName ?? (autoSizeContent ? "relative h-full w-full" : fullBleed ? "flex-1 min-h-0 relative" : "px-3 pb-4 flex-1 min-h-0 overflow-visible flex flex-col")}>{children}</div>
        {floatingPanel}
        {!isPreviewMode && <NodeGenerationComposer nodeId={id} selected={selected} />}
      </div>
      {settingsPanel && (
        <div ref={settingsPanelRef}>
          {settingsPanel}
        </div>
      )}
    </div>
  );
}
