// SPEC: PJ2026-010606 GitHub入口 draft-2026-06-25-gh-split
// Moved mechanically from scripts/src/gh.ts:3193-4143.

import path from "node:path";
import { bodySha, preview } from "./auth-and-safety";
import { boardColumnIndexMap, boardColumnKindForField, boardDuplicateRows, boardHeaderColumnKind, boardIssueColumnIndex, boardRowFromCells, boardRowsExactMatch, boardRowSummary, boardRowUpsertFieldValues, boardRowValuePollutionEvidence, boardSectionInsertAfterLine, findBoardRow, insertBoardBodyLineAfter, normalizeBoardHeader, removeBoardBodyLine, renderMarkdownTableRow, replaceBoardBodyLine, stripMarkdownInline, targetBoardSection } from "./board-parser";
import { normalizeNewlines } from "./body-profiles";
import { validationError } from "./client";
import { BOARD_MUTATION_SECTIONS, BOARD_ROW_FIELDS, BOARD_ROW_UPSERT_TEXT_FIELDS } from "./types";
import type { BoardColumnKind, BoardGithubStatus, BoardMutationSection, BoardRowField, BoardRowMutationPlanResult, BoardRowUpdatePlanResult, BoardRowUpsertField, BoardRowUpsertTextField, BoardRowUpsertValues, BoardRowValidationWarning, BoardTableRow, BoardTableSection, GitHubCommandFailure, GitHubIssue, GitHubOptions } from "./types";

export function boardRowUpdatePlan(
  repo: string,
  boardIssue: GitHubIssue,
  issueNumber: number,
  field: BoardRowField,
  value: string,
  parsed: { sections: BoardTableSection[]; warnings: BoardRowValidationWarning[] },
): BoardRowUpdatePlanResult | GitHubCommandFailure {
  const found = findBoardRow(repo, parsed.sections, issueNumber);
  if (found.ok === false) return found;
  const { section, row } = found;
  const targetColumn = boardColumnKindForField(field);
  const columnMap = boardColumnIndexMap(section.headers);
  const columnIndex = columnMap.get(targetColumn);
  if (columnIndex === undefined) {
    return validationError("issue board-row update", repo, `board table does not contain a column for --field ${field}`, {
      issueNumber,
      field,
      targetColumn,
      section: section.kind,
      headers: section.headers,
      supportedFields: BOARD_ROW_FIELDS.slice(),
    }) as GitHubCommandFailure;
  }
  const oldCells = row.cells.slice();
  const newCells = oldCells.slice();
  while (newCells.length < section.headers.length) newCells.push("");
  newCells[columnIndex] = value;
  const newRow = renderMarkdownTableRow(newCells);
  const oldBody = boardIssue.body ?? "";
  let newBody: string;
  try {
    newBody = replaceBoardBodyLine(oldBody, row.lineNumber, newRow);
  } catch (error) {
    return validationError("issue board-row update", repo, error instanceof Error ? error.message : String(error), {
      issueNumber,
      field,
      lineNumber: row.lineNumber,
    }) as GitHubCommandFailure;
  }
  return {
    ok: true,
    newBody,
    plan: {
      repo,
      boardIssue: boardIssue.number,
      issueNumber,
      field,
      targetColumn,
      targetColumnIndex: columnIndex,
      section: section.kind,
      lineNumber: row.lineNumber,
      oldRow: row.raw,
      newRow,
      oldCells,
      newCells,
      oldValue: oldCells[columnIndex] ?? "",
      newValue: value,
      row: {
        old: boardRowSummary(section, row),
        new: {
          ...boardRowSummary(section, { ...row, raw: newRow, cells: newCells }),
          updatedField: field,
        },
      },
      body: {
        oldBodyChars: oldBody.length,
        oldBodySha: bodySha(oldBody),
        newBodyChars: newBody.length,
        newBodySha: bodySha(newBody),
        changed: oldBody !== newBody,
      },
      tableParser: {
        reused: "parseBoardTables",
        rowValidationWarnings: parsed.warnings.length,
      },
      request: {
        method: "PATCH",
        path: `/repos/{owner}/{repo}/issues/${boardIssue.number}`,
        body: { bodyChars: newBody.length },
      },
    },
  };
}

export function expectedBoardGithubStatus(section: BoardMutationSection): BoardGithubStatus {
  return section === "open" ? "OPEN" : "CLOSED";
}

export function normalizeBoardGithubStatusCell(value: string | undefined): BoardGithubStatus | null {
  const normalized = stripMarkdownInline(value ?? "").toUpperCase().replace(/\s+/g, "");
  if (normalized === "OPEN") return "OPEN";
  if (normalized === "CLOSED") return "CLOSED";
  return null;
}

export function boardHeaderShape(headers: string[]): string[] {
  return headers.map((header) => normalizeBoardHeader(header));
}

