import { stableDigest, truncateText } from "@best-agent/foundation";
import type {
  AgentRun,
  ArtifactRef,
  CapabilitySummary,
  Item,
  NodeContract,
  NodePacket,
  PlanProjection,
  PlanTodo,
  ThreadEvidenceRecord,
  ThreadId,
  TranscriptExchange,
  UnresolvedBlocker,
} from "@best-agent/protocol";

export interface ApprovedExecutionNodePacketInput {
  items: Item[];
  latestRun?: AgentRun;
  nodeContract?: NodeContract;
  plan: PlanProjection;
  recentEvidence: ThreadEvidenceRecord[];
}

export interface PlanModeNodePacketInput {
  items: Item[];
  latestRun?: AgentRun;
  nodeContract?: NodeContract;
  plan: PlanProjection;
  recentEvidence: ThreadEvidenceRecord[];
}

export interface InitialPlanModeNodePacketInput {
  items: Item[];
  latestRun?: AgentRun;
  recentEvidence: ThreadEvidenceRecord[];
  threadId: ThreadId;
}

export interface OrdinaryTurnNodePacketInput {
  activePaths?: string[];
  currentObjective?: string;
  items: Item[];
  latestRun?: AgentRun;
  recentEvidence: ThreadEvidenceRecord[];
  threadId: ThreadId;
}

const MAX_NODE_PACKET_EVIDENCE = 12;

export function selectNodePacketEvidence(
  evidence: ThreadEvidenceRecord[],
  preferredEvidenceIds: string[] = [],
): ThreadEvidenceRecord[] {
  const preferred = new Set(preferredEvidenceIds);
  return [...evidence]
    .filter((record) => record.payload.indexable !== false)
    .sort((left, right) => {
      const scoreDelta = evidencePriority(right, preferred) - evidencePriority(left, preferred);
      if (scoreDelta !== 0) {
        return scoreDelta;
      }
      return Date.parse(right.createdAt) - Date.parse(left.createdAt);
    })
    .slice(0, MAX_NODE_PACKET_EVIDENCE)
    .sort((left, right) => Date.parse(left.createdAt) - Date.parse(right.createdAt));
}

export function buildApprovedExecutionNodePacket(
  input: ApprovedExecutionNodePacketInput,
): NodePacket {
  const unresolvedTodos = input.plan.todos.filter((todo) => isUnresolvedTodoStatus(todo.status));
  const activeTodo =
    unresolvedTodos.find((todo) => todo.status === "in_progress") ?? unresolvedTodos[0];
  const nodeId = input.nodeContract?.nodeId ?? activeTodo?.id ?? input.plan.rootNodeId ?? "root";
  const nodeContract =
    input.nodeContract ?? buildDefaultNodeContract(nodeId, input.plan, activeTodo);
  const recentEvidence = selectNodePacketEvidence(input.recentEvidence, input.plan.evidenceRefs);
  const recentEvidenceIds = recentEvidence.map((record) => record.evidenceId);
  const relevantEvidenceIds = uniqueValues([...input.plan.evidenceRefs, ...recentEvidenceIds]);
  // Plan-preferred evidence that didn't make the top-12 still contributes file paths/artifact refs
  const planOnlyEvidence = input.recentEvidence.filter(
    (record) =>
      input.plan.evidenceRefs.includes(record.evidenceId) &&
      !recentEvidenceIds.includes(record.evidenceId),
  );
  const evidenceForPaths = [...recentEvidence, ...planOnlyEvidence];

  return {
    artifactRefs: collectArtifactRefs(evidenceForPaths),
    boundedTranscriptSuffix: buildBoundedTranscriptSuffix(input.items),
    capabilitySummary: buildCapabilitySummary(nodeId, nodeContract),
    continuitySnapshot: {
      frontierNodeIds: unresolvedTodos.map((todo) => todo.id),
      pendingApprovalNodeIds: [],
      pendingJoinNodeIds: [],
      pendingWaitNodeIds:
        input.latestRun && isWaitingRun(input.latestRun) && input.latestRun.waitReason !== "none"
          ? [nodeId]
          : [],
      recentEvidenceIds,
      touchedPaths: uniqueValues([
        ...input.plan.affectedPaths,
        ...evidenceForPaths.flatMap((record) => record.relatedFilePaths ?? []),
      ]),
      unresolvedNodeIds: unresolvedTodos.map((todo) => todo.id),
    },
    graphId: input.plan.graphId,
    graphRevision: input.plan.graphRevision,
    nodeContract,
    nodeId,
    nodeObjective: activeTodo?.content ?? summarizePlanObjective(input.plan),
    relevantEvidenceIds,
    unresolvedBlockers: buildUnresolvedBlockers(nodeId, unresolvedTodos, input.latestRun),
  };
}

