/** Resolve node displayName from dynamic ref like `aesthetic.output`. */
export function nodeDisplayNameFromRef(ref, pipeline) {
  const nodes = pipeline?.nodes || [];
  const node = nodes.find((n) => ref.startsWith(`${n.id}.`));
  return node?.displayName || ref.split('.')[0];
}

/** Extract pipeline node id from a step output ref (`nodeId.output`). */
export function parseNodeRef(ref) {
  if (!ref || typeof ref !== 'string') return null;
  const m = ref.match(/^([^.]+)\.output$/);
  return m ? m[1] : null;
}

/** Locale-aware display name compare (Chinese + numeric tie-break). */
export function compareNodeDisplayNames(a, b) {
  const nameA = a?.displayName || '';
  const nameB = b?.displayName || '';
  return nameA.localeCompare(nameB, 'zh-CN', {
    numeric: true,
    sensitivity: 'base',
  });
}

/**
 * Topological layer sort by `inputs[].ref` dependencies.
 * Same layer → display name (zh-CN localeCompare).
 */
export function sortNodesByLogic(pipeline) {
  const nodes = pipeline?.nodes || [];
  if (nodes.length <= 1) return [...nodes];

  const edges = buildNodeDependencyEdges(pipeline);
  const depth = Object.fromEntries(nodes.map((n) => [n.id, 0]));
  const pipelineIndex = Object.fromEntries(nodes.map((n, i) => [n.id, i]));

  let changed = true;
  while (changed) {
    changed = false;
    edges.forEach(({ from, to }) => {
      const next = (depth[from] || 0) + 1;
      if (next > (depth[to] || 0)) {
        depth[to] = next;
        changed = true;
      }
    });
  }

  const layers = {};
  nodes.forEach((n) => {
    const d = depth[n.id] || 0;
    if (!layers[d]) layers[d] = [];
    layers[d].push(n);
  });

  const sorted = [];
  Object.keys(layers)
    .map(Number)
    .sort((a, b) => a - b)
    .forEach((d) => {
      layers[d].sort((a, b) => {
        const byName = compareNodeDisplayNames(a, b);
        if (byName !== 0) return byName;
        return (pipelineIndex[a.id] ?? 0) - (pipelineIndex[b.id] ?? 0);
      });
      sorted.push(...layers[d]);
    });

  return sorted;
}

/** Order node ids by manual pipeline order snapshot. */
export function orderNodesByIds(nodes, orderIds) {
  const orderMap = Object.fromEntries((orderIds || []).map((id, i) => [id, i]));
  return [...(nodes || [])].sort((a, b) => {
    const ia = orderMap[a.id] ?? Number.MAX_SAFE_INTEGER;
    const ib = orderMap[b.id] ?? Number.MAX_SAFE_INTEGER;
    if (ia !== ib) return ia - ib;
    return compareNodeDisplayNames(a, b);
  });
}

/** Dependency edges derived from `inputs[].ref` between pipeline nodes. */
export function buildNodeDependencyEdges(pipeline) {
  const nodes = pipeline?.nodes || [];
  const nodeIds = new Set(nodes.map((n) => n.id));
  const edges = [];
  const edgeKeys = new Set();

  nodes.forEach((node) => {
    (node.inputs || []).forEach((inp) => {
      const src = parseNodeRef(inp.ref);
      if (!src || src === node.id || !nodeIds.has(src)) return;
      const key = `${src}->${node.id}`;
      if (edgeKeys.has(key)) return;
      edgeKeys.add(key);
      edges.push({ from: src, to: node.id });
    });
  });

  return edges;
}

