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

import path from "node:path";
import { existsSync, readFileSync } from "node:fs";
import { preview, shellPollutionEvidence } from "./auth-and-safety";
import { isDailyCommanderBriefTitle, issueEditGuardSummary, validateIssueBodyGuard } from "./body-guards";
import { normalizeNewlines } from "./body-profiles";
import { validationError } from "./client";
import { BOARD_AUDIT_REQUIRED_COLUMNS, CODE_QUEUE_BOARD_TARGET_ISSUE, DEFAULT_BOARD_KNOWN_META_ISSUES } from "./types";
import type { BoardColumnKind, BoardIgnoreReason, BoardIssueEntry, BoardMutationSection, BoardRequiredColumn, BoardRowField, BoardRowLookup, BoardRowUpsertField, BoardRowValidationWarning, BoardSectionKind, BoardTableRow, BoardTableSection, GitHubCommandFailure, GitHubCommandResult, GitHubIssue, GitHubOptions, IssueListState } from "./types";

export function boardIssueEntry(issue: GitHubIssue): BoardIssueEntry {
  return {
    number: issue.number,
    title: issue.title,
    state: issue.state,
    url: issue.html_url,
  };
}

export function sortedIssueEntries(issues: BoardIssueEntry[]): BoardIssueEntry[] {
  return issues.slice().sort((a, b) => a.number - b.number);
}

export function mergedKnownMetaIssues(options: GitHubOptions): number[] {
  const seen = new Set<number>();
  const values: number[] = [];
  for (const issueNumber of [...DEFAULT_BOARD_KNOWN_META_ISSUES, ...options.knownMetaIssues]) {
    if (seen.has(issueNumber)) continue;
    seen.add(issueNumber);
    values.push(issueNumber);
  }
  return values;
}

export function boardIgnoreMap(options: GitHubOptions, issues: BoardIssueEntry[] = []): Map<number, BoardIgnoreReason> {
  const ignored = new Map<number, BoardIgnoreReason>();
  for (const issueNumber of mergedKnownMetaIssues(options)) ignored.set(issueNumber, "known-meta");
  for (const issueNumber of options.ignoredIssues) ignored.set(issueNumber, "ignored");
  if (options.boardIssue === CODE_QUEUE_BOARD_TARGET_ISSUE) {
    for (const issue of issues) {
      if (!ignored.has(issue.number) && isDailyCommanderBriefTitle(issue.title)) ignored.set(issue.number, "brief-index-managed");
    }
  }
  return ignored;
}

