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

import path from "node:path";
import { bodySafetySignals, bodySha, preview, repoParts } from "./auth-and-safety";
import { boardBodyGuardSummary, boardBodySafetyIssue, boardRowsForState, boardRowSummary, boardRowValuePollutionEvidence, findBoardRow, parseBoardTables, readBoardRowFile } from "./board-parser";
import { boardRowAddPlan, boardRowDeletePlan, boardRowMovePlan, boardRowUpdatePlan, boardRowUpsertPlan } from "./board-plans";
import { assertConcurrencyOptions, validateIssueConcurrency } from "./body-guards";
import { codeQueueBoardCommanderBriefHint } from "./body-profiles";
import { commandError, githubRequest, isGitHubError, issueBodyReadCommands, issueWriteDisclosure, validationError } from "./client";
import { getIssue } from "./issue-read";
import { issueSummary } from "./issue-summary";
import { BOARD_MUTATION_SECTIONS, BOARD_ROW_FIELDS } from "./types";
import type { BoardMutationSection, BoardRowMutationPlanResult, BoardRowValidationWarning, BoardTableSection, GitHubCommandFailure, GitHubCommandResult, GitHubIssue, GitHubOptions } from "./types";

export async function issueBoardRowMutation(
  repo: string,
  token: string,
  issueNumber: number,
  options: GitHubOptions,
  commandName: "issue board-row add" | "issue board-row move" | "issue board-row delete" | "issue board-row upsert",
  mutationKey: "add" | "move" | "delete" | "upsert",
  buildPlan: (boardIssue: GitHubIssue, parsed: { sections: BoardTableSection[]; warnings: BoardRowValidationWarning[] }) => BoardRowMutationPlanResult | GitHubCommandFailure,
): Promise<GitHubCommandResult> {
  const concurrencyOptionError = assertConcurrencyOptions(options, commandName);
  if (concurrencyOptionError !== null) return { ...concurrencyOptionError, command: commandName, repo };
  const boardIssue = await getIssue(token, repo, options.boardIssue);
  if (isGitHubError(boardIssue)) return commandError(commandName, repo, boardIssue, { boardIssue: options.boardIssue, issueNumber });
  const parsed = parseBoardTables(boardIssue.body ?? "");
  const planned = buildPlan(boardIssue, parsed);
  if (planned.ok === false) return planned;
  const guardError = boardBodySafetyIssue(repo, options.boardIssue, planned.newBody, options);
  const guard = boardBodyGuardSummary(options.boardIssue, planned.newBody, options);
  const hasConcurrencyGuard = options.expectBodySha !== undefined || options.expectUpdatedAt !== undefined;
  const effectiveDryRun = options.dryRun || !hasConcurrencyGuard;
  const base = {
    ok: true,
    command: commandName,
    repo,
    boardIssue: {
      number: boardIssue.number,
      title: boardIssue.title,
      state: boardIssue.state,
      url: boardIssue.html_url,
      bodyChars: (boardIssue.body ?? "").length,
      bodySha: bodySha(boardIssue.body ?? ""),
      updatedAt: boardIssue.updated_at ?? null,
    },
    codeQueueBoardHint: codeQueueBoardCommanderBriefHint(options.boardIssue, planned.newBody),
    issueNumber,
    operation: typeof planned.plan.operation === "string" ? planned.plan.operation : mutationKey,
    dryRun: effectiveDryRun,
    planned: true,
    [mutationKey]: planned.plan,
    guard,
    bodyOnlySafety: {
      oldBody: {
        fetched: true,
        bodyChars: (boardIssue.body ?? "").length,
        bodySha: bodySha(boardIssue.body ?? ""),
        updatedAt: boardIssue.updated_at ?? null,
      },
      newBody: {
        bodyChars: planned.newBody.length,
        bodySha: bodySha(planned.newBody),
        ...bodySafetySignals(planned.newBody),
      },
    },
    concurrency: {
      expectUpdatedAt: options.expectUpdatedAt ?? null,
      expectBodySha: options.expectBodySha ?? null,
      note: `non-dry-run board-row ${mutationKey} requires --expect-body-sha or --expect-updated-at and checks the current board issue before PATCH`,
    },
  };
  if (guardError !== null) return { ...guardError, command: commandName, [mutationKey]: planned.plan, guard };
  if (effectiveDryRun) {
    return {
      ...base,
      dryRun: true,
      disclosure: issueWriteDisclosure(options, repo, options.boardIssue, true),
      readCommands: issueBodyReadCommands(repo, options.boardIssue),
      wouldPatch: { issueNumber: options.boardIssue, bodySha: bodySha(planned.newBody), bodyChars: planned.newBody.length },
      note: !hasConcurrencyGuard
        ? "Default dry-run because no concurrency expectation was supplied; no GitHub issue body was modified."
        : "Dry-run only; no GitHub issue body was modified.",
    };
  }
  const concurrencyError = validateIssueConcurrency(repo, options.boardIssue, boardIssue, options);
  if (concurrencyError !== null) return { ...base, ...concurrencyError, command: commandName };
  const { owner, name } = repoParts(repo);
  const issue = await githubRequest<GitHubIssue>(token, "PATCH", `/repos/${owner}/${name}/issues/${options.boardIssue}`, { body: planned.newBody });
  if (isGitHubError(issue)) return commandError(commandName, repo, issue, { issueNumber, [mutationKey]: planned.plan });
  return {
    ...base,
    dryRun: false,
    planned: false,
    issue: issueSummary(issue, { includeBody: options.raw || options.full }),
    disclosure: issueWriteDisclosure(options, repo, options.boardIssue, false),
    readCommands: issueBodyReadCommands(repo, options.boardIssue),
    concurrency: {
      checked: true,
      oldIssueUpdatedAt: boardIssue.updated_at ?? null,
      oldBodySha: bodySha(boardIssue.body ?? ""),
      expectUpdatedAt: options.expectUpdatedAt ?? null,
      expectBodySha: options.expectBodySha ?? null,
    },
    rest: true,
  };
}