export function sameBoardHeaderShape(a: string[], b: string[]): boolean {
  const left = boardHeaderShape(a);
  const right = boardHeaderShape(b);
  return left.length === right.length && left.every((value, index) => value === right[index]);
}

export function boardRowDuplicateFailure(
  repo: string,
  commandName: string,
  sections: BoardTableSection[],
  issueNumber: number,
): GitHubCommandFailure | null {
  const duplicates = boardDuplicateRows(sections, issueNumber);
  if (duplicates.length === 0) return null;
  return validationError(commandName, repo, `board row for issue #${issueNumber} already exists; refusing duplicate row`, {
    issueNumber,
    matches: duplicates.map(({ section, row }) => ({ section: section.kind, lineNumber: row.lineNumber, rowPreview: preview(row.raw) })),
  }) as GitHubCommandFailure;
}

export function boardStatusOptionFailure(
  repo: string,
  commandName: string,
  targetSection: BoardMutationSection,
  status: BoardGithubStatus | undefined,
): GitHubCommandFailure | null {
  if (status === undefined) return null;
  const expected = expectedBoardGithubStatus(targetSection);
  if (status === expected) return null;
  return validationError(commandName, repo, `--status ${status} conflicts with --to ${targetSection}; expected ${expected}`, {
    targetSection,
    status,
    expectedStatus: expected,
  }) as GitHubCommandFailure;
}

export function boardRowStatusConsistency(
  repo: string,
  commandName: string,
  section: BoardTableSection,
  row: BoardTableRow,
  role: "source" | "target",
): { ok: true; githubStatusColumnIndex: number; actualStatus: BoardGithubStatus | null; expectedStatus: BoardGithubStatus } | GitHubCommandFailure {
  const columnMap = boardColumnIndexMap(section.headers);
  const githubStatusColumnIndex = columnMap.get("githubStatus");
  if (githubStatusColumnIndex === undefined) {
    return validationError(commandName, repo, `${role} board table does not contain a GitHub 状态 column`, {
      section: section.kind,
      headers: section.headers,
      lineNumber: row.lineNumber,
      rowPreview: preview(row.raw),
    }) as GitHubCommandFailure;
  }
  const expectedStatus = expectedBoardGithubStatus(section.kind);
  const actualStatus = normalizeBoardGithubStatusCell(row.cells[githubStatusColumnIndex]);
  if (actualStatus !== expectedStatus) {
    return validationError(commandName, repo, `${role} row GitHub 状态 ${actualStatus ?? "missing"} conflicts with ${section.kind.toUpperCase()} section; expected ${expectedStatus}`, {
      section: section.kind,
      lineNumber: row.lineNumber,
      rowPreview: preview(row.raw),
      actualStatus,
      expectedStatus,
      githubStatusColumnIndex,
      headers: section.headers,
    }) as GitHubCommandFailure;
  }
  return { ok: true, githubStatusColumnIndex, actualStatus, expectedStatus };
}

export function boardRowUpsertValueEntries(values: BoardRowUpsertValues): Array<{ field: BoardRowUpsertField; value: string }> {
  const entries: Array<{ field: BoardRowUpsertField; value: string }> = [];
  for (const field of BOARD_ROW_UPSERT_TEXT_FIELDS) {
    const value = values[field];
    if (value !== undefined) entries.push({ field, value });
  }
  if (values.status !== undefined) entries.push({ field: "status", value: values.status });
  return entries;
}

export function boardColumnKindForUpsertField(field: BoardRowUpsertField): BoardColumnKind {
  if (field === "category") return "category";
  if (field === "branch") return "branch";
  if (field === "tasks") return "relatedTask";
  if (field === "summary") return "summary";
  if (field === "focus") return "focus";
  if (field === "validation") return "acceptance";
  if (field === "status") return "githubStatus";
  return "progress";
}

export function renderBoardIssueCell(repo: string, issueNumber: number, summary: string | undefined): string {
  const trimmed = (summary ?? "").trim();
  if (trimmed.length === 0) return `#${issueNumber}`;
  const targetHref = `https://github.com/${repo}/issues/${issueNumber}`;
  const firstMarkdownIssue = /^\s*\[[^\]]*\]\(([^)]*)\)/u.exec(trimmed)?.[1] ?? "";
  if (new RegExp(`/issues/${issueNumber}(?:\\b|$)`, "u").test(firstMarkdownIssue)) return trimmed;
  return `[#${issueNumber}](${targetHref}) ${trimmed}`;
}