export function stripMarkdownInline(text: string): string {
  return text
    .replace(/\[[^\]]*]\(([^)]*)\)/g, "$1")
    .replace(/<[^>]+>/g, "")
    .replace(/[*_`~]/g, "")
    .trim();
}

export function normalizeBoardHeader(header: string): string {
  return stripMarkdownInline(header).toLowerCase().replace(/\s+/g, "");
}

export function boardHeaderColumnKind(header: string): BoardColumnKind | null {
  const normalized = normalizeBoardHeader(header);
  if (["category", "类别", "分类", "类型", "任务类型"].includes(normalized)) return "category";
  if (["branch", "分支", "目标分支", "工作分支"].includes(normalized)) return "branch";
  if (["acceptance", "验收", "验收状态", "验收结果", "验收标准"].includes(normalized)) return "acceptance";
  if (["relatedtask", "task", "codequeue", "codequeuetask", "相关任务", "任务", "codequeue任务", "cq任务", "相关codequeue任务", "相关codequeuetask"].includes(normalized)) return "relatedTask";
  if (["githubstatus", "githubstate", "github状态", "githubissue状态", "issuestate", "issue状态", "issuegithub状态", "开闭状态", "openclosed", "openclosed状态"].includes(normalized)) return "githubStatus";
  if (["summary", "摘要", "概述", "任务摘要", "任务概述", "标题"].includes(normalized)) return "summary";
  if (["focus", "currentfocus", "关注点", "当前关注点", "当前focus"].includes(normalized)) return "focus";
  if (["progress", "进度", "状态", "当前进度"].includes(normalized)) return "progress";
  return null;
}

export function boardColumnIndexMap(headers: string[]): Map<BoardColumnKind, number> {
  const map = new Map<BoardColumnKind, number>();
  headers.forEach((header, headerIndex) => {
    const kind = boardHeaderColumnKind(header);
    if (kind !== null && !map.has(kind)) map.set(kind, headerIndex);
  });
  return map;
}

export function boardRequiredColumnIndexMap(headers: string[]): Map<BoardRequiredColumn, number> {
  const map = new Map<BoardRequiredColumn, number>();
  const allColumns = boardColumnIndexMap(headers);
  for (const column of BOARD_AUDIT_REQUIRED_COLUMNS) {
    const index = allColumns.get(column);
    if (index !== undefined) map.set(column, index);
  }
  return map;
}

export function boardColumnKindForField(field: BoardRowField): BoardColumnKind {
  if (field === "branch") return "branch";
  if (field === "tasks") return "relatedTask";
  if (field === "focus") return "focus";
  if (field === "progress") return "progress";
  return "acceptance";
}

export function boardIssueColumnIndex(headers: string[]): number | null {
  for (let index = 0; index < headers.length; index += 1) {
    const normalized = normalizeBoardHeader(headers[index]);
    if (["issue", "issue#", "issuenumber", "issueid", "issue编号", "issue号", "编号"].includes(normalized)) return index;
  }
  return null;
}

export function markdownCells(line: string): string[] {
  const trimmed = line.trim();
  const withoutOuterPipes = trimmed.replace(/^\|/u, "").replace(/\|$/u, "");
  const cells: string[] = [];
  let current = "";
  for (let index = 0; index < withoutOuterPipes.length; index += 1) {
    const char = withoutOuterPipes[index];
    const previous = withoutOuterPipes[index - 1];
    if (char === "|" && previous !== "\\") {
      cells.push(current.trim().replace(/\\\|/g, "|"));
      current = "";
      continue;
    }
    current += char;
  }
  cells.push(current.trim().replace(/\\\|/g, "|"));
  return cells;
}

export function isMarkdownTableSeparator(line: string): boolean {
  const cells = markdownCells(line);
  return cells.length > 0 && cells.every((cell) => /^:?-{3,}:?$/u.test(cell.trim()));
}

export function boardSectionKindFromHeading(line: string): BoardSectionKind | null {
  const normalized = line.toLowerCase();
  if (!line.trimStart().startsWith("#")) return null;
  if (normalized.includes("open") || normalized.includes("看板（open）") || normalized.includes("看板(open)") || normalized.includes("开放")) return "open";
  if (normalized.includes("closed") || normalized.includes("看板（closed）") || normalized.includes("看板(closed)") || normalized.includes("关闭") || normalized.includes("已关闭")) return "closed";
  return null;
}

export function extractIssueNumbers(text: string): number[] {
  const numbers: number[] = [];
  const seen = new Set<number>();
  const patterns = [/#(\d+)/g, /\/issues\/(\d+)/g];
  for (const pattern of patterns) {
    pattern.lastIndex = 0;
    let match = pattern.exec(text);
    while (match !== null) {
      const value = Number(match[1]);
      if (Number.isInteger(value) && value > 0 && !seen.has(value)) {
        seen.add(value);
        numbers.push(value);
      }
      match = pattern.exec(text);
    }
  }
  return numbers;
}

export function primaryBoardIssueNumberFromIssueCell(issueCell: string | undefined): number | null {
  if (issueCell === undefined) return null;
  const markdownLinkPattern = /\[[^\]]*]\(([^)]*)\)/g;
  let match = markdownLinkPattern.exec(issueCell);
  while (match !== null) {
    const issueNumber = /\/issues\/(\d+)/u.exec(match[1] ?? "")?.[1];
    if (issueNumber !== undefined) {
      const value = Number(issueNumber);
      if (Number.isInteger(value) && value > 0) return value;
    }
    match = markdownLinkPattern.exec(issueCell);
  }
  const leadingIssueReference = /^\s*#(\d+)\b/u.exec(issueCell)?.[1];
  if (leadingIssueReference !== undefined) {
    const value = Number(leadingIssueReference);
    if (Number.isInteger(value) && value > 0) return value;
  }
  return null;
}

export function boardRowFromCells(section: BoardTableSection, raw: string, cells: string[], lineNumber: number): BoardTableRow {
  const columnMap = boardRequiredColumnIndexMap(section.headers);
  const issueColumnIndex = boardIssueColumnIndex(section.headers);
  const issueCell = issueColumnIndex === null ? undefined : cells[issueColumnIndex];
  const primaryIssueNumber = issueColumnIndex === null ? null : primaryBoardIssueNumberFromIssueCell(issueCell);
  const issueNumbers = issueColumnIndex === null
    ? extractIssueNumbers(raw)
    : (primaryIssueNumber === null ? extractIssueNumbers(issueCell ?? "") : [primaryIssueNumber]);
  const columns: Partial<Record<BoardRequiredColumn, string>> = {};
  for (const column of BOARD_AUDIT_REQUIRED_COLUMNS) {
    const cellIndex = columnMap.get(column);
    if (cellIndex !== undefined) columns[column] = cells[cellIndex] ?? "";
  }
  return {
    section: section.kind,
    lineNumber,
    raw,
    cells,
    issueNumbers,
    issueNumber: issueNumbers[0] ?? null,
    title: issueColumnIndex === null
      ? (cells.find((cell) => extractIssueNumbers(cell).length > 0) ?? cells[0] ?? null)
      : (issueCell ?? cells[0] ?? null),
    columns,
  };
}

export function normalizedBoardCellPlaceholder(value: string): { spaced: string; compact: string } {
  const spaced = stripMarkdownInline(value).toLowerCase().replace(/\s+/g, " ").trim();
  return { spaced, compact: spaced.replace(/\s+/g, "") };
}

export function isRelatedTaskNoTaskPlaceholder(value: string): boolean {
  const normalized = normalizedBoardCellPlaceholder(value);
  return [
    "-",
    "—",
    "–",
    "n/a",
    "na",
    "none",
    "no task",
    "no-task",
    "no code queue task",
    "not applicable",
  ].includes(normalized.spaced) || [
    "无",
    "无任务",
    "无关联任务",
    "无相关任务",
    "无codequeue任务",
    "无codequeuetask",
    "不适用",
  ].includes(normalized.compact);
}

export function cellHasMeaningfulValue(value: string | undefined, column: BoardRequiredColumn): boolean {
  if (value === undefined) return false;
  const stripped = stripMarkdownInline(value);
  if (stripped.length === 0) return false;
  if (column === "relatedTask" && isRelatedTaskNoTaskPlaceholder(value)) return true;
  return !["-", "—", "n/a", "na", "todo", "tbd", "待补", "未填", "无"].includes(stripped.toLowerCase());
}

export function parseBoardTables(body: string): { sections: BoardTableSection[]; warnings: BoardRowValidationWarning[] } {
  const lines = normalizeNewlines(body).split("\n");
  const sections: BoardTableSection[] = [];
  const warnings: BoardRowValidationWarning[] = [];
  let currentKind: BoardSectionKind | null = null;
  let currentHeading = "";
  let currentHeadingLine = 0;
  for (let index = 0; index < lines.length; index += 1) {
    const headingKind = boardSectionKindFromHeading(lines[index]);
    if (headingKind !== null) {
      currentKind = headingKind;
      currentHeading = lines[index].trim();
      currentHeadingLine = index + 1;
      continue;
    }
    if (currentKind === null) continue;
    if (!lines[index].trim().startsWith("|")) continue;
    const separator = lines[index + 1];
    if (separator === undefined || !separator.trim().startsWith("|") || !isMarkdownTableSeparator(separator)) continue;
    const headers = markdownCells(lines[index]);
    const rows: BoardTableRow[] = [];
    let rowIndex = index + 2;
    while (rowIndex < lines.length && lines[rowIndex].trim().startsWith("|")) {
      if (!isMarkdownTableSeparator(lines[rowIndex])) {
        const cells = markdownCells(lines[rowIndex]);
        const row = boardRowFromCells(
          { kind: currentKind, heading: currentHeading, headingLine: currentHeadingLine, headerLine: index + 1, headers, rows: [] },
          lines[rowIndex],
          cells,
          rowIndex + 1,
        );
        rows.push(row);
        if (row.issueNumbers.length === 0) {
          warnings.push({
            issueNumber: null,
            section: currentKind,
            lineNumber: row.lineNumber,
            kind: "missing-issue-reference",
            message: "Board table row does not contain a GitHub issue reference such as #36.",
            rowPreview: preview(row.raw),
          });
        }
        if (row.issueNumbers.length > 1) {
          warnings.push({
            issueNumber: row.issueNumber,
            section: currentKind,
            lineNumber: row.lineNumber,
            kind: "multiple-issue-references",
            message: boardIssueColumnIndex(headers) === null
              ? "Board table row contains multiple issue references; audit uses the first number as the row key."
              : "Board table row contains multiple issue references in the Issue column; audit uses the first number as the row key.",
            rowPreview: preview(row.raw),
          });
        }
        const missingColumns = BOARD_AUDIT_REQUIRED_COLUMNS.filter((column) => !cellHasMeaningfulValue(row.columns[column], column));
        if (missingColumns.length > 0) {
          warnings.push({
            issueNumber: row.issueNumber,
            section: currentKind,
            lineNumber: row.lineNumber,
            kind: "missing-required-columns",
            message: "Board table row is missing branch, acceptance, related task, or progress information.",
            missingColumns,
            columns: row.columns,
            rowPreview: preview(row.raw),
          });
        }
      }
      rowIndex += 1;
    }
    sections.push({
      kind: currentKind,
      heading: currentHeading,
      headingLine: currentHeadingLine,
      headerLine: index + 1,
      headers,
      rows,
    });
    index = rowIndex - 1;
  }
  return { sections, warnings };
}

export function boardRowsByIssue(rows: BoardTableRow[]): Map<number, BoardTableRow[]> {
  const map = new Map<number, BoardTableRow[]>();
  for (const row of rows) {
    if (row.issueNumber === null) continue;
    const list = map.get(row.issueNumber);
    if (list === undefined) map.set(row.issueNumber, [row]);
    else list.push(row);
  }
  return map;
}

export function boardRowFieldValues(section: BoardTableSection, row: BoardTableRow): Record<BoardRowField, string | null> {
  const columnMap = boardColumnIndexMap(section.headers);
  const valueFor = (kind: BoardColumnKind): string | null => {
    const index = columnMap.get(kind);
    return index === undefined ? null : row.cells[index] ?? "";
  };
  const acceptance = valueFor("acceptance");
  return {
    progress: valueFor("progress"),
    status: acceptance,
    validation: acceptance,
    branch: valueFor("branch"),
    tasks: valueFor("relatedTask"),
    focus: valueFor("focus"),
  };
}

export function boardRowUpsertFieldValues(section: BoardTableSection, row: BoardTableRow): Record<BoardRowUpsertField, string | null> {
  const columnMap = boardColumnIndexMap(section.headers);
  const valueFor = (kind: BoardColumnKind): string | null => {
    const index = columnMap.get(kind);
    return index === undefined ? null : row.cells[index] ?? "";
  };
  const acceptance = valueFor("acceptance");
  return {
    category: valueFor("category"),
    branch: valueFor("branch"),
    tasks: valueFor("relatedTask"),
    summary: valueFor("summary"),
    focus: valueFor("focus"),
    validation: acceptance,
    progress: valueFor("progress"),
    status: valueFor("githubStatus"),
  };
}

export function boardRowSummary(section: BoardTableSection, row: BoardTableRow): Record<string, unknown> {
  return {
    issueNumber: row.issueNumber,
    section: row.section,
    lineNumber: row.lineNumber,
    heading: section.heading,
    headers: section.headers,
    title: row.title,
    cells: row.cells,
    fields: boardRowFieldValues(section, row),
    raw: row.raw,
    rowPreview: preview(row.raw),
  };
}

export function boardRowsExactMatch(
  sections: BoardTableSection[],
  issueNumber: number,
): Array<{ section: BoardTableSection; row: BoardTableRow }> {
  return boardRowsForState(sections, "all").filter(({ row }) => row.issueNumber === issueNumber);
}

export function boardRowsForState(sections: BoardTableSection[], state: IssueListState): Array<{ section: BoardTableSection; row: BoardTableRow }> {
  return sections
    .filter((section) => state === "all" || section.kind === state)
    .flatMap((section) => section.rows.map((row) => ({ section, row })));
}

export function boardDuplicateRows(
  sections: BoardTableSection[],
  issueNumber: number,
): Array<{ section: BoardTableSection; row: BoardTableRow }> {
  return boardRowsForState(sections, "all").filter(({ row }) => row.issueNumber === issueNumber);
}

export function targetBoardSection(
  repo: string,
  sections: BoardTableSection[],
  kind: BoardMutationSection,
  commandName: string,
): { ok: true; section: BoardTableSection } | GitHubCommandFailure {
  const matches = sections.filter((section) => section.kind === kind);
  if (matches.length === 0) {
    return validationError(commandName, repo, `board ${kind.toUpperCase()} table was not found`, {
      section: kind,
      availableSections: sections.map((section) => ({ kind: section.kind, heading: section.heading, headerLine: section.headerLine })),
    }) as GitHubCommandFailure;
  }
  if (matches.length > 1) {
    return validationError(commandName, repo, `board ${kind.toUpperCase()} table is ambiguous`, {
      section: kind,
      matches: matches.map((section) => ({ heading: section.heading, headingLine: section.headingLine, headerLine: section.headerLine })),
    }) as GitHubCommandFailure;
  }
  return { ok: true, section: matches[0] };
}

export function findBoardRow(
  repo: string,
  sections: BoardTableSection[],
  issueNumber: number,
  state: IssueListState = "all",
): BoardRowLookup | GitHubCommandFailure {
  const matches = boardRowsForState(sections, state).filter(({ row }) => row.issueNumber === issueNumber);
  if (matches.length === 0) {
    return validationError("issue board-row", repo, `board row for issue #${issueNumber} was not found`, {
      issueNumber,
      state,
      availableIssueNumbers: boardRowsForState(sections, state)
        .map(({ row }) => row.issueNumber)
        .filter((value): value is number => value !== null)
        .sort((a, b) => a - b),
    }) as GitHubCommandFailure;
  }
  if (matches.length > 1) {
    return validationError("issue board-row", repo, `board row for issue #${issueNumber} is ambiguous`, {
      issueNumber,
      state,
      matches: matches.map(({ section, row }) => ({ section: section.kind, lineNumber: row.lineNumber, rowPreview: preview(row.raw) })),
    }) as GitHubCommandFailure;
  }
  return { ok: true, ...matches[0], matches };
}

