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

import path from "node:path";
import { bodySafetySignals, bodySha, normalizeExpectedSha, preview, previewLines } from "./auth-and-safety";
import { sortedIssueEntries } from "./board-parser";
import { codeQueueBoardCommanderBriefHint, codeQueueBoardHintHasCommanderBriefUpdates, codeQueueBoardHintHasHwlabProductRouting, extractTimelineSections, normalizeNewlines } from "./body-profiles";
import { validationError } from "./client";
import { CODE_QUEUE_BOARD_TARGET_ISSUE, COMMANDER_BRIEF_TARGET_ISSUE, DAILY_COMMANDER_BRIEF_BODY_HEADING_PATTERN, DAILY_COMMANDER_BRIEF_TITLE_PATTERN, ISSUE_BODY_PROFILES, LEGACY_COMMANDER_BRIEF_BODY_HEADING_PATTERN, MIN_SAFE_ISSUE_BODY_CHARS } from "./types";
import type { BoardIgnoredIssue, BoardIgnoreReason, BoardIssueEntry, GitHubCommandResult, GitHubComment, GitHubIssue, GitHubOptions, IssueBodyProfileName, IssueBodyProfileOption, IssueProfileValidationContext } from "./types";

export function ignoredIssueList(issues: BoardIssueEntry[], ignoreMap: Map<number, BoardIgnoreReason>): BoardIgnoredIssue[] {
  return sortedIssueEntries(issues.filter((issue) => ignoreMap.has(issue.number))).map((issue) => ({
    number: issue.number,
    reason: ignoreMap.get(issue.number) ?? "ignored",
    title: issue.title,
    state: issue.state,
    url: issue.url,
  }));
}

export function ignoredBoardOnlyIssues(issueNumbers: number[], issueMap: Map<number, BoardIssueEntry>, ignoreMap: Map<number, BoardIgnoreReason>): BoardIgnoredIssue[] {
  return issueNumbers
    .filter((issueNumber) => ignoreMap.has(issueNumber) && !issueMap.has(issueNumber))
    .sort((a, b) => a - b)
    .map((issueNumber) => ({ number: issueNumber, reason: ignoreMap.get(issueNumber) ?? "ignored" }));
}

export function issueProfileFor(issueNumber: number, requested: IssueBodyProfileOption): IssueBodyProfileName | null {
  if (requested !== "auto") return requested;
  if (issueNumber === CODE_QUEUE_BOARD_TARGET_ISSUE) return "code-queue-board";
  if (issueNumber === COMMANDER_BRIEF_TARGET_ISSUE) return "commander-brief";
  return null;
}

export function firstMarkdownLine(body: string | null | undefined): string {
  return normalizeNewlines(body ?? "").split("\n").find((line) => line.trim().length > 0)?.trim() ?? "";
}

export function isDailyCommanderBriefTitle(title: string | null | undefined): boolean {
  return DAILY_COMMANDER_BRIEF_TITLE_PATTERN.test((title ?? "").trim());
}

export function isDailyCommanderBriefBodyHeading(line: string): boolean {
  return DAILY_COMMANDER_BRIEF_BODY_HEADING_PATTERN.test(line.trim());
}

export function isLegacyCommanderBriefBodyHeading(line: string): boolean {
  return LEGACY_COMMANDER_BRIEF_BODY_HEADING_PATTERN.test(line.trim());
}

export function currentBodyLooksLikeCommanderBrief(body: string | null | undefined): boolean {
  const normalized = normalizeNewlines(body ?? "");
  const firstLine = firstMarkdownLine(normalized);
  if (isDailyCommanderBriefBodyHeading(firstLine)) return true;
  return isLegacyCommanderBriefBodyHeading(firstLine)
    && normalized.includes("## 常驻观察与长期建议")
    && extractTimelineSections(normalized).length > 0;
}

export function commanderBriefIssueProfileMatch(issueNumber: number, context: IssueProfileValidationContext): { ok: boolean; reason: string } {
  if (issueNumber === COMMANDER_BRIEF_TARGET_ISSUE) return { ok: true, reason: "legacy-issue-number" };
  if (isDailyCommanderBriefTitle(context.issueTitle)) return { ok: true, reason: "daily-title" };
  if (currentBodyLooksLikeCommanderBrief(context.issueBody)) return { ok: true, reason: "current-body-heading" };
  return {
    ok: false,
    reason: context.issueMetadataFetched === true ? "not-commander-brief" : "issue-metadata-unavailable",
  };
}