export function boardRowUpsertTargetColumn(
  repo: string,
  commandName: string,
  section: BoardTableSection,
  issueNumber: number,
  field: BoardRowUpsertField,
): { ok: true; columnIndex: number; targetColumn: BoardColumnKind | "issue"; usesIssueCell: boolean } | GitHubCommandFailure {
  const columnMap = boardColumnIndexMap(section.headers);
  if (field === "summary") {
    const summaryIndex = columnMap.get("summary");
    if (summaryIndex !== undefined) return { ok: true, columnIndex: summaryIndex, targetColumn: "summary", usesIssueCell: false };
    const issueColumnIndex = boardIssueColumnIndex(section.headers);
    if (issueColumnIndex !== null) return { ok: true, columnIndex: issueColumnIndex, targetColumn: "issue", usesIssueCell: true };
  }
  const targetColumn = boardColumnKindForUpsertField(field);
  const columnIndex = columnMap.get(targetColumn);
  if (columnIndex === undefined) {
    return validationError(commandName, repo, `board table does not contain a column for --${field}`, {
      issueNumber,
      field,
      targetColumn,
      section: section.kind,
      headers: section.headers,
    }) as GitHubCommandFailure;
  }
  return { ok: true, columnIndex, targetColumn, usesIssueCell: false };
}

export function boardRowUpsertRenderedValue(
  repo: string,
  issueNumber: number,
  field: BoardRowUpsertField,
  value: string,
  targetColumn: BoardColumnKind | "issue",
): string {
  if (field === "summary" && targetColumn === "issue") return renderBoardIssueCell(repo, issueNumber, value);
  return value;
}

export function boardRowUpsertPollutionFailure(
  repo: string,
  commandName: string,
  issueNumber: number,
  values: BoardRowUpsertValues,
): GitHubCommandFailure | null {
  const findings = BOARD_ROW_UPSERT_TEXT_FIELDS
    .map((field) => {
      const value = values[field];
      if (value === undefined) return null;
      const evidence = boardRowValuePollutionEvidence(value);
      return evidence.length === 0 ? null : { field, valuePreview: preview(value), evidence };
    })
    .filter((item): item is { field: BoardRowUpsertTextField; valuePreview: string; evidence: string[] } => item !== null);
  if (findings.length === 0) return null;
  return validationError(commandName, repo, "issue board-row upsert field value contains literal shell escape text that would pollute a board cell; pass real newlines or plain text instead", {
    issueNumber,
    shellPollution: { suspected: true, findings },
  }) as GitHubCommandFailure;
}

export function boardRowUpsertUpdatePlan(
  repo: string,
  boardIssue: GitHubIssue,
  issueNumber: number,
  found: { section: BoardTableSection; row: BoardTableRow },
  values: BoardRowUpsertValues,
  requestedSection: BoardMutationSection | undefined,
  parsed: { sections: BoardTableSection[]; warnings: BoardRowValidationWarning[] },
): BoardRowMutationPlanResult | GitHubCommandFailure {
  const commandName = "issue board-row upsert";
  const { section, row } = found;
  if (requestedSection !== undefined && requestedSection !== section.kind) {
    return validationError(commandName, repo, `--section ${requestedSection} conflicts with existing row in ${section.kind}; use gh issue board-row move for section migration`, {
      issueNumber,
      requestedSection,
      existingSection: section.kind,
      moveCommand: `bun scripts/cli.ts gh issue board-row move ${issueNumber} --board-issue ${boardIssue.number} --to ${requestedSection}`,
    }) as GitHubCommandFailure;
  }
  if (values.status !== undefined) {
    const expectedStatus = expectedBoardGithubStatus(section.kind);
    if (values.status !== expectedStatus) {
      return validationError(commandName, repo, `--status ${values.status} conflicts with existing ${section.kind.toUpperCase()} section; use gh issue board-row move for section migration`, {
        issueNumber,
        existingSection: section.kind,
        requestedStatus: values.status,
        expectedStatus,
        moveCommand: `bun scripts/cli.ts gh issue board-row move ${issueNumber} --board-issue ${boardIssue.number} --to ${values.status === "OPEN" ? "open" : "closed"} --status ${values.status}`,
      }) as GitHubCommandFailure;
    }
  }
  const entries = boardRowUpsertValueEntries(values);
  if (entries.length === 0) {
    return validationError(commandName, repo, "issue board-row upsert found an existing row but no upsert fields were provided", {
      issueNumber,
      existingSection: section.kind,
      supportedFields: ["--category", "--branch", "--tasks", "--summary", "--focus", "--validation", "--progress", "--status"],
    }) as GitHubCommandFailure;
  }
  if (row.cells.length !== section.headers.length) {
    return validationError(commandName, repo, "existing row column count does not match the board table header", {
      issueNumber,
      section: section.kind,
      expectedColumns: section.headers.length,
      actualColumns: row.cells.length,
      headers: section.headers,
      lineNumber: row.lineNumber,
      rowPreview: preview(row.raw),
    }) as GitHubCommandFailure;
  }
  const oldCells = row.cells.slice();
  const newCells = oldCells.slice();
  const changedFields: Record<string, unknown>[] = [];
  for (const entry of entries) {
    const target = boardRowUpsertTargetColumn(repo, commandName, section, issueNumber, entry.field);
    if (target.ok === false) return target;
    const newValue = boardRowUpsertRenderedValue(repo, issueNumber, entry.field, entry.value, target.targetColumn);
    const oldValue = oldCells[target.columnIndex] ?? "";
    newCells[target.columnIndex] = newValue;
    changedFields.push({
      field: entry.field,
      targetColumn: target.targetColumn,
      targetColumnIndex: target.columnIndex,
      requestedValue: entry.value,
      oldValue,
      newValue,
      changed: oldValue !== newValue,
    });
  }
  const newRow = renderMarkdownTableRow(newCells);
  const oldBody = boardIssue.body ?? "";
  let newBody: string;
  try {
    newBody = replaceBoardBodyLine(oldBody, row.lineNumber, newRow);
  } catch (error) {
    return validationError(commandName, repo, error instanceof Error ? error.message : String(error), {
      issueNumber,
      lineNumber: row.lineNumber,
    }) as GitHubCommandFailure;
  }
  return {
    ok: true,
    newBody,
    plan: {
      repo,
      boardIssue: boardIssue.number,
      issueNumber,
      operation: "update",
      action: "update",
      section: section.kind,
      lineNumber: row.lineNumber,
      requestedSection: requestedSection ?? null,
      providedFields: entries.map((entry) => entry.field),
      changedFields,
      oldRow: row.raw,
      newRow,
      oldCells,
      newCells,
      row: {
        old: boardRowSummary(section, row),
        new: {
          ...boardRowSummary(section, { ...row, raw: newRow, cells: newCells }),
          upsertFields: boardRowUpsertFieldValues(section, { ...row, raw: newRow, cells: newCells }),
        },
      },
      linePlan: {
        action: "replace",
        section: section.kind,
        lineNumber: row.lineNumber,
      },
      sectionPlan: {
        kind: section.kind,
        heading: section.heading,
        headerLine: section.headerLine,
        rowCount: section.rows.length,
      },
      body: {
        oldBodyChars: oldBody.length,
        oldBodySha: bodySha(oldBody),
        newBodyChars: newBody.length,
        newBodySha: bodySha(newBody),
        changed: oldBody !== newBody,
      },
      tableParser: {
        reused: "parseBoardTables",
        rowValidationWarnings: parsed.warnings.length,
      },
      request: {
        method: "PATCH",
        path: `/repos/{owner}/{repo}/issues/${boardIssue.number}`,
        body: { bodyChars: newBody.length },
      },
    },
  };
}