export function buildPlanModeNodePacket(input: PlanModeNodePacketInput): NodePacket {
  const unresolvedTodos = input.plan.todos.filter((todo) => isUnresolvedTodoStatus(todo.status));
  const activeTodo =
    unresolvedTodos.find((todo) => todo.status === "in_progress") ?? unresolvedTodos[0];
  const nodeId =
    input.nodeContract?.nodeId ?? activeTodo?.id ?? input.plan.rootNodeId ?? "plan-root";
  const nodeContract = input.nodeContract ?? buildPlanModeNodeContract(nodeId, input.plan);
  const recentEvidence = selectNodePacketEvidence(input.recentEvidence, input.plan.evidenceRefs);
  const recentEvidenceIds = recentEvidence.map((record) => record.evidenceId);
  const relevantEvidenceIds = uniqueValues([...input.plan.evidenceRefs, ...recentEvidenceIds]);
  // Plan-preferred evidence that didn't make the top-12 still contributes file paths/artifact refs
  const planOnlyEvidence = input.recentEvidence.filter(
    (record) =>
      input.plan.evidenceRefs.includes(record.evidenceId) &&
      !recentEvidenceIds.includes(record.evidenceId),
  );
  const evidenceForPaths = [...recentEvidence, ...planOnlyEvidence];

  return {
    artifactRefs: collectArtifactRefs(evidenceForPaths),
    boundedTranscriptSuffix: buildBoundedTranscriptSuffix(input.items),
    capabilitySummary: buildCapabilitySummary(nodeId, nodeContract),
    continuitySnapshot: {
      frontierNodeIds: unresolvedTodos.map((todo) => todo.id),
      pendingApprovalNodeIds: input.plan.status === "review_ready" ? [nodeId] : [],
      pendingJoinNodeIds: [],
      pendingWaitNodeIds:
        input.latestRun && isWaitingRun(input.latestRun) && input.latestRun.waitReason !== "none"
          ? [nodeId]
          : [],
      recentEvidenceIds,
      touchedPaths: uniqueValues([
        ...input.plan.affectedPaths,
        ...evidenceForPaths.flatMap((record) => record.relatedFilePaths ?? []),
      ]),
      unresolvedNodeIds: unresolvedTodos.map((todo) => todo.id),
    },
    graphId: input.plan.graphId,
    graphRevision: input.plan.graphRevision,
    nodeContract,
    nodeId,
    nodeObjective: `Review or revise the thread plan for ${input.plan.title}.`,
    relevantEvidenceIds,
    unresolvedBlockers: buildUnresolvedBlockers(nodeId, unresolvedTodos, input.latestRun),
  };
}

export function buildInitialPlanModeNodePacket(input: InitialPlanModeNodePacketInput): NodePacket {
  const nodeId = "plan-root";
  const nodeContract = buildPlanModeNodeContract(nodeId);
  const recentEvidence = selectNodePacketEvidence(input.recentEvidence);
  const recentEvidenceIds = recentEvidence.map((record) => record.evidenceId);

  return {
    artifactRefs: collectArtifactRefs(recentEvidence),
    boundedTranscriptSuffix: buildBoundedTranscriptSuffix(input.items),
    capabilitySummary: buildCapabilitySummary(nodeId, nodeContract),
    continuitySnapshot: {
      frontierNodeIds: [],
      pendingApprovalNodeIds: [],
      pendingJoinNodeIds: [],
      pendingWaitNodeIds:
        input.latestRun && isWaitingRun(input.latestRun) && input.latestRun.waitReason !== "none"
          ? [nodeId]
          : [],
      recentEvidenceIds,
      touchedPaths: uniqueValues(recentEvidence.flatMap((record) => record.relatedFilePaths ?? [])),
      unresolvedNodeIds: [],
    },
    graphId: `${input.threadId}:plan-authoring`,
    graphRevision: 0,
    nodeContract,
    nodeId,
    nodeObjective: "Author the initial thread plan.",
    relevantEvidenceIds: recentEvidenceIds,
    unresolvedBlockers: buildUnresolvedBlockers(nodeId, [], input.latestRun),
  };
}