export async function issueBoardRowList(repo: string, token: string, options: GitHubOptions): Promise<GitHubCommandResult> {
  const commandName = "issue board-row list";
  const boardIssue = await getIssue(token, repo, options.boardIssue);
  if (isGitHubError(boardIssue)) return commandError(commandName, repo, boardIssue, { boardIssue: options.boardIssue });
  const parsed = parseBoardTables(boardIssue.body ?? "");
  const rows = boardRowsForState(parsed.sections, options.listState);
  return {
    ok: true,
    command: commandName,
    repo,
    dryRun: true,
    readOnly: true,
    codeQueueBoardHint: codeQueueBoardCommanderBriefHint(options.boardIssue, boardIssue.body ?? ""),
    boardIssue: {
      number: boardIssue.number,
      title: boardIssue.title,
      state: boardIssue.state,
      url: boardIssue.html_url,
      bodyChars: (boardIssue.body ?? "").length,
      bodySha: bodySha(boardIssue.body ?? ""),
      updatedAt: boardIssue.updated_at ?? null,
    },
    state: options.listState,
    count: rows.length,
    rows: rows.map(({ section, row }) => boardRowSummary(section, row)),
    sections: parsed.sections.map((section) => ({ kind: section.kind, heading: section.heading, headingLine: section.headingLine, headerLine: section.headerLine, headers: section.headers, rows: section.rows.length })),
    rowValidationWarnings: parsed.warnings,
    note: "Read-only board row list; no issue body was edited.",
  };
}

export async function issueBoardRowGet(repo: string, token: string, issueNumber: number, options: GitHubOptions): Promise<GitHubCommandResult> {
  const commandName = "issue board-row get";
  const boardIssue = await getIssue(token, repo, options.boardIssue);
  if (isGitHubError(boardIssue)) return commandError(commandName, repo, boardIssue, { boardIssue: options.boardIssue, issueNumber });
  const parsed = parseBoardTables(boardIssue.body ?? "");
  const found = findBoardRow(repo, parsed.sections, issueNumber);
  if (found.ok === false) return { ...found, command: commandName, repo, boardIssue: options.boardIssue };
  return {
    ok: true,
    command: commandName,
    repo,
    dryRun: true,
    readOnly: true,
    codeQueueBoardHint: codeQueueBoardCommanderBriefHint(options.boardIssue, boardIssue.body ?? ""),
    boardIssue: {
      number: boardIssue.number,
      title: boardIssue.title,
      state: boardIssue.state,
      url: boardIssue.html_url,
      bodyChars: (boardIssue.body ?? "").length,
      bodySha: bodySha(boardIssue.body ?? ""),
      updatedAt: boardIssue.updated_at ?? null,
    },
    issueNumber,
    row: boardRowSummary(found.section, found.row),
    rowValidationWarnings: parsed.warnings.filter((warning) => warning.issueNumber === issueNumber),
    note: "Read-only board row get; no issue body was edited.",
  };
}