export function boardRowUpsertAddPlan(
  repo: string,
  boardIssue: GitHubIssue,
  issueNumber: number,
  sectionKind: BoardMutationSection,
  values: BoardRowUpsertValues,
  parsed: { sections: BoardTableSection[]; warnings: BoardRowValidationWarning[] },
): BoardRowMutationPlanResult | GitHubCommandFailure {
  const commandName = "issue board-row upsert";
  const targetLookup = targetBoardSection(repo, parsed.sections, sectionKind, commandName);
  if (targetLookup.ok === false) return targetLookup;
  const target = targetLookup.section;
  const issueColumnIndex = boardIssueColumnIndex(target.headers);
  if (issueColumnIndex === null) {
    return validationError(commandName, repo, "board table does not contain an Issue column required for generated upsert rows", {
      boardIssue: boardIssue.number,
      section: sectionKind,
      headers: target.headers,
    }) as GitHubCommandFailure;
  }
  const columnMap = boardColumnIndexMap(target.headers);
  const githubStatusColumnIndex = columnMap.get("githubStatus");
  if (githubStatusColumnIndex === undefined) {
    return validationError(commandName, repo, "board table does not contain a GitHub 状态 column required for generated upsert rows", {
      boardIssue: boardIssue.number,
      section: sectionKind,
      headers: target.headers,
    }) as GitHubCommandFailure;
  }
  const expectedStatus = expectedBoardGithubStatus(sectionKind);
  const requestedStatus = values.status ?? expectedStatus;
  if (requestedStatus !== expectedStatus) {
    return validationError(commandName, repo, `--status ${requestedStatus} conflicts with --section ${sectionKind}; expected ${expectedStatus}`, {
      issueNumber,
      section: sectionKind,
      requestedStatus,
      expectedStatus,
    }) as GitHubCommandFailure;
  }
  const appliedFields = new Set<BoardRowUpsertField>();
  const missingFields: BoardRowUpsertTextField[] = [];
  const unsupportedColumns: Array<{ index: number; header: string }> = [];
  const summaryColumnIndex = columnMap.get("summary");
  const cells = target.headers.map((header, index) => {
    if (index === issueColumnIndex) {
      if (summaryColumnIndex === undefined && values.summary !== undefined) appliedFields.add("summary");
      return summaryColumnIndex === undefined ? renderBoardIssueCell(repo, issueNumber, values.summary) : `#${issueNumber}`;
    }
    const kind = boardHeaderColumnKind(header);
    if (kind === "githubStatus") {
      if (values.status !== undefined) appliedFields.add("status");
      return requestedStatus;
    }
    const valueFor = (field: BoardRowUpsertTextField, value: string | undefined): string => {
      if (value === undefined) {
        missingFields.push(field);
        return "";
      }
      appliedFields.add(field);
      return value;
    };
    if (kind === "category") return valueFor("category", values.category);
    if (kind === "branch") return valueFor("branch", values.branch);
    if (kind === "relatedTask") return valueFor("tasks", values.tasks);
    if (kind === "summary") return valueFor("summary", values.summary);
    if (kind === "focus") return valueFor("focus", values.focus);
    if (kind === "acceptance") return valueFor("validation", values.validation);
    if (kind === "progress") return valueFor("progress", values.progress);
    unsupportedColumns.push({ index, header });
    return "";
  });
  if (unsupportedColumns.length > 0) {
    return validationError(commandName, repo, "target board table contains unsupported column(s) for generated upsert rows", {
      issueNumber,
      section: sectionKind,
      unsupportedColumns,
      headers: target.headers,
    }) as GitHubCommandFailure;
  }
  const unappliedFields = boardRowUpsertValueEntries(values)
    .map((entry) => entry.field)
    .filter((field) => !appliedFields.has(field));
  if (unappliedFields.length > 0) {
    return validationError(commandName, repo, "target board table does not contain column(s) for supplied upsert field(s)", {
      issueNumber,
      section: sectionKind,
      unappliedFields,
      headers: target.headers,
    }) as GitHubCommandFailure;
  }
  const uniqueMissingFields = Array.from(new Set(missingFields));
  if (uniqueMissingFields.length > 0) {
    return validationError(commandName, repo, "issue board-row upsert add requires values for every generated row column", {
      issueNumber,
      section: sectionKind,
      missingFields: uniqueMissingFields,
      requiredOptions: uniqueMissingFields.map((field) => `--${field}`),
      headers: target.headers,
    }) as GitHubCommandFailure;
  }
  const newRow = renderMarkdownTableRow(cells);
  const row = boardRowFromCells(target, newRow, cells, 0);
  if (row.issueNumber !== issueNumber) {
    return validationError(commandName, repo, "generated Issue column does not match the requested issue number", {
      issueNumber,
      issueColumnIndex,
      issueCell: cells[issueColumnIndex] ?? "",
      detectedIssueNumber: row.issueNumber,
      rowPreview: preview(newRow),
    }) as GitHubCommandFailure;
  }
  const actualStatus = normalizeBoardGithubStatusCell(cells[githubStatusColumnIndex]);
  if (actualStatus !== expectedStatus) {
    return validationError(commandName, repo, "generated GitHub 状态 column does not match the target section", {
      issueNumber,
      section: sectionKind,
      expectedStatus,
      actualStatus,
      statusColumnIndex: githubStatusColumnIndex,
      statusCell: cells[githubStatusColumnIndex] ?? "",
      rowPreview: preview(newRow),
    }) as GitHubCommandFailure;
  }
  const oldBody = boardIssue.body ?? "";
  const insertAfterLine = boardSectionInsertAfterLine(target);
  let newBody: string;
  try {
    newBody = insertBoardBodyLineAfter(oldBody, insertAfterLine, newRow);
  } catch (error) {
    return validationError(commandName, repo, error instanceof Error ? error.message : String(error), {
      issueNumber,
      section: sectionKind,
      insertAfterLine,
    }) as GitHubCommandFailure;
  }
  return {
    ok: true,
    newBody,
    plan: {
      repo,
      boardIssue: boardIssue.number,
      issueNumber,
      operation: "add",
      action: "add",
      section: sectionKind,
      insertAfterLine,
      providedFields: boardRowUpsertValueEntries(values).map((entry) => entry.field),
      newRow,
      newCells: cells,
      row: {
        old: null,
        new: {
          ...boardRowSummary(target, { ...row, lineNumber: insertAfterLine + 1 }),
          upsertFields: boardRowUpsertFieldValues(target, { ...row, lineNumber: insertAfterLine + 1 }),
        },
      },
      linePlan: {
        action: "insert",
        section: sectionKind,
        insertAfterLine,
        newLineNumber: insertAfterLine + 1,
      },
      sectionPlan: {
        kind: sectionKind,
        heading: target.heading,
        headerLine: target.headerLine,
        rowCount: target.rows.length,
      },
      validation: {
        issueColumnIndex,
        githubStatusColumnIndex,
        expectedStatus,
        actualStatus,
        columnCount: cells.length,
      },
      body: {
        oldBodyChars: oldBody.length,
        oldBodySha: bodySha(oldBody),
        newBodyChars: newBody.length,
        newBodySha: bodySha(newBody),
        changed: oldBody !== newBody,
      },
      tableParser: {
        reused: "parseBoardTables",
        rowValidationWarnings: parsed.warnings.length,
      },
      request: {
        method: "PATCH",
        path: `/repos/{owner}/{repo}/issues/${boardIssue.number}`,
        body: { bodyChars: newBody.length },
      },
    },
  };
}

