export type WorkflowNodePosition = {
  x: number
  y: number
}

export type WorkflowHelperLineNode = {
  id: string
  position: WorkflowNodePosition
  width?: number | null
  height?: number | null
  measured?: {
    width?: number | null
    height?: number | null
  }
}

export type WorkflowHelperLine = {
  horizontal?: {
    y: number
    left: number
    width: number
  }
  vertical?: {
    x: number
    top: number
    height: number
  }
}

export type WorkflowHelperLineResult = WorkflowHelperLine & {
  position: WorkflowNodePosition
}

export type WorkflowEditorNode = {
  id: string
  type?: string
  position: WorkflowNodePosition
  data?: {
    nodeType?: string
    name?: string
    label?: string
    config?: Record<string, unknown>
    inputs?: Record<string, WorkflowVariableSelector>
  }
}

export type WorkflowEditorEdge = {
  id: string
  source: string
  target: string
  data?: {
    condition?: {
      expression?: string
      left?: WorkflowVariableSelector
      operator?: string
      right?: unknown
    }
  }
}

export type WorkflowDraft = {
  nodes: WorkflowEditorNode[]
  edges: WorkflowEditorEdge[]
}

export type WorkflowDefinition = {
  schemaVersion: number
  entryNodeId: string
  nodes: {
    id: string
    type: string
    name: string
    position: WorkflowNodePosition
    config: Record<string, unknown>
    inputs?: Record<string, WorkflowVariableSelector>
  }[]
  edges: {
    id: string
    source: string
    target: string
    condition?: {
      expression?: string
      left?: WorkflowVariableSelector
      operator?: string
      right?: unknown
    }
  }[]
}

export type WorkflowVariableType =
  | "string"
  | "integer"
  | "boolean"
  | "object"
  | "array<string>"
  | "array<int>"
  | "array<object>"
  | "any"

export type WorkflowVariableSelector = {
  nodeId: string
  field: string
}

export type WorkflowVariableSpec = {
  name: string
  type: WorkflowVariableType
  required?: boolean
  description?: string
}

export type WorkflowNodeSpec = {
  type: string
  title?: string
  description?: string
  inputSchema?: WorkflowVariableSpec[]
  outputSchema?: WorkflowVariableSpec[]
  defaultInputs?: Record<string, WorkflowVariableSelector>
}

export type WorkflowVariableRef = {
  nodeId: string
  nodeName: string
  field: string
  type: string
  description: string
}

export type WorkflowDraftValidation = {
  valid: boolean
  errors: string[]
}

export type WorkflowHistory<T> = {
  past: T[]
  future: T[]
  limit: number
}

export type WorkflowHistoryChange<T> = {
  history: WorkflowHistory<T>
  snapshot: T
}

const helperLineAlignmentThreshold = 6
const defaultWorkflowHistoryLimit = 50

function cloneHistorySnapshot<T>(snapshot: T): T {
  return JSON.parse(JSON.stringify(snapshot)) as T
}

export function createWorkflowHistory<T>(limit = defaultWorkflowHistoryLimit): WorkflowHistory<T> {
  return {
    past: [],
    future: [],
    limit,
  }
}

export function pushWorkflowHistory<T>(
  history: WorkflowHistory<T>,
  snapshot: T
): WorkflowHistory<T> {
  const past = [...history.past, cloneHistorySnapshot(snapshot)]
  return {
    past: past.slice(Math.max(0, past.length - history.limit)),
    future: [],
    limit: history.limit,
  }
}

export function undoWorkflowHistory<T>(
  history: WorkflowHistory<T>,
  current: T
): WorkflowHistoryChange<T> | null {
  const snapshot = history.past.at(-1)
  if (!snapshot) {
    return null
  }
  return {
    snapshot: cloneHistorySnapshot(snapshot),
    history: {
      past: history.past.slice(0, -1),
      future: [cloneHistorySnapshot(current), ...history.future],
      limit: history.limit,
    },
  }
}

export function redoWorkflowHistory<T>(
  history: WorkflowHistory<T>,
  current: T
): WorkflowHistoryChange<T> | null {
  const snapshot = history.future[0]
  if (!snapshot) {
    return null
  }
  const past = [...history.past, cloneHistorySnapshot(current)]
  return {
    snapshot: cloneHistorySnapshot(snapshot),
    history: {
      past: past.slice(Math.max(0, past.length - history.limit)),
      future: history.future.slice(1),
      limit: history.limit,
    },
  }
}