export function buildOrdinaryTurnNodePacket(input: OrdinaryTurnNodePacketInput): NodePacket {
  const nodeId = "turn-root";
  const graphId = `${input.threadId}:ordinary-turn`;
  const recentEvidence = selectNodePacketEvidence(input.recentEvidence);
  const recentEvidenceIds = recentEvidence.map((record) => record.evidenceId);
  const activePaths = uniqueValues([
    ...(input.activePaths ?? []),
    ...recentEvidence.flatMap((record) => record.relatedFilePaths ?? []),
  ]);
  const nodeContract = buildOrdinaryTurnNodeContract(nodeId, activePaths);
  let latestUserObjective = input.currentObjective?.trim();
  if (!latestUserObjective) {
    for (let index = input.items.length - 1; index >= 0; index -= 1) {
      const item = input.items[index];
      if (item.kind === "message" && item.role === "user" && item.text.trim()) {
        latestUserObjective = item.text.trim();
        break;
      }
    }
  }
  latestUserObjective ||= "Continue the current thread objective.";

  return {
    artifactRefs: collectArtifactRefs(recentEvidence),
    boundedTranscriptSuffix: buildBoundedTranscriptSuffix(input.items),
    capabilitySummary: buildCapabilitySummary(nodeId, nodeContract),
    continuitySnapshot: {
      frontierNodeIds: [],
      pendingApprovalNodeIds: [],
      pendingJoinNodeIds: [],
      pendingWaitNodeIds:
        input.latestRun && isWaitingRun(input.latestRun) && input.latestRun.waitReason !== "none"
          ? [nodeId]
          : [],
      recentEvidenceIds,
      touchedPaths: activePaths,
      unresolvedNodeIds: [],
    },
    graphId,
    graphRevision:
      input.latestRun?.graphId === graphId && typeof input.latestRun.graphRevision === "number"
        ? input.latestRun.graphRevision
        : 1,
    nodeContract,
    nodeId,
    nodeObjective: latestUserObjective,
    relevantEvidenceIds: recentEvidenceIds,
    unresolvedBlockers: buildUnresolvedBlockers(nodeId, [], input.latestRun),
  };
}