export async function issueBoardRowUpdate(repo: string, token: string, issueNumber: number, options: GitHubOptions): Promise<GitHubCommandResult> {
  const commandName = "issue board-row update";
  if (options.boardRowField === undefined) return validationError(commandName, repo, "issue board-row update requires --field progress|status|validation|branch|tasks|focus", { supportedFields: BOARD_ROW_FIELDS.slice() });
  if (options.boardRowValue === undefined) return validationError(commandName, repo, "issue board-row update requires --value <text>", { issueNumber, field: options.boardRowField });
  const valuePollutionEvidence = boardRowValuePollutionEvidence(options.boardRowValue);
  if (valuePollutionEvidence.length > 0) {
    return validationError(commandName, repo, "issue board-row update --value contains literal shell escape text that would pollute the board cell; pass real newlines or plain text instead", {
      issueNumber,
      field: options.boardRowField,
      valuePreview: preview(options.boardRowValue),
      shellPollution: { suspected: true, evidence: valuePollutionEvidence },
    });
  }
  const concurrencyOptionError = assertConcurrencyOptions(options, commandName);
  if (concurrencyOptionError !== null) return { ...concurrencyOptionError, command: commandName, repo };

  const boardIssue = await getIssue(token, repo, options.boardIssue);
  if (isGitHubError(boardIssue)) return commandError(commandName, repo, boardIssue, { boardIssue: options.boardIssue, issueNumber });
  const parsed = parseBoardTables(boardIssue.body ?? "");
  const planned = boardRowUpdatePlan(repo, boardIssue, issueNumber, options.boardRowField, options.boardRowValue, parsed);
  if (planned.ok === false) return planned;
  const guardError = boardBodySafetyIssue(repo, options.boardIssue, planned.newBody, options);
  const guard = boardBodyGuardSummary(options.boardIssue, planned.newBody, options);
  const effectiveDryRun = options.dryRun || (options.expectBodySha === undefined && options.expectUpdatedAt === undefined);
  const base = {
    ok: true,
    command: commandName,
    repo,
    boardIssue: {
      number: boardIssue.number,
      title: boardIssue.title,
      state: boardIssue.state,
      url: boardIssue.html_url,
      bodyChars: (boardIssue.body ?? "").length,
      bodySha: bodySha(boardIssue.body ?? ""),
      updatedAt: boardIssue.updated_at ?? null,
    },
    codeQueueBoardHint: codeQueueBoardCommanderBriefHint(options.boardIssue, planned.newBody),
    issueNumber,
    dryRun: effectiveDryRun,
    planned: true,
    update: planned.plan,
    guard,
    bodyOnlySafety: {
      oldBody: {
        fetched: true,
        bodyChars: (boardIssue.body ?? "").length,
        bodySha: bodySha(boardIssue.body ?? ""),
        updatedAt: boardIssue.updated_at ?? null,
      },
      newBody: {
        bodyChars: planned.newBody.length,
        bodySha: bodySha(planned.newBody),
        ...bodySafetySignals(planned.newBody),
      },
    },
    concurrency: {
      expectUpdatedAt: options.expectUpdatedAt ?? null,
      expectBodySha: options.expectBodySha ?? null,
      note: "non-dry-run board-row update requires --expect-body-sha or --expect-updated-at and checks the current board issue before PATCH",
    },
  };
  if (guardError !== null) return { ...guardError, command: commandName, update: planned.plan, guard };
  if (effectiveDryRun) {
    return {
      ...base,
      dryRun: true,
      disclosure: issueWriteDisclosure(options, repo, options.boardIssue, true),
      readCommands: issueBodyReadCommands(repo, options.boardIssue),
      wouldPatch: { issueNumber: options.boardIssue, bodySha: bodySha(planned.newBody), bodyChars: planned.newBody.length },
      note: options.expectBodySha === undefined && options.expectUpdatedAt === undefined
        ? "Default dry-run because no concurrency expectation was supplied; no GitHub issue body was modified."
        : "Dry-run only; no GitHub issue body was modified.",
    };
  }
  const concurrencyError = validateIssueConcurrency(repo, options.boardIssue, boardIssue, options);
  if (concurrencyError !== null) return { ...base, ...concurrencyError, command: commandName };
  const { owner, name } = repoParts(repo);
  const issue = await githubRequest<GitHubIssue>(token, "PATCH", `/repos/${owner}/${name}/issues/${options.boardIssue}`, { body: planned.newBody });
  if (isGitHubError(issue)) return commandError(commandName, repo, issue, { issueNumber, update: planned.plan });
  return {
    ...base,
    dryRun: false,
    planned: false,
    issue: issueSummary(issue, { includeBody: options.raw || options.full }),
    disclosure: issueWriteDisclosure(options, repo, options.boardIssue, false),
    readCommands: issueBodyReadCommands(repo, options.boardIssue),
    concurrency: {
      checked: true,
      oldIssueUpdatedAt: boardIssue.updated_at ?? null,
      oldBodySha: bodySha(boardIssue.body ?? ""),
      expectUpdatedAt: options.expectUpdatedAt ?? null,
      expectBodySha: options.expectBodySha ?? null,
    },
    rest: true,
  };
}