export function issueProfileNeedsMetadata(issueNumber: number, requested: IssueBodyProfileOption): boolean {
  return requested === "commander-brief" && issueNumber !== COMMANDER_BRIEF_TARGET_ISSUE;
}

export function issueProfileValidation(issueNumber: number, body: string, requested: IssueBodyProfileOption, context: IssueProfileValidationContext = {}): Record<string, unknown> {
  const profileName = issueProfileFor(issueNumber, requested);
  if (profileName === null) return { profile: null, applied: false, requiredHeadings: [], missingHeadings: [], ok: true };
  const profile = ISSUE_BODY_PROFILES[profileName];
  const match = profileName === "commander-brief"
    ? commanderBriefIssueProfileMatch(issueNumber, context)
    : { ok: issueNumber === ISSUE_BODY_PROFILES["code-queue-board"].issueNumber, reason: issueNumber === ISSUE_BODY_PROFILES["code-queue-board"].issueNumber ? "issue-number" : "wrong-issue-number" };
  const missingHeadings = profile.requiredHeadings.filter((heading) => !body.includes(heading));
  return {
    profile: profileName,
    label: profile.label,
    applied: true,
    ...(profileName === "code-queue-board"
      ? { expectedIssueNumber: ISSUE_BODY_PROFILES["code-queue-board"].issueNumber }
      : { legacyIssueNumber: COMMANDER_BRIEF_TARGET_ISSUE }),
    issueMatchesProfile: match.ok,
    issueMatchReason: match.reason,
    issueMetadataFetched: context.issueMetadataFetched === true,
    issueTitle: context.issueTitle ?? null,
    requiredHeadings: profile.requiredHeadings,
    missingHeadings,
    ok: match.ok && missingHeadings.length === 0,
  };
}

export function validationBoolean(record: Record<string, unknown>, key: string): boolean {
  return record[key] === true;
}

export function validationStringArray(record: Record<string, unknown>, key: string): string[] {
  const value = record[key];
  return Array.isArray(value) ? value.map((item) => String(item)) : [];
}

export function validateIssueBodyGuard(repo: string, issueNumber: number, body: string, options: GitHubOptions, profileContext: IssueProfileValidationContext = {}, commandName = "issue edit", bodySource: Record<string, unknown> = { kind: "body-file", path: options.bodyFile ?? null }): GitHubCommandResult | null {
  const trimmed = body.trim();
  const isLiteralNull = trimmed.toLowerCase() === "null";
  const isBlank = trimmed.length === 0;
  const isShort = trimmed.length > 0 && trimmed.length < MIN_SAFE_ISSUE_BODY_CHARS;
  const profileValidation = issueProfileValidation(issueNumber, body, options.bodyProfile, profileContext);
  const boardBriefHint = codeQueueBoardCommanderBriefHint(issueNumber, body);
  const boardContainsCommanderBriefUpdates = codeQueueBoardHintHasCommanderBriefUpdates(boardBriefHint);
  const profileOk = validationBoolean(profileValidation, "ok");
  const failures: string[] = [];
  if (isLiteralNull) failures.push("literal-null-body");
  if (isBlank) failures.push("blank-body");
  if (isShort && !options.allowShortBody) failures.push("short-body");
  if (boardContainsCommanderBriefUpdates) failures.push("code-queue-board-contains-commander-brief-updates");
  if (!profileOk) {
    if (profileValidation.issueMatchesProfile === false) failures.push("profile-issue-mismatch");
    const missingHeadings = validationStringArray(profileValidation, "missingHeadings");
    if (missingHeadings.length > 0) failures.push("profile-heading-missing");
  }
  if (failures.length === 0) return null;
  const overrideAvailable = failures.every((failure) => failure === "short-body");
  const message = overrideAvailable
    ? `${commandName} body is shorter than the safe default; pass --allow-short-body only for intentional short writes`
    : `${commandName} body failed safety guard; refusing to write possible body-only issue corruption`;
  return validationError(commandName, repo, message, {
    issueNumber,
    guard: {
      ok: false,
      failures,
      minSafeBodyChars: MIN_SAFE_ISSUE_BODY_CHARS,
      allowShortBody: options.allowShortBody,
      shortBodyOverrideAvailable: overrideAvailable,
      bodySource,
      bodyPreview: preview(body),
      bodyPreviewLines: previewLines(body),
      codeQueueBoardHint: boardBriefHint,
      ...bodySafetySignals(body),
      isLiteralNull,
      isBlank,
      isShort,
      profile: profileValidation,
    },
  });
}