export function compileNodePacketPrompt(
  packet: NodePacket,
  evidence: ThreadEvidenceRecord[] = [],
): string {
  const evidenceById = new Map(evidence.map((record) => [record.evidenceId, record]));
  const evidenceLines = packet.relevantEvidenceIds.flatMap((evidenceId) => {
    const record = evidenceById.get(evidenceId);
    if (!record) {
      return [];
    }
    const relatedPaths =
      record.relatedFilePaths && record.relatedFilePaths.length > 0
        ? ` (${record.relatedFilePaths.join(", ")})`
        : "";
    return [`- [${evidenceId}] ${record.kind}: ${record.summary}${relatedPaths}`];
  });
  const transcriptLines = packet.boundedTranscriptSuffix.map(
    (exchange) =>
      `- ${exchange.role}: ${truncateText(exchange.text, exchange.role === "assistant" ? 600 : 160)}`,
  );
  const blockerLines = packet.unresolvedBlockers.map(
    (blocker) => `- [${blocker.kind}] ${blocker.summary}`,
  );
  const allowedTools =
    packet.nodeContract.allowedTools.length > 0
      ? packet.nodeContract.allowedTools.join(", ")
      : "(runtime-selected)";
  const requiredEvidenceKinds =
    packet.nodeContract.requiredEvidenceKinds.length > 0
      ? packet.nodeContract.requiredEvidenceKinds.join(", ")
      : "(none)";
  const writeScope =
    packet.nodeContract.writeScope.paths.length > 0
      ? `${packet.nodeContract.writeScope.mode}: ${packet.nodeContract.writeScope.paths.join(", ")}`
      : packet.nodeContract.writeScope.mode;

  return [
    "Approved execution node packet:",
    `- Graph: ${packet.graphId} rev ${packet.graphRevision}`,
    `- Node: ${packet.nodeId}`,
    `- Objective: ${packet.nodeObjective}`,
    `- Purpose: ${packet.nodeContract.purpose}`,
    `- Routing role: ${packet.nodeContract.preferredRoutingRole ?? "change"}`,
    `- Allowed tools: ${allowedTools}`,
    `- Write scope: ${writeScope}`,
    `- Required evidence kinds: ${requiredEvidenceKinds}`,
    `- Capability summary: ${packet.capabilitySummary.description}`,
    "",
    "Continuity snapshot:",
    `- Frontier nodes: ${formatList(packet.continuitySnapshot.frontierNodeIds)}`,
    `- Unresolved nodes: ${formatList(packet.continuitySnapshot.unresolvedNodeIds)}`,
    `- Recent evidence ids: ${formatList(packet.continuitySnapshot.recentEvidenceIds)}`,
    `- Touched paths: ${formatList(packet.continuitySnapshot.touchedPaths)}`,
    `- Pending waits: ${formatList(packet.continuitySnapshot.pendingWaitNodeIds)}`,
    "",
    "Relevant durable evidence:",
    ...renderSection(evidenceLines),
    "",
    "Bounded transcript suffix:",
    ...renderSection(transcriptLines),
    "",
    "Unresolved blockers:",
    ...renderSection(blockerLines),
    "",
    "Use this node packet as the primary execution context instead of reconstructing the plan from handoff prose.",
  ].join("\n");
}

export function compilePlanModeNodePacketPrompt(
  packet: NodePacket,
  plan: PlanProjection | null,
  evidence: ThreadEvidenceRecord[] = [],
): string {
  const evidenceById = new Map(evidence.map((record) => [record.evidenceId, record]));
  const evidenceLines = packet.relevantEvidenceIds.flatMap((evidenceId) => {
    const record = evidenceById.get(evidenceId);
    if (!record) {
      return [];
    }
    const relatedPaths =
      record.relatedFilePaths && record.relatedFilePaths.length > 0
        ? ` (${record.relatedFilePaths.join(", ")})`
        : "";
    return [`- [${evidenceId}] ${record.kind}: ${record.summary}${relatedPaths}`];
  });
  const transcriptLines = packet.boundedTranscriptSuffix.map(
    (exchange) =>
      `- ${exchange.role}: ${truncateText(exchange.text, exchange.role === "assistant" ? 600 : 160)}`,
  );
  const blockerLines = packet.unresolvedBlockers.map(
    (blocker) => `- [${blocker.kind}] ${blocker.summary}`,
  );
  const allowedTools =
    packet.nodeContract.allowedTools.length > 0
      ? packet.nodeContract.allowedTools.join(", ")
      : "(runtime-selected)";
  const writeScope =
    packet.nodeContract.writeScope.paths.length > 0
      ? `${packet.nodeContract.writeScope.mode}: ${packet.nodeContract.writeScope.paths.join(", ")}`
      : packet.nodeContract.writeScope.mode;
  const todoLines = plan
    ? plan.todos.map((todo) => `- ${renderTodoStatus(todo.status)} ${todo.content}`)
    : [];

  return [
    "Plan mode node packet:",
    `- Graph: ${packet.graphId} rev ${packet.graphRevision}`,
    `- Node: ${packet.nodeId}`,
    `- Objective: ${packet.nodeObjective}`,
    `- Purpose: ${packet.nodeContract.purpose}`,
    `- Routing role: ${packet.nodeContract.preferredRoutingRole ?? "plan"}`,
    `- Allowed tools: ${allowedTools}`,
    `- Write scope: ${writeScope}`,
    `- Current plan status: ${plan?.status ?? "(none yet)"}`,
    `- Capability summary: ${packet.capabilitySummary.description}`,
    "",
    "Current graph-backed planning state:",
    `- Title: ${plan?.title ?? "(none yet)"}`,
    `- Summary: ${plan?.summary || "(empty)"}`,
    "Plan todos:",
    ...renderSection(todoLines),
    "",
    "Risks:",
    ...renderSection((plan?.risks ?? []).map((risk) => `- ${risk}`)),
    "",
    "Open questions:",
    ...renderSection((plan?.openQuestions ?? []).map((question) => `- ${question}`)),
    "",
    "Acceptance criteria:",
    ...renderSection((plan?.acceptanceCriteria ?? []).map((criterion) => `- ${criterion}`)),
    "",
    "Affected paths:",
    ...renderSection((plan?.affectedPaths ?? []).map((path) => `- ${path}`)),
    "",
    "Continuity snapshot:",
    `- Frontier nodes: ${formatList(packet.continuitySnapshot.frontierNodeIds)}`,
    `- Unresolved nodes: ${formatList(packet.continuitySnapshot.unresolvedNodeIds)}`,
    `- Recent evidence ids: ${formatList(packet.continuitySnapshot.recentEvidenceIds)}`,
    `- Touched paths: ${formatList(packet.continuitySnapshot.touchedPaths)}`,
    `- Pending approvals: ${formatList(packet.continuitySnapshot.pendingApprovalNodeIds)}`,
    "",
    "Relevant durable evidence:",
    ...renderSection(evidenceLines),
    "",
    "Bounded transcript suffix:",
    ...renderSection(transcriptLines),
    "",
    "Unresolved blockers:",
    ...renderSection(blockerLines),
    "",
    "This node accepts only a structured plan_update action through the __best_agent_plan_update tool with the updated plan state.",
    "Use this node packet as the primary planning context instead of reconstructing the current plan from handoff prose or rendered plan text.",
  ].join("\n");
}