export async function issueBoardRowAdd(repo: string, token: string, issueNumber: number, options: GitHubOptions): Promise<GitHubCommandResult> {
  const commandName = "issue board-row add";
  if (options.boardSection === undefined) return validationError(commandName, repo, "issue board-row add requires --section open|closed", { issueNumber, supportedSections: BOARD_MUTATION_SECTIONS.slice() });
  let rowFile: { path: string; raw: string; cells: string[] };
  try {
    rowFile = readBoardRowFile(options.boardRowFile, commandName);
  } catch (error) {
    return validationError(commandName, repo, error instanceof Error ? error.message : String(error), {
      issueNumber,
      rowFile: options.boardRowFile ?? null,
    });
  }
  return issueBoardRowMutation(
    repo,
    token,
    issueNumber,
    options,
    commandName,
    "add",
    (boardIssue, parsed) => boardRowAddPlan(repo, boardIssue, issueNumber, options.boardSection as BoardMutationSection, rowFile, parsed),
  );
}

export async function issueBoardRowUpsert(repo: string, token: string, issueNumber: number, options: GitHubOptions): Promise<GitHubCommandResult> {
  const commandName = "issue board-row upsert";
  return issueBoardRowMutation(
    repo,
    token,
    issueNumber,
    options,
    commandName,
    "upsert",
    (boardIssue, parsed) => boardRowUpsertPlan(repo, boardIssue, issueNumber, options, parsed),
  );
}

export async function issueBoardRowMove(repo: string, token: string, issueNumber: number, options: GitHubOptions): Promise<GitHubCommandResult> {
  const commandName = "issue board-row move";
  if (options.boardMoveTo === undefined) return validationError(commandName, repo, "issue board-row move requires --to open|closed", { issueNumber, supportedTargets: BOARD_MUTATION_SECTIONS.slice() });
  return issueBoardRowMutation(
    repo,
    token,
    issueNumber,
    options,
    commandName,
    "move",
    (boardIssue, parsed) => boardRowMovePlan(repo, boardIssue, issueNumber, options.boardMoveTo as BoardMutationSection, options.boardGithubStatus, parsed),
  );
}

export async function issueBoardRowDelete(repo: string, token: string, issueNumber: number, options: GitHubOptions): Promise<GitHubCommandResult> {
  const commandName = "issue board-row delete";
  return issueBoardRowMutation(
    repo,
    token,
    issueNumber,
    options,
    commandName,
    "delete",
    (boardIssue, parsed) => boardRowDeletePlan(repo, boardIssue, issueNumber, parsed),
  );
}