export function boardRowUpsertPlan(
  repo: string,
  boardIssue: GitHubIssue,
  issueNumber: number,
  options: GitHubOptions,
  parsed: { sections: BoardTableSection[]; warnings: BoardRowValidationWarning[] },
): BoardRowMutationPlanResult | GitHubCommandFailure {
  const commandName = "issue board-row upsert";
  const pollutionFailure = boardRowUpsertPollutionFailure(repo, commandName, issueNumber, options.boardRowUpsertValues);
  if (pollutionFailure !== null) return pollutionFailure;
  const matches = boardRowsExactMatch(parsed.sections, issueNumber);
  if (matches.length > 1) {
    return validationError(commandName, repo, `board row for issue #${issueNumber} is ambiguous; refusing upsert`, {
      issueNumber,
      matches: matches.map(({ section, row }) => ({ section: section.kind, lineNumber: row.lineNumber, rowPreview: preview(row.raw) })),
    }) as GitHubCommandFailure;
  }
  if (matches.length === 1) {
    return boardRowUpsertUpdatePlan(repo, boardIssue, issueNumber, matches[0], options.boardRowUpsertValues, options.boardSection, parsed);
  }
  if (options.boardSection === undefined) {
    return validationError(commandName, repo, "issue board-row upsert add requires --section open|closed when the row does not already exist", {
      issueNumber,
      supportedSections: BOARD_MUTATION_SECTIONS.slice(),
    }) as GitHubCommandFailure;
  }
  return boardRowUpsertAddPlan(repo, boardIssue, issueNumber, options.boardSection, options.boardRowUpsertValues, parsed);
}