function getNodeSize(node: WorkflowHelperLineNode) {
  return {
    width: node.measured?.width ?? node.width ?? 0,
    height: node.measured?.height ?? node.height ?? 0,
  }
}

function getNodeAnchorValues(node: WorkflowHelperLineNode) {
  const size = getNodeSize(node)
  return {
    x: [
      node.position.x,
      node.position.x + size.width / 2,
      node.position.x + size.width,
    ],
    y: [
      node.position.y,
      node.position.y + size.height / 2,
      node.position.y + size.height,
    ],
  }
}

function getNearestAlignment(
  axis: "x" | "y",
  nodes: WorkflowHelperLineNode[],
  draggingNode: WorkflowHelperLineNode
) {
  const draggingAnchors = getNodeAnchorValues(draggingNode)[axis]
  let nearest:
    | {
        diff: number
        targetValue: number
        candidate: WorkflowHelperLineNode
      }
    | undefined

  for (const candidate of nodes) {
    if (candidate.id === draggingNode.id) {
      continue
    }
    const candidateAnchors = getNodeAnchorValues(candidate)[axis]
    for (const draggingAnchor of draggingAnchors) {
      for (const candidateAnchor of candidateAnchors) {
        const diff = candidateAnchor - draggingAnchor
        if (Math.abs(diff) > helperLineAlignmentThreshold) {
          continue
        }
        if (!nearest || Math.abs(diff) < Math.abs(nearest.diff)) {
          nearest = {
            diff,
            targetValue: candidateAnchor,
            candidate,
          }
        }
      }
    }
  }

  return nearest
}

export function calculateWorkflowHelperLines(
  nodes: WorkflowHelperLineNode[],
  draggingNode: WorkflowHelperLineNode
): WorkflowHelperLineResult {
  const xAlignment = getNearestAlignment("x", nodes, draggingNode)
  const yAlignment = getNearestAlignment("y", nodes, draggingNode)
  const position = {
    x: draggingNode.position.x + (xAlignment?.diff ?? 0),
    y: draggingNode.position.y + (yAlignment?.diff ?? 0),
  }
  const draggingSize = getNodeSize(draggingNode)
  const snappedDraggingNode = {
    ...draggingNode,
    position,
  }
  const result: WorkflowHelperLineResult = {
    position,
  }

  if (yAlignment) {
    const candidateSize = getNodeSize(yAlignment.candidate)
    const left = Math.min(snappedDraggingNode.position.x, yAlignment.candidate.position.x)
    const right = Math.max(
      snappedDraggingNode.position.x + draggingSize.width,
      yAlignment.candidate.position.x + candidateSize.width
    )
    result.horizontal = {
      y: yAlignment.targetValue,
      left,
      width: right - left,
    }
  }

  if (xAlignment) {
    const candidateSize = getNodeSize(xAlignment.candidate)
    const top = Math.min(snappedDraggingNode.position.y, xAlignment.candidate.position.y)
    const bottom = Math.max(
      snappedDraggingNode.position.y + draggingSize.height,
      xAlignment.candidate.position.y + candidateSize.height
    )
    result.vertical = {
      x: xAlignment.targetValue,
      top,
      height: bottom - top,
    }
  }

  return result
}