export function compileOrdinaryTurnNodePacketPrompt(
  packet: NodePacket,
  evidence: ThreadEvidenceRecord[] = [],
): string {
  const evidenceById = new Map(evidence.map((record) => [record.evidenceId, record]));
  const evidenceLines = packet.relevantEvidenceIds.flatMap((evidenceId) => {
    const record = evidenceById.get(evidenceId);
    if (!record) {
      return [];
    }
    const relatedPaths =
      record.relatedFilePaths && record.relatedFilePaths.length > 0
        ? ` (${record.relatedFilePaths.join(", ")})`
        : "";
    return [`- [${evidenceId}] ${record.kind}: ${record.summary}${relatedPaths}`];
  });
  const transcriptLines = packet.boundedTranscriptSuffix.map(
    (exchange) =>
      `- ${exchange.role}: ${truncateText(exchange.text, exchange.role === "assistant" ? 600 : 160)}`,
  );
  const blockerLines = packet.unresolvedBlockers.map(
    (blocker) => `- [${blocker.kind}] ${blocker.summary}`,
  );
  const allowedTools =
    packet.nodeContract.allowedTools.length > 0
      ? packet.nodeContract.allowedTools.join(", ")
      : "(runtime-selected)";
  const writeScope =
    packet.nodeContract.writeScope.paths.length > 0
      ? `${packet.nodeContract.writeScope.mode}: ${packet.nodeContract.writeScope.paths.join(", ")}`
      : packet.nodeContract.writeScope.mode;

  return [
    "Ordinary turn node packet:",
    `- Graph: ${packet.graphId} rev ${packet.graphRevision}`,
    `- Node: ${packet.nodeId}`,
    `- Objective: ${packet.nodeObjective}`,
    `- Purpose: ${packet.nodeContract.purpose}`,
    `- Routing role: ${packet.nodeContract.preferredRoutingRole ?? "change"}`,
    `- Allowed tools: ${allowedTools}`,
    `- Write scope: ${writeScope}`,
    `- Capability summary: ${packet.capabilitySummary.description}`,
    "",
    "Continuity snapshot:",
    `- Recent evidence ids: ${formatList(packet.continuitySnapshot.recentEvidenceIds)}`,
    `- Touched paths: ${formatList(packet.continuitySnapshot.touchedPaths)}`,
    `- Pending waits: ${formatList(packet.continuitySnapshot.pendingWaitNodeIds)}`,
    "",
    "Relevant durable evidence:",
    ...renderSection(evidenceLines),
    "",
    "Bounded transcript suffix:",
    ...renderSection(transcriptLines),
    "",
    "Unresolved blockers:",
    ...renderSection(blockerLines),
    "",
    "Convergence policy:",
    "- If the requested artifact or target file has been created or updated, verify it directly before doing more exploration.",
    "- If a direct verification fails, use the shortest fix-and-verify loop supported by the failure evidence.",
    "- Avoid repeating high-cost actions such as full rebuilds, full retraining, or broad codebase reading after a clear verification signal unless that signal specifically requires it.",
    "",
    "Use this node packet as the primary ordinary-turn context instead of reconstructing state from handoff prose or the unbounded recent-message window.",
  ].join("\n");
}

