import { queryKeys } from '@lenserfight/data/cache'
import { lensesService, workflowsService, seoService } from '@lenserfight/data/repositories'
import type { WorkflowNodeResultRecord } from '@lenserfight/data/repositories'
import {
  validateBrowserExecutionPlan,
  validateWorkflow,
  TRIGGER_NODE_TYPES,
} from '@lenserfight/infra/execution'
import { ArtifactLifecycleMenu } from '@lenserfight/features/artifact-lifecycle'
import { useAuth } from '@lenserfight/features/auth'
import { useAIModels } from '@lenserfight/features/generations'
import {
  useCreateLens,
  CreateLensModal,
  useFundingSource,
  FundingSourceToggle,
} from '@lenserfight/features/lenses'
import { useChainabitConnection } from '@lenserfight/features/store'
import { useLenser } from '@lenserfight/features/profile'
import { useShareContext } from '@lenserfight/features/share'
import { ExportButton } from '@lenserfight/features/exports'
import { Badge, Button } from '@lenserfight/ui/components'
import { PageMeta } from '@lenserfight/ui/layout'
import { Dialog } from '@lenserfight/ui/overlays'
import { useQuery, useQueryClient } from '@tanstack/react-query'
import {
  AlertTriangle,
  ArrowLeft,
  Bookmark,
  CalendarClock,
  ChevronDown,
  FlaskConical,
  GitBranch,
  GitFork,
  History,
  Layers,
  Lock,
  Maximize2,
  Pencil,
  Play,
  Square,
  ThumbsUp,
  X,
  Zap,
} from 'lucide-react'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useNavigate, useSearchParams } from 'react-router-dom'
import { toast } from 'sonner'

import { CreateWorkflowWizard } from '../components/CreateWorkflowWizard'
import { WorkflowBuilderCanvas } from '../components/WorkflowBuilderCanvas'
import { WorkflowCronPanel } from '../components/WorkflowCronPanel'
import { WorkflowFinalOutputBanner } from '../components/WorkflowFinalOutputBanner'
import { WorkflowLensPalette } from '../components/WorkflowLensPalette'
import { WorkflowNodeConfigDispatcher } from '../components/WorkflowNodeConfigDispatcher'
import { WorkflowPhasesEditor } from '../components/WorkflowPhasesEditor'
import { WorkflowProgressView } from '../components/WorkflowProgressView'
import { WorkflowRootInputsPanel } from '../components/WorkflowRootInputsPanel'
import { WorkflowRunHistoryPanel } from '../components/WorkflowRunHistoryPanel'
import { WorkflowRunRecoveryBanner } from '../components/WorkflowRunRecoveryBanner'
import { ExecutionControlBanner } from '../components/ExecutionControlBanner'
import { useExecutionControl } from '../hooks/useExecutionControl'
import { validateDryRunPlan } from '../execution/workflowDryRun'
import { useForkWorkflow } from '../hooks/useForkWorkflow'
import { useWorkflow } from '../hooks/useWorkflow'
import { useWorkflowExecution } from '../hooks/useWorkflowExecution'
import { useWorkflowReaction } from '../hooks/useWorkflowReaction'
import { useWorkflowRun } from '../hooks/useWorkflowRun'
import { useWorkflowRunHistory } from '../hooks/useWorkflowRunHistory'
import { useWorkflowRunProvenance, useWorkflowRunState } from '../hooks/useWorkflowRunState'

import type { WorkflowNodeConfig } from '../components/WorkflowCanvasNode'
import type { AIProvider, AIProviderModel } from '@lenserfight/types'

interface WorkflowBuilderPageProps {
  workflowId: string
  runId?: string
  onBattleClick?: (workflowId: string) => void
}