export function escapeMarkdownTableCell(value: string): string {
  return value
    .replace(/\r\n/g, " ")
    .replace(/[\r\n]+/g, " ")
    .replace(/\|/g, "\\|")
    .trim();
}

export function boardRowValuePollutionEvidence(value: string): string[] {
  return shellPollutionEvidence(value).filter((item) => item !== "bare-carriage-return");
}

export function renderMarkdownTableRow(cells: string[]): string {
  return `| ${cells.map(escapeMarkdownTableCell).join(" | ")} |`;
}

export function validateMarkdownTableRowLine(line: string): string | null {
  if (!line.trim().startsWith("|") || !line.trim().endsWith("|")) return "row-file must contain a Markdown table row starting and ending with |";
  if (isMarkdownTableSeparator(line)) return "row-file must contain a data row, not a Markdown table separator";
  return null;
}

export function readBoardRowFile(path: string | undefined, command: string): { path: string; raw: string; cells: string[] } {
  if (path === undefined) throw new Error(`${command} requires --row-file <markdown-row-file>`);
  if (!existsSync(path)) throw new Error(`row file not found: ${path}`);
  const normalized = normalizeNewlines(readFileSync(path, "utf8")).trim();
  if (normalized.length === 0) throw new Error("row-file must contain exactly one non-empty Markdown table row");
  const lines = normalized.split("\n");
  if (lines.length !== 1) throw new Error("row-file must contain exactly one Markdown table row");
  const line = lines[0];
  const lineError = validateMarkdownTableRowLine(line);
  if (lineError !== null) throw new Error(lineError);
  return { path, raw: line, cells: markdownCells(line) };
}