export function createNodePacketPromptCacheKey(
  packet: NodePacket,
  evidence: ThreadEvidenceRecord[] = [],
): string {
  return stableDigest({
    evidence: evidence.map((record) => ({
      createdAt: record.createdAt,
      evidenceId: record.evidenceId,
      kind: record.kind,
      relatedFilePaths: record.relatedFilePaths ?? [],
      summary: record.summary,
    })),
    packet,
  });
}

function buildDefaultNodeContract(
  nodeId: string,
  plan: PlanProjection,
  activeTodo?: PlanTodo,
): NodeContract {
  return {
    allowedTools: [],
    budget: {},
    capabilityRequirements: [],
    closurePolicy: {
      deterministicSufficient: false,
      humanVerdictRequired: false,
      modelVerdictAllowed: true,
      onInconclusive: "reopen",
      requiredAgreement: "any-one",
    },
    joinPolicy: {
      allowPartialSuccess: false,
      mode: "all-of",
      onPendingOrUnresolved: "block-parent",
      requiresDeterministicMerge: false,
      successCondition: "Complete the approved plan node and preserve durable evidence.",
    },
    nodeId,
    preferredRoutingRole: "change",
    purpose: activeTodo
      ? `Execute approved plan todo: ${activeTodo.content}`
      : `Execute approved plan: ${plan.title}`,
    requiredEvidenceKinds: [],
    verifierPolicy: {
      kind: "model",
      required: false,
    },
    writeScope: {
      mode: "workspace-write",
      paths: [...plan.affectedPaths],
    },
  };
}

function buildPlanModeNodeContract(nodeId: string, plan?: PlanProjection): NodeContract {
  return {
    allowedTools: [],
    budget: {},
    capabilityRequirements: [],
    closurePolicy: {
      deterministicSufficient: false,
      humanVerdictRequired: false,
      modelVerdictAllowed: true,
      onInconclusive: "reopen",
      requiredAgreement: "any-one",
    },
    joinPolicy: {
      allowPartialSuccess: false,
      mode: "all-of",
      onPendingOrUnresolved: "block-parent",
      requiresDeterministicMerge: false,
      successCondition:
        "Return a revised plan projection or confirm the current planning state remains correct.",
    },
    nodeId,
    preferredRoutingRole: "plan",
    purpose: plan
      ? `Review or revise thread plan: ${plan.title}`
      : "Author the initial thread plan.",
    requiredEvidenceKinds: [],
    verifierPolicy: {
      kind: "model",
      required: false,
    },
    writeScope: {
      mode: "read-only",
      paths: [...(plan?.affectedPaths ?? [])],
    },
  };
}

function buildOrdinaryTurnNodeContract(nodeId: string, activePaths: string[]): NodeContract {
  return {
    allowedTools: [],
    budget: {},
    capabilityRequirements: [],
    closurePolicy: {
      deterministicSufficient: false,
      humanVerdictRequired: false,
      modelVerdictAllowed: true,
      onInconclusive: "reopen",
      requiredAgreement: "any-one",
    },
    joinPolicy: {
      allowPartialSuccess: false,
      mode: "all-of",
      onPendingOrUnresolved: "block-parent",
      requiresDeterministicMerge: false,
      successCondition:
        "Advance the current thread objective and preserve durable evidence when relevant.",
    },
    nodeId,
    preferredRoutingRole: activePaths.length > 0 ? "change" : "inspect",
    purpose: "Continue the current thread objective with bounded ordinary-turn context.",
    requiredEvidenceKinds: [],
    verifierPolicy: {
      kind: "model",
      required: false,
    },
    writeScope: {
      mode: "workspace-write",
      paths: [...activePaths],
    },
  };
}