export function issueEditGuardSummary(issueNumber: number, body: string, options: GitHubOptions, profileContext: IssueProfileValidationContext = {}): Record<string, unknown> {
  const trimmed = body.trim();
  const profile = issueProfileValidation(issueNumber, body, options.bodyProfile, profileContext);
  const boardBriefHint = codeQueueBoardCommanderBriefHint(issueNumber, body);
  const warnings: string[] = [];
  if (trimmed.length > 0 && trimmed.length < MIN_SAFE_ISSUE_BODY_CHARS) {
    warnings.push(options.allowShortBody ? "short-body-allowed" : "short-body-would-fail");
  }
  if (options.allowShortBody) warnings.push("allow-short-body enabled; caller accepted short-body corruption risk");
  if (profile.ok === false) warnings.push("profile guard would fail");
  if (codeQueueBoardHintHasCommanderBriefUpdates(boardBriefHint)) warnings.push("code-queue-board-contains-commander-brief-updates");
  if (codeQueueBoardHintHasHwlabProductRouting(boardBriefHint)) warnings.push("code-queue-board-contains-hwlab-product-work");
  const signals = bodySafetySignals(body);
  const shellPollution = signals.shellPollution as Record<string, unknown>;
  if (shellPollution.suspected === true) warnings.push("shell-pollution-suspected");
  return {
    ok: trimmed.length > 0
      && trimmed.toLowerCase() !== "null"
      && (trimmed.length >= MIN_SAFE_ISSUE_BODY_CHARS || options.allowShortBody)
      && profile.ok === true
      && !codeQueueBoardHintHasCommanderBriefUpdates(boardBriefHint),
    minSafeBodyChars: MIN_SAFE_ISSUE_BODY_CHARS,
    allowShortBody: options.allowShortBody,
    warnings,
    profile,
    codeQueueBoardHint: boardBriefHint,
    ...signals,
  };
}

export function assertConcurrencyOptions(options: GitHubOptions, commandName: string): GitHubCommandResult | null {
  if (options.expectBodySha === undefined) return null;
  try {
    normalizeExpectedSha(options.expectBodySha);
    return null;
  } catch (error) {
    return validationError(commandName, options.repo, error instanceof Error ? error.message : String(error), {
      expectBodySha: options.expectBodySha,
    });
  }
}

export function validateIssueConcurrency(repo: string, issueNumber: number, oldIssue: GitHubIssue, options: GitHubOptions, commandName = "issue edit"): GitHubCommandResult | null {
  const checks: Record<string, unknown> = {
    issueNumber,
    expectUpdatedAt: options.expectUpdatedAt ?? null,
    actualUpdatedAt: oldIssue.updated_at ?? null,
    expectBodySha: options.expectBodySha ?? null,
    actualBodySha: bodySha(oldIssue.body ?? ""),
  };
  if (options.expectUpdatedAt !== undefined && oldIssue.updated_at !== options.expectUpdatedAt) {
    return validationError(commandName, repo, "--expect-updated-at did not match current GitHub issue updated_at; refusing stale body overwrite", checks);
  }
  if (options.expectBodySha !== undefined) {
    const expected = normalizeExpectedSha(options.expectBodySha);
    if (bodySha(oldIssue.body ?? "") !== expected) {
      return validationError(commandName, repo, "--expect-body-sha did not match current GitHub issue body; refusing stale body overwrite", checks);
    }
  }
  return null;
}

export function validateCommentConcurrency(repo: string, commentId: number, oldComment: GitHubComment, options: GitHubOptions, commandName: string): GitHubCommandResult | null {
  const checks: Record<string, unknown> = {
    commentId,
    expectUpdatedAt: options.expectUpdatedAt ?? null,
    actualUpdatedAt: oldComment.updated_at ?? null,
    expectBodySha: options.expectBodySha ?? null,
    actualBodySha: bodySha(oldComment.body ?? ""),
  };
  if (options.expectUpdatedAt !== undefined && oldComment.updated_at !== options.expectUpdatedAt) {
    return validationError(commandName, repo, "--expect-updated-at did not match current GitHub comment updated_at; refusing stale body overwrite", checks);
  }
  if (options.expectBodySha !== undefined) {
    const expected = normalizeExpectedSha(options.expectBodySha);
    if (bodySha(oldComment.body ?? "") !== expected) {
      return validationError(commandName, repo, "--expect-body-sha did not match current GitHub comment body; refusing stale body overwrite", checks);
    }
  }
  return null;
}