/** True if adding `fromNodeId → toNodeId` would close a dependency cycle. */
export function wouldCreateDependencyCycle(pipeline, toNodeId, fromNodeId) {
  if (!toNodeId || !fromNodeId || toNodeId === fromNodeId) return true;

  const edges = buildNodeDependencyEdges(pipeline);
  const visited = new Set();
  const stack = [toNodeId];

  while (stack.length) {
    const cur = stack.pop();
    if (cur === fromNodeId) return true;
    if (visited.has(cur)) continue;
    visited.add(cur);
    edges.filter((e) => e.from === cur).forEach((e) => stack.push(e.to));
  }

  return false;
}

/** Find dependency cycles in the pipeline (node id paths). */
export function detectReferenceCycles(pipeline) {
  const nodes = pipeline?.nodes || [];
  const nodeIds = nodes.map((n) => n.id);
  const adj = Object.fromEntries(nodeIds.map((id) => [id, []]));

  buildNodeDependencyEdges(pipeline).forEach(({ from, to }) => {
    adj[from].push(to);
  });

  const cycles = [];
  const visited = new Set();
  const stack = new Set();
  const path = [];

  const dfs = (nodeId) => {
    visited.add(nodeId);
    stack.add(nodeId);
    path.push(nodeId);

    (adj[nodeId] || []).forEach((next) => {
      if (!visited.has(next)) {
        dfs(next);
      } else if (stack.has(next)) {
        const start = path.indexOf(next);
        if (start >= 0) {
          cycles.push([...path.slice(start), next]);
        }
      }
    });

    path.pop();
    stack.delete(nodeId);
  };

  nodeIds.forEach((id) => {
    if (!visited.has(id)) dfs(id);
  });

  return cycles;
}

export function formatCycleWarning(cycles, pipeline) {
  if (!cycles.length) return null;
  const nameOf = (id) =>
    pipeline?.nodes?.find((n) => n.id === id)?.displayName || id;
  const first = cycles[0];
  const chain = first.map(nameOf).join(' → ');
  return `检测到循环引用：${chain}`;
}

/** Worldbook steps that may be referenced without creating a cycle. */
export function getSelectableStepOutputRefs(pipeline, selectedNode) {
  if (!selectedNode?.id) return [];

  return (pipeline?.nodes || []).filter(
    (n) =>
      n.enabled !== false &&
      n.skillId === 'studio.worldbook_entry' &&
      n.id !== selectedNode.id &&
      !wouldCreateDependencyCycle(pipeline, selectedNode.id, n.id)
  );
}

export function buildStepOutputRef(nodeId) {
  return `${nodeId}.output`;
}

export function stepOutputLabel(displayName) {
  return `${displayName} · 世界书条目`;
}

export function dynamicChipLabel(ref, pipeline, fallbackLabel) {
  const name = nodeDisplayNameFromRef(ref, pipeline);
  if (name && name !== ref.split('.')[0]) return stepOutputLabel(name);
  return fallbackLabel || stepOutputLabel(name);
}

export function dynamicTooltip(ref, pipeline) {
  const name = nodeDisplayNameFromRef(ref, pipeline);
  return `引用前序步骤「${name}」的最终世界书条目 · ${ref}`;
}

export const DEFAULT_THINKING_PROMPT = `====== 思考流程 ======
Step1: 简短确认任务性质（新设计/修改）
Step2: 阅读绑定角色/世界书与上文引用
Step3: 按步骤目标起草世界书条目
Step4: 对照评价维度自检并优化表述

（核心目的、评价标准与优化建议由系统在运行时自动注入，无需在此填写）`;

export const AUTO_INJECTED_CONTEXT_ITEMS = [
  {
    id: 'currentProduct',
    label: '目前产物',
    description:
      '当前步骤已生成的世界书条目草稿或最新版本，供模型在迭代修改时对照与延续。',
  },
  {
    id: 'thinkingFlow',
    label: '思考流程',
    description:
      '本步骤配置的 thinkingPrompt，引导模型按既定步骤推理与自检。',
  },
  {
    id: 'coreGoal',
    label: '核心目的',
    description:
      '本步骤的 stepGoal（步骤目标），明确本步要产出的内容与边界。',
  },
  {
    id: 'scoringCriteria',
    label: '评价标准与优化建议',
    description:
      '本步骤启用的 scoring 评价维度及准则，用于模型自检与优化表述。',
  },
];