export function validateWorkflowDraft(
  draft: WorkflowDraft,
  nodeSpecs: WorkflowNodeSpec[] = []
): WorkflowDraftValidation {
  const errors: string[] = []
  const nodeIds = new Set<string>()
  let startCount = 0
  let endCount = 0

  for (const node of draft.nodes) {
    const id = node.id.trim()
    if (!id) {
      errors.push("node id is required")
      continue
    }
    if (nodeIds.has(id)) {
      errors.push(`duplicate node id: ${id}`)
    }
    nodeIds.add(id)
    const nodeType = node.data?.nodeType ?? node.type
    if (nodeType === "start") {
      startCount += 1
    }
    if (nodeType === "end") {
      endCount += 1
    }
  }

  if (startCount !== 1) {
    errors.push("workflow must contain exactly one start node")
  }
  if (endCount < 1) {
    errors.push("workflow must contain at least one end node")
  }

  const edgeIds = new Set<string>()
  const conditionalSources = new Set<string>()
  const defaultSources = new Set<string>()
  for (const edge of draft.edges) {
    const id = edge.id.trim()
    if (!id) {
      errors.push("edge id is required")
    } else if (edgeIds.has(id)) {
      errors.push(`duplicate edge id: ${id}`)
    }
    edgeIds.add(id)
    if (!nodeIds.has(edge.source)) {
      errors.push(`edge source node does not exist: ${edge.source}`)
    }
    if (!nodeIds.has(edge.target)) {
      errors.push(`edge target node does not exist: ${edge.target}`)
    }
    if (edge.data?.condition) {
      conditionalSources.add(edge.source)
      if (!edge.data.condition.left?.nodeId || !edge.data.condition.left.field) {
        errors.push(`edge ${edge.id} condition left variable is required`)
      }
      if (!edge.data.condition.operator) {
        errors.push(`edge ${edge.id} condition operator is required`)
      }
    } else {
      defaultSources.add(edge.source)
    }
  }
  for (const source of conditionalSources) {
    if (!defaultSources.has(source)) {
      errors.push(`node ${source} conditional branch must include a default edge`)
    }
  }

  for (const node of draft.nodes) {
    const nodeType = node.data?.nodeType ?? node.type ?? ""
    const spec = getNodeSpec(nodeSpecs, nodeType)
    if (!spec) {
      continue
    }
    for (const input of getRequiredInputs(spec)) {
      const selector = node.data?.inputs?.[input.name]
      if (!selector?.nodeId || !selector.field) {
        const nodeName = node.data?.name ?? spec.title ?? node.id
        errors.push(`${nodeName} 缺少必填输入「${input.name}」，请选择上游节点的输出变量。`)
      }
    }
  }

  return {
    valid: errors.length === 0,
    errors,
  }
}

export function toApiDefinition(draft: WorkflowDraft): WorkflowDefinition {
  const startNode = draft.nodes.find((node) => (node.data?.nodeType ?? node.type) === "start")
  return {
    schemaVersion: 1,
    entryNodeId: startNode?.id ?? "",
    nodes: draft.nodes.map((node) => ({
      id: node.id,
      type: node.data?.nodeType ?? node.type ?? "",
      name: node.data?.name ?? node.type ?? node.id,
      position: {
        x: node.position.x,
        y: node.position.y,
      },
      config: node.data?.config ?? {},
      ...(node.data?.inputs ? { inputs: node.data.inputs } : {}),
    })),
    edges: draft.edges.map((edge) => ({
      id: edge.id,
      source: edge.source,
      target: edge.target,
      ...(edge.data?.condition
        ? {
            condition: {
              ...(edge.data.condition.expression ? { expression: edge.data.condition.expression } : {}),
              ...(edge.data.condition.left ? { left: edge.data.condition.left } : {}),
              ...(edge.data.condition.operator ? { operator: edge.data.condition.operator } : {}),
              ...(edge.data.condition.right !== undefined ? { right: edge.data.condition.right } : {}),
            },
          }
        : {}),
    })),
  }
}

export function fromApiDefinition(definition: WorkflowDefinition): WorkflowDraft {
  return {
    nodes: (definition.nodes ?? []).map((node) => ({
      id: node.id,
      type: node.type,
      position: node.position ?? { x: 0, y: 0 },
      data: {
        nodeType: node.type,
        name: node.name,
        config: node.config ?? {},
        inputs: node.inputs ?? {},
      },
    })),
    edges: (definition.edges ?? []).map((edge) => ({
      id: edge.id,
      source: edge.source,
      target: edge.target,
      data: edge.condition ? { condition: edge.condition } : undefined,
    })),
  }
}

export function applyAutoInputMappings(
  draft: WorkflowDraft,
  sourceNodeId: string,
  targetNodeId: string,
  nodeSpecs: WorkflowNodeSpec[]
): WorkflowDraft {
  const sourceNode = draft.nodes.find((node) => node.id === sourceNodeId)
  const targetNode = draft.nodes.find((node) => node.id === targetNodeId)
  if (!sourceNode || !targetNode) {
    return draft
  }
  const sourceSpec = getNodeSpec(nodeSpecs, sourceNode.data?.nodeType ?? sourceNode.type ?? "")
  const targetSpec = getNodeSpec(nodeSpecs, targetNode.data?.nodeType ?? targetNode.type ?? "")
  if (!sourceSpec || !targetSpec) {
    return draft
  }
  const nextInputs = { ...(targetNode.data?.inputs ?? {}) }
  let changed = false

  for (const input of targetSpec.inputSchema ?? []) {
    if (nextInputs[input.name]) {
      continue
    }
    const output = findPreferredOutput(input.name, input.type, sourceSpec.outputSchema ?? [])
    if (!output) {
      continue
    }
    nextInputs[input.name] = { nodeId: sourceNodeId, field: output.name }
    changed = true
  }

  if (!changed) {
    return draft
  }

  return {
    ...draft,
    nodes: draft.nodes.map((node) =>
      node.id === targetNodeId
        ? {
            ...node,
            data: {
              ...node.data,
              inputs: nextInputs,
            },
          }
        : node
    ),
  }
}