export function WorkflowBuilderPage({ workflowId }: WorkflowBuilderPageProps) {
  const navigate = useNavigate()
  const queryClient = useQueryClient()
  const [searchParams, setSearchParams] = useSearchParams()
  const { user } = useAuth()
  const { lenser } = useLenser()
  const { setShareConfig } = useShareContext()
  const {
    workflow,
    nodes: bootstrapNodes,
    edges: bootstrapEdges,
    isLoading,
  } = useWorkflow(workflowId)

  // Live node/edge state: the canvas writes its saves back to these cache keys
  // via queryClient.setQueryData. Subscribing here ensures execution and the
  // progress view always see the current canvas state, not the stale bootstrap
  // snapshot captured at page load.
  const { data: liveNodes } = useQuery<typeof bootstrapNodes>({
    queryKey: queryKeys.workflows.nodes(workflowId),
    queryFn: () => Promise.resolve([] as typeof bootstrapNodes),
    staleTime: Infinity,
    enabled: false, // Never auto-fetch — the canvas owns this cache via setQueryData
  })
  const { data: liveEdges } = useQuery<typeof bootstrapEdges>({
    queryKey: queryKeys.workflows.edges(workflowId),
    queryFn: () => Promise.resolve([] as typeof bootstrapEdges),
    staleTime: Infinity,
    enabled: false, // Never auto-fetch — the canvas owns this cache via setQueryData
  })

  const nodes = liveNodes ?? bootstrapNodes
  const edges = liveEdges ?? bootstrapEdges
  const { models, isLoading: modelsLoading } = useAIModels()
  const [showRunPanel, setShowRunPanel] = useState(() => searchParams.get('run') === '1')
  const [isEditModalOpen, setIsEditModalOpen] = useState(false)
  const [runPanelTab, setRunPanelTab] = useState<'run' | 'history' | 'schedule'>('run')
  const [builderMode, setBuilderMode] = useState<'canvas' | 'phases'>('canvas')
  const [selectedHistoryRunId, setSelectedHistoryRunId] = useState<string | null>(null)
  const returnTo = searchParams.get('returnTo') || '/workflows'

  // Provider/model selection state (replaces globalModelId SelectField)
  const [selectedProviderKey, setSelectedProviderKey] = useState('')
  const [selectedModelKey, setSelectedModelKey] = useState(() => {
    if (typeof window === 'undefined') return ''
    return localStorage.getItem('lf-workflow-global-model') ?? ''
  })

  const [paletteCollapsed, setPaletteCollapsed] = useState(
    () => typeof window !== 'undefined' && window.innerWidth < 768
  )

  // ── Funding source (BYOK / platform credit) ────────────────────────────────
  const funding = useFundingSource(selectedProviderKey)
  const chainabit = useChainabitConnection()

  const {
    startRun,
    stopRun,
    retryRun,
    isPending: starting,
    isRetrying,
    runId,
    nodeResults,
    isRunning,
  } = useWorkflowRun(workflowId, {
    skipSse: funding.fundingSource === 'user_byok_local',
  })
  const {
    isSystemLocked,
    isQueueFrozen,
    isStopping,
    status: execCtrlStatus,
  } = useExecutionControl()
  const resolveLocalKeyRef = useRef<((id: string) => Promise<string>) | undefined>(undefined)
  resolveLocalKeyRef.current = funding.resolveLocalKey
  const stableResolveLocalKey = useCallback(
    (keyId: string) =>
      resolveLocalKeyRef.current
        ? resolveLocalKeyRef.current(keyId)
        : Promise.reject(new Error('Local key resolver not ready')),
    []
  )

  const {
    execute: executeWorkflow,
    stopExecution,
    executeDryRun,
    stopDryRun,
  } = useWorkflowExecution({
    nodes,
    edges,
    models,
    fundingSource: funding.fundingSource,
    selectedKeyRefId: funding.selectedKeyRefId,
    selectedLocalKeyId: funding.selectedLocalKeyId,
    resolveLocalKey: stableResolveLocalKey,
    localKeys: funding.localKeys,
  })

  // ── Dry Run state ───────────────────────────────────────────────────────────
  const [isDryRunning, setIsDryRunning] = useState(false)
  const [dryRunNodeResults, setDryRunNodeResults] = useState<WorkflowNodeResultRecord[]>([])
  const [showDryRunPanel, setShowDryRunPanel] = useState(false)

  // Per-node config overrides — synced to DB via canvas debounced save (workflow_nodes.config)
  const [nodeConfigs, setNodeConfigs] = useState<Record<string, WorkflowNodeConfig>>({})

  // Selected node for the config panel
  const [selectedNodeConfig, setSelectedNodeConfig] = useState<{
    nodeId: string
    lensId: string
    versionId: string | null
    nodeLabel: string
  } | null>(null)

  // ── Run history ─────────────────────────────────────────────────────────────
  // Defer the run-history fetch until the user opens the run panel. This keeps
  // initial page load lean for the common case (just viewing the workflow).
  const { runs: historyRuns } = useWorkflowRunHistory(workflowId, {
    enabled: showRunPanel,
  })

  const { data: historyNodeResults = [] } = useQuery({
    queryKey: ['workflow', workflowId, 'run', selectedHistoryRunId, 'nodeResults'],
    queryFn: () => workflowsService.getNodeResults(selectedHistoryRunId!),
    enabled: !!selectedHistoryRunId,
    staleTime: 1000 * 60,
  })

  // N8N inspector — drive the live run strip from the canonical projection so
  // active node + waiting reasons reflect engine state instead of just SSE
  // frames. The history variant uses the same projection scoped to the
  // selected run so replays show identical lineage badges.
  const { data: liveRunState } = useWorkflowRunState(runId)
  const { data: liveProvenance = [] } = useWorkflowRunProvenance(runId)
  const { data: historyRunState } = useWorkflowRunState(selectedHistoryRunId)
  const { data: historyProvenance = [] } = useWorkflowRunProvenance(selectedHistoryRunId)

  // Terminal node: prefer a single sink; with multiple sinks pick the most
  // recently completed (then highest ordinal) so the banner matches user intent.
  const terminalNodeId = useMemo(() => {
    const targetSet = new Set(edges.map((e) => e.target_node_id))
    const sinks = nodes.filter((n) => !targetSet.has(n.id))
    if (sinks.length === 0) return null
    if (sinks.length === 1) return sinks[0]!.id

    const ranked = [...sinks].sort((a, b) => {
      const ra = nodeResults.find((r) => r.node_id === a.id)
      const rb = nodeResults.find((r) => r.node_id === b.id)
      const ta = ra?.completed_at ? new Date(ra.completed_at).getTime() : 0
      const tb = rb?.completed_at ? new Date(rb.completed_at).getTime() : 0
      if (ta !== tb) return tb - ta
      return b.ordinal - a.ordinal
    })
    return ranked[0]?.id ?? null
  }, [nodes, edges, nodeResults])

  const terminalNodeResult = nodeResults.find((r) => r.node_id === terminalNodeId)
  const terminalNodeLabel =
    nodes.find((n) => n.id === terminalNodeId)?.label ??
    `Node ${(nodes.find((n) => n.id === terminalNodeId)?.ordinal ?? 0) + 1}`

  // ── Trigger node presence check (editor warning + execution gate) ──────────
  const hasTriggerNode = useMemo(() => {
    if (nodes.length === 0) return true // empty canvas — no warning yet
    const targetSet = new Set(edges.map((e) => e.target_node_id))
    const rootNodes = nodes.filter((n) => !targetSet.has(n.id))
    return rootNodes.some((n) => {
      const nodeType = (n.config as Record<string, unknown> | null)?.['node_type'] as
        | string
        | undefined
      return TRIGGER_NODE_TYPES.has(nodeType ?? '')
    })
  }, [nodes, edges])

  // ── Output callbacks — defined after handleExecuteClick (see below) ──────────
  const handlePostToThread = useCallback(
    (text: string, nodeLabel: string) => {
      const body = encodeURIComponent(`**${nodeLabel}**\n\n${text}`)
      navigate(`/threads/compose?body=${body}`)
    },
    [navigate]
  )

  const handleEditClose = useCallback(() => {
    setIsEditModalOpen(false)
    setSearchParams(
      (prev) => {
        const next = new URLSearchParams(prev)
        next.delete('step')
        return next
      },
      { replace: true }
    )
  }, [setSearchParams])

  const isOwner = !!lenser && lenser.id === workflow?.lenser_id
  const { mutate: forkWorkflow, isPending: isForking } = useForkWorkflow()
  const {
    liked,
    saved,
    likeCount,
    toggleLike,
    toggleSave,
    isPending: reactionPending,
  } = useWorkflowReaction(
    workflowId,
    workflow?.reaction_totals as Record<string, number> | null | undefined,
    workflow?.viewer_reactions as Record<string, boolean> | null | undefined,
    isLoading
  )

  // ── Lens edit modal (via useCreateLens in edit mode) ────────────────────────
  const lensModal = useCreateLens()
  const [editingLensId, setEditingLensId] = useState<string | null>(null)

  const handleEditLens = async (lensId: string) => {
    try {
      const lens = await lensesService.getLensDetail(lensId, user?.id)
      if (!lens) return
      let initialVersionParams: Array<{ label: string; toolId: string }> = []
      if (lens.latestVersionId) {
        const versionDetail = await lensesService.getVersionById(lens.latestVersionId)
        initialVersionParams = (versionDetail?.parameters ?? []).map((param) => ({
          label: param.label,
          toolId: param.toolId,
        }))
      }
      setEditingLensId(lens.id)
      lensModal.openModal({
        id: lens.id,
        title: lens.title,
        content: lens.content,
        tags: lens.tags ?? [],
        visibility: lens.visibility,
        versionParams: initialVersionParams,
      })
    } catch {
      // silently ignore — can't edit if fetch fails
    }
  }

  useEffect(() => {
    if (!lensModal.isOpen) {
      setEditingLensId(null)
    }
  }, [lensModal.isOpen])

  useEffect(() => {
    if (workflow) {
      setShareConfig({
        title: workflow.title,
        resourceType: 'workflow',
        resourceId: workflow.id,
      })
    }
    return () => setShareConfig(null)
  }, [workflow, setShareConfig])

  // ── Node config panel ───────────────────────────────────────────────────────
  const handleConfigNode = (nodeId: string, lensId: string) => {
    const node = nodes.find((n) => n.id === nodeId)
    setSelectedNodeConfig({
      nodeId,
      lensId,
      versionId: node?.version_id ?? null,
      nodeLabel: node?.label ?? `Node ${(node?.ordinal ?? 0) + 1}`,
    })
    // Hide run panel when config panel opens
    setShowRunPanel(false)
  }

  const handleSaveNodeConfig = (nodeId: string, config: WorkflowNodeConfig) => {
    setNodeConfigs((prev) => ({ ...prev, [nodeId]: config }))
    const node = nodes.find((candidate) => candidate.id === nodeId)
    if (!node || nodeId.startsWith('tmp-')) return

    queryClient.setQueryData(
      queryKeys.workflows.nodes(workflowId),
      (old: typeof nodes | undefined) =>
        (old ?? nodes).map((candidate) =>
          candidate.id === nodeId ? { ...candidate, config } : candidate
        )
    )

    workflowsService
      .upsertNodes(workflowId, [
        {
          id: node.id,
          lens_id: node.lens_id,
          version_id: node.version_id ?? null,
          label: node.label ?? undefined,
          ordinal: node.ordinal,
          position_x: node.position_x,
          position_y: node.position_y,
          config: config as Record<string, unknown>,
        },
      ])
      .then((savedNodes) => {
        if (savedNodes.length === 0) return
        queryClient.setQueryData(
          queryKeys.workflows.nodes(workflowId),
          (old: typeof nodes | undefined) => {
            const byId = new Map((old ?? nodes).map((candidate) => [candidate.id, candidate]))
            for (const savedNode of savedNodes) byId.set(savedNode.id, savedNode)
            return Array.from(byId.values()).sort((a, b) => a.ordinal - b.ordinal)
          }
        )
      })
      .catch(() => {
        toast.error('Could not save node configuration. Try again in a moment.')
      })
  }

  const selectedCurrentConfig = useMemo(() => {
    if (!selectedNodeConfig) return {}
    const persisted = nodes.find((node) => node.id === selectedNodeConfig.nodeId)?.config
    return (
      nodeConfigs[selectedNodeConfig.nodeId] ??
      (persisted as WorkflowNodeConfig | null | undefined) ??
      {}
    )
  }, [nodeConfigs, nodes, selectedNodeConfig])

  // ── Providers/Models derived from flat useAIModels list ─────────────────────
  const providers: AIProvider[] = useMemo(() => {
    const seen = new Set<string>()
    return models
      .filter(
        (m) => m.is_active && !!m.key && !seen.has(m.provider) && (seen.add(m.provider), true)
      )
      .map((m) => ({
        key: m.provider,
        display_name: m.providerDisplayName ?? m.provider,
        id: m.provider_id ?? '',
      }))
  }, [models])

  const effectiveProviderKey = useMemo(() => {
    if (funding.fundingSource === 'user_byok_cloud') {
      return funding.availableKeys.find((k) => k.id === funding.selectedKeyRefId)?.providerKey ?? ''
    }
    if (funding.fundingSource === 'user_byok_local') {
      return funding.localKeys.find((k) => k.id === funding.selectedLocalKeyId)?.provider ?? ''
    }
    return selectedProviderKey
  }, [
    funding.fundingSource,
    funding.availableKeys,
    funding.selectedKeyRefId,
    funding.localKeys,
    funding.selectedLocalKeyId,
    selectedProviderKey,
  ])

  const providerModels: AIProviderModel[] = useMemo(() => {
    if (!effectiveProviderKey) return []
    return models
      .filter((m) => m.is_active && !!m.key && m.provider === effectiveProviderKey)
      .map((m) => ({ key: m.key, name: m.name, inputModalities: m.input_modalities }))
  }, [models, effectiveProviderKey])

  // ── Run ─────────────────────────────────────────────────────────────────────
  const handleModelChange = (value: string) => {
    setSelectedModelKey(value)
    if (typeof window !== 'undefined') localStorage.setItem('lf-workflow-global-model', value)
  }

  const handleProviderChange = (key: string) => {
    setSelectedProviderKey(key)
    setSelectedModelKey('')
  }

  /** Cloud BYOK vault keys are not available in the browser build outside dev — no worker claims manual runs today. */
  const cloudByokNeedsExecutor = funding.fundingSource === 'user_byok_cloud' && !import.meta.env.DEV
  const canExecute = !!selectedModelKey && funding.isReady && !cloudByokNeedsExecutor

  useEffect(() => {
    if (funding.fundingSource !== 'user_byok_cloud' || isRunning) return
    const hasCloudAuthFailure = nodeResults.some((r) =>
      /invalid[_\s-]?api[_\s-]?key|api key|unauthorized|auth|permission|forbidden/i.test(
        String(r.error_message ?? '')
      )
    )
    if (!hasCloudAuthFailure) return

    funding.setFundingSource('platform_credit')
    toast.error('Cloud BYOK key failed authentication. Switched funding mode to Cloud credits.')
  }, [funding, isRunning, nodeResults])

  const handleExecuteClick = async (rootInputs: Record<string, unknown> = {}) => {
    if (!canExecute) return

    if (nodes.length > 0) {
      const struct = validateWorkflow(
        nodes.map((n) => {
          const cfg = (n.config ?? null) as Record<string, unknown> | null
          const nodeType = cfg?.['node_type'] as string | undefined
          return {
            id: n.id,
            lensId: n.lens_id ?? undefined,
            versionId: n.version_id ?? null,
            config: cfg,
            // Pass node_type as kind so the trigger presence rule can identify
            // entry nodes independently of lens-kind classifications.
            kind: nodeType,
          }
        }),
        edges.map((e) => ({
          id: e.id,
          sourceNodeId: e.source_node_id,
          targetNodeId: e.target_node_id,
          sourceOutputKey: e.source_output_key,
          targetParamLabel: e.target_param_label,
          condition: e.condition as
            | { type: 'equals' | 'contains' | 'present' | 'truthy'; value?: unknown }
            | null
            | undefined,
        })),
        { requireTriggerNode: true }
      )
      if (!struct.ok) {
        toast.error(struct.errors[0]?.message ?? 'Workflow validation failed')
        return
      }
    }

    const plan = validateBrowserExecutionPlan(
      nodes.map((n) => ({ id: n.id, config: n.config ?? null })),
      selectedModelKey,
      models.map((m) => ({ key: m.key, provider: m.provider })),
      funding.fundingSource
    )
    if (!plan.ok) {
      toast.error(
        plan.errors[0]?.message ??
          'This workflow cannot run with the current funding and model selection.'
      )
      return
    }

    const run = await startRun({ inputs: rootInputs, globalModelId: selectedModelKey })
    setSelectedNodeConfig(null)
    if (!run?.id) return

    // Fire execution orchestrator in background — status updates flow via Realtime.
    // Production Cloud BYOK is blocked above (no worker claims manual workflow_runs).
    executeWorkflow(run.id, selectedModelKey, rootInputs).catch((err) => {
      toast.error(`Workflow execution failed: ${err instanceof Error ? err.message : String(err)}`)
    })
  }

  const handleStopClick = () => {
    stopExecution()
    stopRun()
  }

  const handleDryRunClick = async (rootInputs: Record<string, unknown> = {}) => {
    const validation = validateDryRunPlan(nodes, edges)
    if (!validation.ok) {
      toast.error(validation.errors[0] ?? 'Dry run validation failed.')
      return
    }
    if (validation.warnings.length > 0) {
      toast.info(validation.warnings[0], { duration: 6000 })
    }

    setDryRunNodeResults([])
    setShowRunPanel(false)
    setShowDryRunPanel(true)
    setSelectedNodeConfig(null)
    setIsDryRunning(true)

    try {
      await executeDryRun(selectedModelKey, rootInputs, (nodeId, record) => {
        setDryRunNodeResults((prev) => {
          const idx = prev.findIndex((r) => r.node_id === nodeId)
          if (idx === -1) return [...prev, record]
          const next = [...prev]
          next[idx] = record
          return next
        })
      })
    } catch (err) {
      const msg = err instanceof Error ? err.message : String(err)
      if (!msg.includes('aborted') && !msg.includes('already in progress')) {
        toast.error(`Dry run failed: ${msg}`)
      }
    } finally {
      setIsDryRunning(false)
    }
  }

  const handleStopDryRun = () => {
    stopDryRun()
    setIsDryRunning(false)
    setDryRunNodeResults((prev) =>
      prev.map((r) => {
        const terminal = [
          'completed',
          'failed',
          'cancelled',
          'skipped',
          'timed_out',
          'blocked',
          'invalidated',
        ].includes(r.status)
        return terminal
          ? r
          : { ...r, status: 'cancelled' as const, error_message: 'Dry run cancelled' }
      })
    )
  }

  // Canvas uses whichever result set is active: dry-run results take priority,
  // then fall back to the most recent real run results.
  const activeNodeResults = useMemo<WorkflowNodeResultRecord[]>(() => {
    if (dryRunNodeResults.length > 0) return dryRunNodeResults
    if (runId && nodeResults.length > 0) return nodeResults
    return []
  }, [dryRunNodeResults, runId, nodeResults])

  const handleRerunWithContext = useCallback(
    (data: Record<string, unknown>) => {
      setRunPanelTab('run')
      setSelectedHistoryRunId(null)
      handleExecuteClick(data)
    },
    // eslint-disable-next-line react-hooks/exhaustive-deps
    [handleExecuteClick]
  )

  // ── Loading / error states ─────────────────────────────────────────────────
  if (isLoading) {
    return (
      <div className="flex h-full items-center justify-center text-sm text-greyscale-400">
        Loading workflow…
      </div>
    )
  }

  if (!workflow) {
    return (
      <div className="flex h-full items-center justify-center text-sm text-greyscale-400">
        Workflow not found.
      </div>
    )
  }

  const wfMeta = seoService.getWorkflowMeta(workflow)

  return (
    <div className="flex h-full flex-col overflow-hidden bg-surface-base">
      <PageMeta
        title={wfMeta.title}
        description={wfMeta.description}
        robots={wfMeta.index === false ? 'noindex,nofollow' : 'index,follow'}
      />
      {/* ── Toolbar ─────────────────────────────────────────────────────────── */}
      <header className="flex flex-shrink-0 items-center border-b border-surface-border bg-surface-base px-4 h-[56px]">
        {/* Left: Identity Group */}
        <div className="flex items-center gap-3 flex-1 min-w-0">
          <Button
            variant="ghost"
            size="sm"
            onClick={() => navigate(returnTo)}
            className="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-xl !p-0 text-greyscale-400 hover:text-greyscale-700 hover:bg-surface-raised transition-colors dark:hover:text-greyscale-200"
            title="Back to workflows"
          >
            <ArrowLeft size={16} />
          </Button>

          <div className="flex items-center gap-2 min-w-0">
            <div className="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-xl bg-surface-raised border border-surface-border shadow-sm">
              <GitBranch
                size={16}
                className="text-primary-yellow-600 dark:text-primary-yellow-500"
              />
            </div>
            <div className="min-w-0">
              <div className="flex items-center gap-2">
                <h1 className="truncate text-sm font-bold text-greyscale-900 dark:text-greyscale-50">
                  {workflow.title}
                </h1>
                <Badge
                  color="blue"
                  variant="outline"
                  className="flex-shrink-0 text-[10px] h-4 px-1.5 font-bold"
                >
                  {nodes.length}
                </Badge>
              </div>
              {workflow.description && (
                <p className="truncate text-[11px] text-greyscale-400 leading-none mt-0.5">
                  {workflow.description}
                </p>
              )}
            </div>
          </div>
        </div>

        {/* Center: Mode Switcher */}
        <div className="flex items-center justify-center px-6">
          <div className="flex items-center gap-0.5 rounded-xl border border-surface-border bg-surface-raised p-1 shadow-inner">
            <button
              type="button"
              onClick={() => setBuilderMode('canvas')}
              className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-semibold transition-all ${
                builderMode === 'canvas'
                  ? 'bg-surface-base text-greyscale-900 dark:text-greyscale-50 shadow-sm'
                  : 'text-greyscale-400 hover:text-greyscale-700 dark:hover:text-greyscale-200'
              }`}
            >
              <GitBranch size={12} /> Canvas
            </button>
            <button
              type="button"
              onClick={() => setBuilderMode('phases')}
              className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-semibold transition-all ${
                builderMode === 'phases'
                  ? 'bg-surface-base text-greyscale-900 dark:text-greyscale-50 shadow-sm'
                  : 'text-greyscale-400 hover:text-greyscale-700 dark:hover:text-greyscale-200'
              }`}
            >
              <Layers size={12} /> Phases
            </button>
          </div>
        </div>

        {/* Right: Actions Group */}
        <div className="flex items-center gap-3 flex-1 justify-end">
          {/* Metadata & Social Group */}
          <div className="flex items-center gap-1.5 pr-3 border-r border-surface-border/50">
            {workflow.parent_workflow_id && (
              <button
                type="button"
                onClick={() => navigate(`/workflows/${workflow.parent_workflow_id}`)}
                className="flex items-center gap-1.5 rounded-full border border-surface-border bg-surface-raised px-2 py-1 transition-colors hover:bg-surface-base group"
                title={`Forked from ${workflow.parent_workflow_title}`}
              >
                <GitFork
                  size={10}
                  className="text-greyscale-400 group-hover:text-primary-yellow-600"
                />
                <span className="max-w-[100px] truncate text-[10px] font-medium text-greyscale-500">
                  {workflow.parent_workflow_title}
                </span>
              </button>
            )}

            {user && (
              <>
                <Button
                  variant="secondary"
                  size="sm"
                  onClick={toggleLike}
                  disabled={reactionPending}
                  className={`!h-8 w-auto rounded-xl px-2.5 transition-colors ${
                    liked
                      ? 'border-primary-yellow-500 bg-primary-yellow-500/10 text-primary-yellow-700'
                      : 'text-greyscale-400'
                  }`}
                >
                  <ThumbsUp size={14} className={liked ? 'fill-current' : ''} />
                  {likeCount > 0 && <span className="text-xs ml-1">{likeCount}</span>}
                </Button>

                <Button
                  variant="secondary"
                  size="sm"
                  onClick={toggleSave}
                  disabled={reactionPending}
                  className={`!h-8 w-auto rounded-xl px-2.5 transition-colors ${
                    saved
                      ? 'border-primary-yellow-500 bg-primary-yellow-500/10 text-primary-yellow-700'
                      : 'text-greyscale-400'
                  }`}
                >
                  <Bookmark size={14} className={saved ? 'fill-current' : ''} />
                </Button>
              </>
            )}

            <ExportButton
              kind="workflow"
              slug={workflowId}
              title={workflow.title ?? undefined}
              fetchPayload={async () => workflow}
              className="!h-8 w-auto rounded-xl px-2.5 text-greyscale-400"
            />
          </div>

          {/* Controls Group */}
          <div className="flex items-center gap-2">
            {isOwner && (
              <Button
                size="sm"
                variant="secondary"
                onClick={() => setIsEditModalOpen(true)}
                className="h-8 gap-1.5 rounded-xl px-3 border-surface-border bg-surface-base"
              >
                <Pencil size={14} /> <span className="hidden xl:inline">Edit</span>
              </Button>
            )}
            {isOwner && (
              <ArtifactLifecycleMenu
                type="workflow"
                id={workflowId}
                extraInvalidateKeys={[
                  queryKeys.workflows.detail(workflowId),
                  queryKeys.workflows.byLenser(workflow?.lenser_id ?? ''),
                  queryKeys.workflows.all,
                ]}
                onDeleted={() => navigate('/workflows')}
              />
            )}

            {!isOwner && workflow.visibility === 'public' && (
              <Button
                size="sm"
                variant="secondary"
                onClick={() => forkWorkflow(workflowId)}
                isLoading={isForking}
                className="h-8 gap-1.5 rounded-xl px-3"
              >
                <GitFork size={14} /> Fork
              </Button>
            )}

            {workflow.visibility === 'private' && (
              <div
                className="flex h-8 w-8 items-center justify-center text-greyscale-400"
                title="Private workflow"
              >
                <Lock size={16} />
              </div>
            )}

            <div className="flex items-center gap-1.5 pl-2 border-l border-surface-border/50">
              {isRunning || isDryRunning ? (
                <Button
                  size="sm"
                  variant="secondary"
                  onClick={isDryRunning ? handleStopDryRun : handleStopClick}
                  className="h-9 gap-1.5 rounded-xl px-4 border-status-red/30 text-status-red hover:bg-status-red/5"
                >
                  <Square size={12} className="fill-current" />
                  {isDryRunning ? 'Stop Test' : 'Stop'}
                </Button>
              ) : (
                <>
                  {/* Test (dry run) button */}
                  <Button
                    size="sm"
                    variant="secondary"
                    onClick={() => handleDryRunClick()}
                    disabled={nodes.length === 0}
                    title="Run a dry test without side effects or a saved run record"
                    className="h-9 gap-1.5 rounded-xl px-3 border-surface-border bg-surface-base text-greyscale-600 hover:text-greyscale-900 dark:hover:text-greyscale-100"
                  >
                    <FlaskConical size={13} />
                    <span className="hidden xl:inline">Test</span>
                  </Button>

                  {/* Run button */}
                  <Button
                    size="sm"
                    onClick={() => {
                      setShowRunPanel(true)
                      setShowDryRunPanel(false)
                      setSelectedNodeConfig(null)
                    }}
                    isLoading={starting}
                    disabled={nodes.length === 0 || isSystemLocked}
                    title={
                      isSystemLocked
                        ? 'Platform is locked — contact a platform admin to resume'
                        : undefined
                    }
                    className="h-9 gap-1.5 rounded-xl px-4 bg-primary-yellow-500 hover:bg-primary-yellow-600 text-black shadow-md shadow-primary-yellow-500/20"
                  >
                    <Play size={12} className="fill-current" /> Run
                  </Button>
                </>
              )}

              {(runId || dryRunNodeResults.length > 0) && (
                <Button
                  variant="secondary"
                  size="sm"
                  onClick={() => {
                    if (dryRunNodeResults.length > 0 && !runId) {
                      setShowDryRunPanel((v) => !v)
                    } else {
                      setShowRunPanel((v) => !v)
                      setSelectedNodeConfig(null)
                    }
                  }}
                  className="h-9 w-auto rounded-xl border border-surface-border bg-surface-raised px-2 text-greyscale-600"
                >
                  <ChevronDown
                    size={16}
                    className={`transition-transform duration-200 ${showRunPanel || showDryRunPanel ? 'rotate-180' : ''}`}
                  />
                </Button>
              )}
            </div>
          </div>
        </div>
      </header>

      {/* ── Main area ──────────────────────────────────────────────────────── */}
      <div className="flex flex-1 overflow-hidden">
        {/* Lens palette sidebar — hidden in Phases mode */}
        {builderMode === 'canvas' && (
          <WorkflowLensPalette
            onDragStart={() => undefined}
            collapsed={paletteCollapsed}
            onToggleCollapse={() => setPaletteCollapsed((v) => !v)}
          />
        )}

        {/* Canvas or Phases editor — fills remaining space */}
        <div className="relative flex-1 overflow-hidden">
          {/* Global execution control banner — shown when the system is locked or queue is frozen */}
          <ExecutionControlBanner
            isSystemLocked={isSystemLocked}
            isQueueFrozen={isQueueFrozen}
            isStopping={isStopping}
            frozenReason={execCtrlStatus?.frozen_reason}
          />
          {/* No-trigger warning banner — non-blocking, shown while editing */}
          {!hasTriggerNode && nodes.length > 0 && isOwner && (
            <div className="flex items-center gap-2 border-b border-amber-400/30 bg-amber-50/80 px-4 py-2 text-xs text-amber-800 dark:bg-amber-900/20 dark:text-amber-300">
              <AlertTriangle size={13} className="flex-shrink-0" />
              <span>
                No trigger node — add a <span className="font-semibold">Manual Trigger</span>,{' '}
                <span className="font-semibold">Webhook Trigger</span>,{' '}
                <span className="font-semibold">Schedule Trigger</span>,{' '}
                <span className="font-semibold">Event Trigger</span>, or{' '}
                <span className="font-semibold">Form Input Trigger</span> to define how this
                workflow starts.
              </span>
              <button
                type="button"
                className="ml-auto flex items-center gap-1 rounded-md border border-amber-400/50 bg-amber-100 px-2 py-0.5 text-xs font-semibold text-amber-800 hover:bg-amber-200 dark:bg-amber-800/30 dark:text-amber-300"
                onClick={() => setPaletteCollapsed(false)}
              >
                <Zap size={10} />
                Add Trigger
              </button>
            </div>
          )}

          {builderMode === 'phases' ? (
            <div className="h-full overflow-y-auto bg-surface-base">
              <WorkflowPhasesEditor workflowId={workflow.id} isOwner={isOwner} />
            </div>
          ) : (
            <>
              <WorkflowBuilderCanvas
                workflowId={workflow.id}
                nodes={nodes}
                edges={edges}
                currentUserId={user?.id}
                nodeConfigOverrides={nodeConfigs}
                onConfigNode={handleConfigNode}
                onEditLens={handleEditLens}
                onEdit={isOwner ? () => setIsEditModalOpen(true) : undefined}
                nodeResults={activeNodeResults}
                configuringNodeId={selectedNodeConfig?.nodeId ?? null}
              />
            </>
          )}
        </div>

        {/* Node config panel — GRASP Polymorphism dispatcher (Lens vs Utility) */}
        {selectedNodeConfig && (
          <WorkflowNodeConfigDispatcher
            nodeId={selectedNodeConfig.nodeId}
            lensId={selectedNodeConfig.lensId}
            versionId={selectedNodeConfig.versionId}
            nodeLabel={selectedNodeConfig.nodeLabel}
            currentUserId={user?.id}
            currentConfig={selectedCurrentConfig}
            nodes={nodes}
            edges={edges}
            nodeResults={activeNodeResults}
            onSave={handleSaveNodeConfig}
            onClose={() => setSelectedNodeConfig(null)}
            onEditLens={handleEditLens}
          />
        )}

        {/* Run results panel — slides in from the right */}
        {showRunPanel && !selectedNodeConfig && (
          <aside className="flex flex-col w-100 flex-shrink-0 border-l border-surface-border bg-surface-base overflow-hidden">
            {/* Panel header with tabs */}
            <div className="flex items-center justify-between px-4 py-2.5 border-b border-surface-border">
              <div className="flex items-center gap-1">
                <button
                  type="button"
                  onClick={() => setRunPanelTab('run')}
                  className={`px-2.5 py-1 rounded-lg text-xs font-medium transition-colors ${
                    runPanelTab === 'run'
                      ? 'bg-surface-raised text-greyscale-900 dark:text-greyscale-50'
                      : 'text-greyscale-400 hover:text-greyscale-700 dark:hover:text-greyscale-200'
                  }`}
                >
                  Current Run
                </button>
                <button
                  type="button"
                  onClick={() => setRunPanelTab('history')}
                  className={`flex items-center gap-1 px-2.5 py-1 rounded-lg text-xs font-medium transition-colors ${
                    runPanelTab === 'history'
                      ? 'bg-surface-raised text-greyscale-900 dark:text-greyscale-50'
                      : 'text-greyscale-400 hover:text-greyscale-700 dark:hover:text-greyscale-200'
                  }`}
                >
                  <History size={11} />
                  History
                  {historyRuns.length > 0 && (
                    <span className="text-[9px] bg-surface-raised rounded-full px-1.5 py-0.5 text-greyscale-500">
                      {historyRuns.length}
                    </span>
                  )}
                </button>
                <button
                  type="button"
                  onClick={() => setRunPanelTab('schedule')}
                  className={`flex items-center gap-1 px-2.5 py-1 rounded-lg text-xs font-medium transition-colors ${
                    runPanelTab === 'schedule'
                      ? 'bg-surface-raised text-greyscale-900 dark:text-greyscale-50'
                      : 'text-greyscale-400 hover:text-greyscale-700 dark:hover:text-greyscale-200'
                  }`}
                >
                  <CalendarClock size={11} />
                  Schedule
                </button>
              </div>
              <Button
                variant="ghost"
                size="sm"
                onClick={() => setShowRunPanel(false)}
                className="!p-1 !h-6 !w-6 text-greyscale-400 hover:text-greyscale-700 transition-colors"
              >
                <X size={14} />
              </Button>
            </div>

            <div className="flex-1 overflow-y-auto">
              {runPanelTab === 'run' ? (
                <>
                  {/* Funding source + model selection */}
                  <div className="px-4 py-3 border-b border-surface-border">
                    <FundingSourceToggle
                      fundingSource={funding.fundingSource}
                      onFundingSourceChange={funding.setFundingSource}
                      selectedKeyRefId={funding.selectedKeyRefId}
                      onKeyRefIdChange={funding.setSelectedKeyRefId}
                      availableKeys={funding.availableKeys}
                      selectedLocalKeyId={funding.selectedLocalKeyId}
                      onLocalKeyIdChange={funding.setSelectedLocalKeyId}
                      availableLocalKeys={funding.localKeys}
                      localKeyAvailability={funding.localKeyAvailability}
                      onAddLocalKey={funding.addLocalKey}
                      onRemoveLocalKey={funding.removeLocalKey}
                      onUpdateLocalKey={funding.updateLocalKey}
                      onPairGateway={funding.pairGateway}
                      onForgetGateway={funding.forgetGateway}
                      onRefreshLocalKeys={funding.refreshLocalKeys}
                      walletBalance={funding.walletBalance}
                      canUseBYOK={funding.canUseBYOK}
                      chainabitState={chainabit.state}
                      chainabitModels={chainabit.models}
                      onChainabitConnect={chainabit.reconnect}
                      providers={providers}
                      isLoadingProviders={modelsLoading}
                      providerModels={providerModels}
                      isLoadingModels={modelsLoading}
                      selectedProviderKey={selectedProviderKey}
                      onProviderChange={handleProviderChange}
                      selectedModelKey={selectedModelKey}
                      onModelChange={handleModelChange}
                    />
                  </div>
                  <WorkflowRootInputsPanel
                    nodes={nodes}
                    edges={edges}
                    onSubmit={(rootInputs) => handleExecuteClick(rootInputs)}
                    isRunning={starting || isRunning}
                    canExecute={canExecute}
                    executeHint={
                      cloudByokNeedsExecutor
                        ? 'Cloud BYOK is not supported for workflow runs in production yet. Switch to Platform credits or a Local key.'
                        : undefined
                    }
                    nodeConfigOverrides={nodeConfigs}
                  />
                  {/* Recovery banner — shown when the run terminated with failure */}
                  {runId &&
                    !isRunning &&
                    (liveRunState?.status === 'failed' || liveRunState?.status === 'timed_out') && (
                      <WorkflowRunRecoveryBanner
                        runStatus={liveRunState!.status}
                        failedNodeLabel={
                          liveRunState?.node_results?.find(
                            (r) => r.status === 'failed' || r.status === 'timed_out'
                          )?.node_label ?? null
                        }
                        errorMessage={
                          liveRunState?.node_results?.find(
                            (r) => r.status === 'failed' || r.status === 'timed_out'
                          )?.error_message ?? null
                        }
                        isRetrying={isRetrying}
                        onRetry={() => {
                          retryRun(runId).catch(() => {})
                          setRunPanelTab('run')
                          setShowRunPanel(true)
                        }}
                      />
                    )}
                  {/* Final output banner — shown above progress when run is complete */}
                  {runId && terminalNodeId && !isRunning && (
                    <WorkflowFinalOutputBanner
                      terminalNodeResult={terminalNodeResult}
                      nodeLabel={terminalNodeLabel}
                      onPostToThread={(text) => handlePostToThread(text, terminalNodeLabel)}
                      onRerunWithContext={handleRerunWithContext}
                    />
                  )}
                  {runId && (
                    <WorkflowProgressView
                      nodes={nodes}
                      edges={edges}
                      nodeResults={nodeResults}
                      terminalNodeId={terminalNodeId}
                      onPostToThread={handlePostToThread}
                      onRerunWithContext={handleRerunWithContext}
                      provenance={liveProvenance}
                      activeNodeId={liveRunState?.active_node_id ?? null}
                      runStartedAt={liveRunState?.started_at ?? null}
                      runCompletedAt={liveRunState?.completed_at ?? null}
                      runStatus={liveRunState?.status ?? null}
                      onOpenFullscreen={() =>
                        navigate(`/workflows/${workflowId}/history/executions/${runId}`)
                      }
                      onConfigureNode={handleConfigNode}
                      isSystemLocked={isSystemLocked}
                    />
                  )}
                </>
              ) : runPanelTab === 'schedule' ? (
                <WorkflowCronPanel workflowId={workflowId} isOwner={isOwner} />
              ) : (
                <>
                  <WorkflowRunHistoryPanel
                    workflowId={workflowId}
                    activeRunId={selectedHistoryRunId}
                    onSelectRun={(id) => setSelectedHistoryRunId(id)}
                  />
                  {selectedHistoryRunId && historyNodeResults.length > 0 && (
                    <WorkflowProgressView
                      nodes={nodes}
                      edges={edges}
                      nodeResults={historyNodeResults}
                      terminalNodeId={terminalNodeId}
                      provenance={historyProvenance}
                      activeNodeId={historyRunState?.active_node_id ?? null}
                      runStartedAt={historyRunState?.started_at ?? null}
                      runCompletedAt={historyRunState?.completed_at ?? null}
                      runStatus={historyRunState?.status ?? null}
                      onOpenFullscreen={() =>
                        navigate(
                          `/workflows/${workflowId}/history/executions/${selectedHistoryRunId}`
                        )
                      }
                    />
                  )}
                </>
              )}
            </div>
          </aside>
        )}

        {/* Dry-run results panel — ephemeral test mode, no history or schedule */}
        {showDryRunPanel && !selectedNodeConfig && (
          <aside className="flex flex-col w-100 flex-shrink-0 border-l border-surface-border bg-surface-base overflow-hidden">
            {/* Panel header */}
            <div className="flex items-center justify-between px-4 py-2.5 border-b border-surface-border">
              <div className="flex items-center gap-2">
                <FlaskConical size={13} className="text-primary-yellow-500" />
                <span className="text-xs font-semibold text-greyscale-900 dark:text-greyscale-50">
                  Test Run
                </span>
                <span className="rounded-full border border-primary-yellow-400/50 bg-primary-yellow-500/10 px-2 py-0.5 text-[9px] font-medium text-primary-yellow-600 dark:text-primary-yellow-300">
                  dry run
                </span>
                {isDryRunning && (
                  <span className="text-[9px] text-greyscale-400 animate-pulse">running…</span>
                )}
              </div>
              <Button
                variant="ghost"
                size="sm"
                onClick={() => {
                  setDryRunNodeResults([])
                  setShowDryRunPanel(false)
                }}
                className="!p-1 !h-6 !w-6 text-greyscale-400 hover:text-greyscale-700 transition-colors"
              >
                <X size={14} />
              </Button>
            </div>

            <div className="flex-1 overflow-y-auto">
              <WorkflowProgressView
                nodes={nodes}
                edges={edges}
                nodeResults={dryRunNodeResults}
                terminalNodeId={null}
                activeNodeId={null}
                runStartedAt={null}
                runCompletedAt={null}
                runStatus={
                  isDryRunning ? 'running' : dryRunNodeResults.length > 0 ? 'completed' : null
                }
                onConfigureNode={handleConfigNode}
              />
            </div>
          </aside>
        )}
      </div>

      {/* ── Workflow edit modal (wizard in edit mode) ────────────────────────── */}
      {isEditModalOpen && (
        <Dialog open={isEditModalOpen} onClose={handleEditClose} maxWidth="max-w-2xl">
          <CreateWorkflowWizard
            editMode
            initialWorkflow={{
              id: workflow.id,
              title: workflow.title,
              description: workflow.description,
              visibility: workflow.visibility ?? 'public',
            }}
            onCreated={handleEditClose}
            onCancel={handleEditClose}
          />
        </Dialog>
      )}

      {/* ── Lens edit modal ─────────────────────────────────────────────────── */}
      <CreateLensModal
        isOpen={lensModal.isOpen}
        onClose={() => {
          lensModal.closeModal()
          setEditingLensId(null)
        }}
        onSubmit={() => lensModal.submit()}
        form={lensModal.form}
        isSubmitting={lensModal.isSubmitting}
        error={lensModal.error}
        isEditMode={lensModal.isEditMode}
        lensId={editingLensId ?? undefined}
      />
    </div>
  )
}