export const AUTO_INJECTED_CONTEXT_LABELS = AUTO_INJECTED_CONTEXT_ITEMS.map(
  (item) => item.label
);

const NODE_W = 128;
const NODE_H = 52;
const GAP_X = 48;
const GAP_Y = 64;
const PAD = 24;

function verticalEdgePath(from, to) {
  const x1 = from.x + NODE_W / 2;
  const y1 = from.y + NODE_H;
  const x2 = to.x + NODE_W / 2;
  const y2 = to.y;
  const dy = Math.max(24, (y2 - y1) * 0.45);
  return `M ${x1} ${y1} C ${x1} ${y1 + dy}, ${x2} ${y2 - dy}, ${x2} ${y2}`;
}

/** Layout nodes as a vertical dependency tree for SVG progress graphs. */
export function buildGraphLayout(pipeline, nodeStates, options = {}) {
  const vertical = options.vertical !== false;
  const nodes = (pipeline?.nodes || []).filter((n) => n.enabled !== false);
  const stateMap = Object.fromEntries(
    (nodeStates || []).map((s) => [s.nodeId, s])
  );
  const pipelineIndex = Object.fromEntries(
    nodes.map((n, i) => [n.id, i])
  );

  const edges = buildNodeDependencyEdges(pipeline);

  const depth = {};
  nodes.forEach((n) => {
    depth[n.id] = 0;
  });

  let changed = true;
  while (changed) {
    changed = false;
    edges.forEach(({ from, to }) => {
      const next = (depth[from] || 0) + 1;
      if (next > (depth[to] || 0)) {
        depth[to] = next;
        changed = true;
      }
    });
  }

  const layers = {};
  nodes.forEach((n) => {
    const d = depth[n.id] || 0;
    if (!layers[d]) layers[d] = [];
    layers[d].push(n);
  });

  Object.values(layers).forEach((layer) => {
    layer.sort((a, b) => (pipelineIndex[a.id] ?? 0) - (pipelineIndex[b.id] ?? 0));
  });

  const positions = {};
  Object.keys(layers)
    .map(Number)
    .sort((a, b) => a - b)
    .forEach((d) => {
      const layer = layers[d];
      layer.forEach((node, col) => {
        if (vertical) {
          positions[node.id] = {
            x: PAD + col * (NODE_W + GAP_X),
            y: PAD + d * (NODE_H + GAP_Y),
          };
        } else {
          positions[node.id] = {
            x: PAD + d * (NODE_W + GAP_X),
            y: PAD + col * (NODE_H + GAP_Y),
          };
        }
      });
    });

  const maxLayer = Math.max(0, ...Object.keys(layers).map(Number));
  const maxCols = Math.max(1, ...Object.values(layers).map((l) => l.length));

  const width = vertical
    ? PAD * 2 + maxCols * NODE_W + (maxCols - 1) * GAP_X
    : PAD * 2 + (maxLayer + 1) * NODE_W + maxLayer * GAP_X;
  const height = vertical
    ? PAD * 2 + (maxLayer + 1) * NODE_H + maxLayer * GAP_Y
    : PAD * 2 + maxCols * NODE_H + (maxCols - 1) * GAP_Y;

  return {
    nodes: nodes.map((n) => ({
      id: n.id,
      label: n.displayName,
      status: stateMap[n.id]?.status || 'pending',
      x: positions[n.id]?.x ?? PAD,
      y: positions[n.id]?.y ?? PAD,
    })),
    edges,
    width,
    height,
    nodeW: NODE_W,
    nodeH: NODE_H,
    edgePath: vertical ? verticalEdgePath : null,
  };
}