export function replaceBoardBodyLine(body: string, lineNumber: number, newLine: string): string {
  const normalized = normalizeNewlines(body);
  const lines = normalized.split("\n");
  if (lineNumber <= 0 || lineNumber > lines.length) throw new Error(`line ${lineNumber} is outside the board body`);
  lines[lineNumber - 1] = newLine;
  return lines.join("\n");
}

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

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

export function boardSectionInsertAfterLine(section: BoardTableSection): number {
  if (section.rows.length === 0) return section.headerLine + 1;
  return section.rows[section.rows.length - 1].lineNumber;
}

export function boardBodySafetyIssue(repo: string, boardIssueNumber: number, body: string, options: GitHubOptions): GitHubCommandResult | null {
  const guardOptions: GitHubOptions = { ...options, bodyProfile: boardIssueNumber === CODE_QUEUE_BOARD_TARGET_ISSUE ? "code-queue-board" : options.bodyProfile };
  return validateIssueBodyGuard(repo, boardIssueNumber, body, guardOptions);
}

export function boardBodyGuardSummary(boardIssueNumber: number, body: string, options: GitHubOptions): Record<string, unknown> {
  const guardOptions: GitHubOptions = { ...options, bodyProfile: boardIssueNumber === CODE_QUEUE_BOARD_TARGET_ISSUE ? "code-queue-board" : options.bodyProfile };
  return issueEditGuardSummary(boardIssueNumber, body, guardOptions);
}