export function boardRowAddPlan(
  repo: string,
  boardIssue: GitHubIssue,
  issueNumber: number,
  sectionKind: BoardMutationSection,
  rowFile: { path: string; raw: string; cells: string[] },
  parsed: { sections: BoardTableSection[]; warnings: BoardRowValidationWarning[] },
): BoardRowMutationPlanResult | GitHubCommandFailure {
  const commandName = "issue board-row add";
  const duplicate = boardRowDuplicateFailure(repo, commandName, parsed.sections, issueNumber);
  if (duplicate !== null) return duplicate;
  const targetLookup = targetBoardSection(repo, parsed.sections, sectionKind, commandName);
  if (targetLookup.ok === false) return targetLookup;
  const target = targetLookup.section;
  const issueColumnIndex = boardIssueColumnIndex(target.headers);
  if (issueColumnIndex === null) {
    return validationError(commandName, repo, "board table does not contain an Issue column required for row-file validation", {
      boardIssue: boardIssue.number,
      section: sectionKind,
      headers: target.headers,
    }) as GitHubCommandFailure;
  }
  const columnMap = boardColumnIndexMap(target.headers);
  const githubStatusColumnIndex = columnMap.get("githubStatus");
  if (githubStatusColumnIndex === undefined) {
    return validationError(commandName, repo, "board table does not contain a GitHub 状态 column required for add section validation", {
      boardIssue: boardIssue.number,
      section: sectionKind,
      headers: target.headers,
    }) as GitHubCommandFailure;
  }
  if (rowFile.cells.length !== target.headers.length) {
    return validationError(commandName, repo, "row-file column count does not match the target board table header", {
      boardIssue: boardIssue.number,
      section: sectionKind,
      expectedColumns: target.headers.length,
      actualColumns: rowFile.cells.length,
      headers: target.headers,
      rowFile: rowFile.path,
      rowPreview: preview(rowFile.raw),
    }) as GitHubCommandFailure;
  }
  const row = boardRowFromCells(target, rowFile.raw, rowFile.cells, 0);
  if (row.issueNumber !== issueNumber) {
    return validationError(commandName, repo, "row-file Issue column does not match the requested issue number", {
      issueNumber,
      issueColumnIndex,
      issueCell: rowFile.cells[issueColumnIndex] ?? "",
      detectedIssueNumber: row.issueNumber,
      rowPreview: preview(rowFile.raw),
    }) as GitHubCommandFailure;
  }
  const expectedStatus = expectedBoardGithubStatus(sectionKind);
  const actualStatus = normalizeBoardGithubStatusCell(rowFile.cells[githubStatusColumnIndex]);
  if (actualStatus !== expectedStatus) {
    return validationError(commandName, repo, "row-file GitHub 状态 column does not match the target section", {
      issueNumber,
      section: sectionKind,
      expectedStatus,
      actualStatus,
      statusColumnIndex: githubStatusColumnIndex,
      statusCell: rowFile.cells[githubStatusColumnIndex] ?? "",
      rowPreview: preview(rowFile.raw),
    }) as GitHubCommandFailure;
  }
  const oldBody = boardIssue.body ?? "";
  const insertAfterLine = boardSectionInsertAfterLine(target);
  let newBody: string;
  try {
    newBody = insertBoardBodyLineAfter(oldBody, insertAfterLine, rowFile.raw);
  } catch (error) {
    return validationError(commandName, repo, error instanceof Error ? error.message : String(error), {
      issueNumber,
      section: sectionKind,
      insertAfterLine,
    }) as GitHubCommandFailure;
  }
  return {
    ok: true,
    newBody,
    plan: {
      repo,
      boardIssue: boardIssue.number,
      issueNumber,
      action: "add",
      section: sectionKind,
      insertAfterLine,
      rowFile: rowFile.path,
      newRow: rowFile.raw,
      newCells: rowFile.cells,
      row: {
        old: null,
        new: boardRowSummary(target, { ...row, lineNumber: insertAfterLine + 1 }),
      },
      linePlan: {
        action: "insert",
        section: sectionKind,
        insertAfterLine,
        newLineNumber: insertAfterLine + 1,
      },
      sectionPlan: {
        kind: sectionKind,
        heading: target.heading,
        headerLine: target.headerLine,
        rowCount: target.rows.length,
      },
      validation: {
        issueColumnIndex,
        githubStatusColumnIndex,
        expectedStatus,
        actualStatus,
        columnCount: rowFile.cells.length,
      },
      body: {
        oldBodyChars: oldBody.length,
        oldBodySha: bodySha(oldBody),
        newBodyChars: newBody.length,
        newBodySha: bodySha(newBody),
        changed: oldBody !== newBody,
      },
      tableParser: {
        reused: "parseBoardTables",
        rowValidationWarnings: parsed.warnings.length,
      },
      request: {
        method: "PATCH",
        path: `/repos/{owner}/{repo}/issues/${boardIssue.number}`,
        body: { bodyChars: newBody.length },
      },
    },
  };
}