export function createWorkflowNodeFromSpec(
  spec: WorkflowNodeSpec,
  existingNodes: Pick<WorkflowEditorNode, "id">[],
  position: WorkflowNodePosition
): WorkflowEditorNode {
  const id = uniqueNodeId(existingNodes, spec.type)
  return {
    id,
    type: "workflowNode",
    position,
    data: {
      nodeType: spec.type,
      name: spec.title ?? spec.type,
      label: spec.title ?? spec.type,
      config: {},
      inputs: spec.defaultInputs ?? {},
    },
  }
}

function uniqueNodeId(existingNodes: Pick<WorkflowEditorNode, "id">[], nodeType: string) {
  let nextIndex = existingNodes.length + 1
  let id = `${nodeType}_${nextIndex}`
  while (existingNodes.some((node) => node.id === id)) {
    nextIndex += 1
    id = `${nodeType}_${nextIndex}`
  }
  return id
}

function findPreferredOutput(
  inputName: string,
  inputType: WorkflowVariableType,
  outputs: WorkflowVariableSpec[]
): WorkflowVariableSpec | undefined {
  const preferred = preferredOutputName(inputName)
  if (preferred) {
    const exact = outputs.find((output) => output.name === preferred && variableTypesCompatible(inputType, output.type))
    if (exact) {
      return exact
    }
  }
  const sameName = outputs.find((output) => output.name === inputName && variableTypesCompatible(inputType, output.type))
  if (sameName) {
    return sameName
  }
  return outputs.find((output) => variableTypesCompatible(inputType, output.type))
}

function preferredOutputName(inputName: string): string {
  switch (inputName) {
    case "query":
    case "userMessage":
    case "issue":
    case "prompt":
      return "userMessage"
    case "knowledgeItems":
      return "items"
    case "replyText":
      return "replyText"
    case "confirmed":
      return "confirmed"
    case "ticketDraft":
      return "ticketDraft"
    case "reason":
      return "reason"
    default:
      return ""
  }
}

function variableTypesCompatible(input: WorkflowVariableType, output: WorkflowVariableType): boolean {
  return input === "any" || output === "any" || input === output
}

export function getNodeSpec(
  nodeSpecs: WorkflowNodeSpec[],
  nodeType: string
): WorkflowNodeSpec | undefined {
  return nodeSpecs.find((spec) => spec.type === nodeType)
}

export function getRequiredInputs(spec: WorkflowNodeSpec | undefined): WorkflowVariableSpec[] {
  return (spec?.inputSchema ?? []).filter((item) => item.required)
}

export function getAvailableVariables(
  draft: WorkflowDraft,
  nodeId: string,
  nodeSpecs: WorkflowNodeSpec[]
): WorkflowVariableRef[] {
  const ancestors = collectAncestorNodeIds(draft, nodeId)
  const nodesById = new Map(draft.nodes.map((node) => [node.id, node]))
  const variables: WorkflowVariableRef[] = []

  for (const sourceNodeId of ancestors) {
    const sourceNode = nodesById.get(sourceNodeId)
    if (!sourceNode) {
      continue
    }
    const nodeType = sourceNode.data?.nodeType ?? sourceNode.type ?? ""
    const spec = getNodeSpec(nodeSpecs, nodeType)
    for (const output of spec?.outputSchema ?? []) {
      variables.push({
        nodeId: sourceNode.id,
        nodeName: sourceNode.data?.name ?? spec?.title ?? sourceNode.id,
        field: output.name,
        type: output.type,
        description: output.description ?? "",
      })
    }
  }

  return variables
}

function collectAncestorNodeIds(draft: WorkflowDraft, nodeId: string): string[] {
  const incoming = new Map<string, string[]>()
  for (const edge of draft.edges) {
    const sources = incoming.get(edge.target) ?? []
    sources.push(edge.source)
    incoming.set(edge.target, sources)
  }

  const visited = new Set<string>()
  const ordered: string[] = []

  function visit(current: string) {
    for (const source of incoming.get(current) ?? []) {
      if (visited.has(source)) {
        continue
      }
      visited.add(source)
      visit(source)
      ordered.push(source)
    }
  }

  visit(nodeId)
  return ordered
}