function buildCapabilitySummary(nodeId: string, nodeContract: NodeContract): CapabilitySummary {
  const toolSummary =
    nodeContract.allowedTools.length > 0
      ? nodeContract.allowedTools.join(", ")
      : "runtime-selected tools";
  return {
    description: `Execution scoped to ${toolSummary} with ${nodeContract.writeScope.mode} access.`,
    id: `node-${nodeId}`,
    kind: "skill",
    source: "runtime",
    toolNames: nodeContract.allowedTools.length > 0 ? [...nodeContract.allowedTools] : undefined,
  };
}

function evidencePriority(record: ThreadEvidenceRecord, preferred: Set<string>): number {
  let score = preferred.has(record.evidenceId) ? 100 : 0;
  switch (record.kind) {
    case "verification-verdict":
      score += 60;
      break;
    case "artifact-fact":
    case "workspace-fact":
      score += 40;
      break;
    case "research-finding":
      score += 30;
      break;
    case "handoff-fact":
      score += 20;
      break;
    case "tool-outcome":
      score += 10;
      break;
  }
  if (record.diagnosticCode) {
    score += 10;
  }
  if (record.summary.toLowerCase().match(/\b(error|fail|failed|failing|unsatisfied)\b/u)) {
    score += 15;
  }
  return score + Math.max(0, Math.min(1, record.confidence)) * 5;
}

function buildUnresolvedBlockers(
  nodeId: string,
  todos: PlanTodo[],
  latestRun?: AgentRun,
): UnresolvedBlocker[] {
  const blockers = todos
    .filter((todo) => todo.status === "blocked")
    .map<UnresolvedBlocker>((todo) => ({
      blockerId: `todo-${todo.id}`,
      kind: "external",
      nodeId: todo.id,
      summary: todo.content,
    }));
  if (latestRun && isWaitingRun(latestRun) && latestRun.waitReason !== "none") {
    blockers.push({
      blockerId: `run-${latestRun.id}`,
      kind: "wait",
      nodeId,
      summary: `Run is waiting on ${latestRun.waitReason}.`,
    });
  }
  return blockers;
}

function buildBoundedTranscriptSuffix(items: Item[]): TranscriptExchange[] {
  const exchanges = items
    .filter(
      (item): item is Extract<Item, { kind: "message" }> =>
        item.kind === "message" && (item.role === "user" || item.role === "agent"),
    )
    .slice(-3)
    .map<TranscriptExchange>((item) => ({
      role: item.role === "agent" ? "assistant" : "user",
      text: item.text,
      turnId: item.turnId,
    }));
  return exchanges;
}

function collectArtifactRefs(records: ThreadEvidenceRecord[]): ArtifactRef[] {
  const artifactRefs: ArtifactRef[] = [];
  const seen = new Set<string>();
  for (const record of records) {
    for (const ref of record.artifactRefs ?? []) {
      const key = `${ref.assetId}:${ref.name ?? ""}`;
      if (seen.has(key)) {
        continue;
      }
      seen.add(key);
      artifactRefs.push(ref);
    }
  }
  return artifactRefs;
}

function summarizePlanObjective(plan: PlanProjection): string {
  const summary = plan.summary.trim();
  return summary ? `${plan.title}: ${summary}` : plan.title;
}

function isUnresolvedTodoStatus(status: PlanTodo["status"]): boolean {
  return status === "pending" || status === "in_progress" || status === "blocked";
}

function isWaitingRun(run: AgentRun): boolean {
  return run.status === "running" || run.status === "paused";
}

function uniqueValues(values: string[]): string[] {
  return [...new Set(values.filter((value) => value.trim().length > 0))];
}

function formatList(values: string[]): string {
  return values.length > 0 ? values.join(", ") : "(none)";
}

function renderSection(lines: string[]): string[] {
  return lines.length > 0 ? lines : ["- (none)"];
}

function renderTodoStatus(status: PlanTodo["status"]): string {
  switch (status) {
    case "done":
      return "[x]";
    case "in_progress":
      return "[~]";
    case "blocked":
      return "[!]";
    default:
      return "[ ]";
  }
}