export function moveBoardBodyRow(body: string, sourceLineNumber: number, insertAfterLine: number, newLine: string): string {
  const normalized = normalizeNewlines(body);
  const lines = normalized.split("\n");
  if (sourceLineNumber <= 0 || sourceLineNumber > lines.length) throw new Error(`line ${sourceLineNumber} is outside the board body`);
  if (insertAfterLine < 0 || insertAfterLine > lines.length) throw new Error(`insert line ${insertAfterLine} is outside the board body`);
  lines.splice(sourceLineNumber - 1, 1);
  const adjustedInsertAfterLine = sourceLineNumber <= insertAfterLine ? insertAfterLine - 1 : insertAfterLine;
  lines.splice(adjustedInsertAfterLine, 0, newLine);
  return lines.join("\n");
}

export function boardRowMovePlan(
  repo: string,
  boardIssue: GitHubIssue,
  issueNumber: number,
  targetKind: BoardMutationSection,
  status: BoardGithubStatus | undefined,
  parsed: { sections: BoardTableSection[]; warnings: BoardRowValidationWarning[] },
): BoardRowMutationPlanResult | GitHubCommandFailure {
  const commandName = "issue board-row move";
  const statusFailure = boardStatusOptionFailure(repo, commandName, targetKind, status);
  if (statusFailure !== null) return statusFailure;
  const found = findBoardRow(repo, parsed.sections, issueNumber);
  if (found.ok === false) return { ...found, command: commandName, repo } as GitHubCommandFailure;
  const sourceConsistency = boardRowStatusConsistency(repo, commandName, found.section, found.row, "source");
  if (sourceConsistency.ok === false) return sourceConsistency;
  const targetLookup = targetBoardSection(repo, parsed.sections, targetKind, commandName);
  if (targetLookup.ok === false) return targetLookup;
  const target = targetLookup.section;
  const { section: source, row } = found;
  if (!sameBoardHeaderShape(source.headers, target.headers)) {
    return validationError(commandName, repo, "source and target board tables have different headers; refusing to move a raw row across mismatched columns", {
      issueNumber,
      source: { section: source.kind, headers: source.headers, headerLine: source.headerLine },
      target: { section: target.kind, headers: target.headers, headerLine: target.headerLine },
    }) as GitHubCommandFailure;
  }
  if (row.cells.length !== target.headers.length) {
    return validationError(commandName, repo, "source row column count does not match the target board table header", {
      issueNumber,
      source: { section: source.kind, lineNumber: row.lineNumber, rowPreview: preview(row.raw), cells: row.cells.length },
      target: { section: target.kind, headers: target.headers, columns: target.headers.length },
    }) as GitHubCommandFailure;
  }
  const columnMap = boardColumnIndexMap(target.headers);
  const githubStatusColumnIndex = columnMap.get("githubStatus");
  if (githubStatusColumnIndex === undefined) {
    return validationError(commandName, repo, "target board table does not contain a GitHub 状态 column required for section migration", {
      issueNumber,
      targetSection: targetKind,
      headers: target.headers,
    }) as GitHubCommandFailure;
  }
  const oldCells = row.cells.slice();
  const newCells = oldCells.slice();
  const expectedStatus = expectedBoardGithubStatus(targetKind);
  if (githubStatusColumnIndex !== undefined) newCells[githubStatusColumnIndex] = status ?? expectedStatus;
  const newRow = renderMarkdownTableRow(newCells);
  const oldBody = boardIssue.body ?? "";
  const insertAfterLine = boardSectionInsertAfterLine(target);
  let newBody: string;
  try {
    newBody = source.kind === target.kind
      ? replaceBoardBodyLine(oldBody, row.lineNumber, newRow)
      : moveBoardBodyRow(oldBody, row.lineNumber, insertAfterLine, newRow);
  } catch (error) {
    return validationError(commandName, repo, error instanceof Error ? error.message : String(error), {
      issueNumber,
      sourceLineNumber: row.lineNumber,
      insertAfterLine,
    }) as GitHubCommandFailure;
  }
  return {
    ok: true,
    newBody,
    plan: {
      repo,
      boardIssue: boardIssue.number,
      issueNumber,
      action: source.kind === target.kind ? "move-within-section" : "move",
      from: source.kind,
      to: target.kind,
      sourceLineNumber: row.lineNumber,
      insertAfterLine,
      oldRow: row.raw,
      newRow,
      oldCells,
      newCells,
      row: {
        old: boardRowSummary(source, row),
        new: boardRowSummary(target, { ...row, section: target.kind, raw: newRow, cells: newCells, lineNumber: source.kind === target.kind ? row.lineNumber : insertAfterLine + 1 }),
      },
      linePlan: {
        action: source.kind === target.kind ? "replace" : "move",
        sourceLineNumber: row.lineNumber,
        insertAfterLine,
        newLineNumber: source.kind === target.kind ? row.lineNumber : insertAfterLine + 1,
      },
      sectionPlan: {
        from: source.kind,
        to: target.kind,
        sourceHeading: source.heading,
        targetHeading: target.heading,
      },
      status: {
        requested: status ?? null,
        targetDefault: expectedStatus,
        githubStatusColumnIndex: githubStatusColumnIndex ?? null,
        old: oldCells[githubStatusColumnIndex] ?? "",
        new: newCells[githubStatusColumnIndex] ?? "",
      },
      body: {
        oldBodyChars: oldBody.length,
        oldBodySha: bodySha(oldBody),
        newBodyChars: newBody.length,
        newBodySha: bodySha(newBody),
        changed: oldBody !== newBody,
      },
      tableParser: {
        reused: "parseBoardTables",
        rowValidationWarnings: parsed.warnings.length,
      },
      request: {
        method: "PATCH",
        path: `/repos/{owner}/{repo}/issues/${boardIssue.number}`,
        body: { bodyChars: newBody.length },
      },
    },
  };
}

export function boardRowDeletePlan(
  repo: string,
  boardIssue: GitHubIssue,
  issueNumber: number,
  parsed: { sections: BoardTableSection[]; warnings: BoardRowValidationWarning[] },
): BoardRowMutationPlanResult | GitHubCommandFailure {
  const commandName = "issue board-row delete";
  const found = findBoardRow(repo, parsed.sections, issueNumber);
  if (found.ok === false) return { ...found, command: commandName, repo } as GitHubCommandFailure;
  const { section, row } = found;
  const sourceConsistency = boardRowStatusConsistency(repo, commandName, section, row, "source");
  if (sourceConsistency.ok === false) return sourceConsistency;
  const oldBody = boardIssue.body ?? "";
  let newBody: string;
  try {
    newBody = removeBoardBodyLine(oldBody, row.lineNumber);
  } catch (error) {
    return validationError(commandName, repo, error instanceof Error ? error.message : String(error), {
      issueNumber,
      lineNumber: row.lineNumber,
    }) as GitHubCommandFailure;
  }
  return {
    ok: true,
    newBody,
    plan: {
      repo,
      boardIssue: boardIssue.number,
      issueNumber,
      action: "delete",
      section: section.kind,
      lineNumber: row.lineNumber,
      oldRow: row.raw,
      oldCells: row.cells,
      row: {
        old: boardRowSummary(section, row),
        new: null,
      },
      linePlan: {
        action: "remove",
        section: section.kind,
        lineNumber: row.lineNumber,
      },
      sectionPlan: {
        kind: section.kind,
        heading: section.heading,
        headerLine: section.headerLine,
        rowCount: section.rows.length,
      },
      body: {
        oldBodyChars: oldBody.length,
        oldBodySha: bodySha(oldBody),
        newBodyChars: newBody.length,
        newBodySha: bodySha(newBody),
        changed: oldBody !== newBody,
      },
      tableParser: {
        reused: "parseBoardTables",
        rowValidationWarnings: parsed.warnings.length,
      },
      request: {
        method: "PATCH",
        path: `/repos/{owner}/{repo}/issues/${boardIssue.number}`,
        body: { bodyChars: newBody.length },
      },
    },
  };
}
