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

import path from "node:path";
import { bodySha, preview, previewLines, repoParts } from "./auth-and-safety";
import { assertConcurrencyOptions, issueEditGuardSummary, issueProfileNeedsMetadata, validateIssueBodyGuard, validateIssueConcurrency } from "./body-guards";
import { applyVirtualBodyPatch, bodyPatchErrorSummary, readBodyPatch, readMarkdownBody, readMarkdownBodyFileOrStdin } from "./body-input";
import { commanderBriefDiff, dryRunBody } from "./body-profiles";
import { commandError, githubRequest, isGitHubError, issueBodyReadCommands, issueWriteDisclosure, runnerDisposition, validationError, writeBodyPlan } from "./client";
import { getIssue } from "./issue-read";
import { issueLabelNames, issueSummary } from "./issue-summary";
import { commanderBriefClaudeQqConfig, commanderBriefNotificationPlan, isRecord, maskedTarget, sendCommanderBriefClaudeQq } from "./notify-claudeqq";
import { ghShort, ghTable, ghText } from "./render";
import { bodyUpdatePlan } from "./repo-compare";
import { COMMANDER_BRIEF_TARGET_ISSUE } from "./types";
import type { ClaudeQqSendResult, CommanderBriefDiff, GitHubCommandResult, GitHubIssue, GitHubOptions, IssueProfileValidationContext } from "./types";

export async function issueCreate(repo: string, token: string, options: GitHubOptions): Promise<GitHubCommandResult> {
  if (options.title === undefined) return withIssueCreateRendered(validationError("issue create", repo, "issue create requires --title <title>"));
  if (options.body !== undefined) {
    return withIssueCreateRendered(validationError("issue create", repo, "issue create does not support --body; use --body-stdin with a quoted heredoc for Markdown", {
      title: options.title,
      bodySource: "inline",
    }));
  }
  const { body, bodySource } = readMarkdownBody({
    ...options,
    body: undefined,
  }, "issue create");
  const labels = options.labels;
  if (options.dryRun) {
    return withIssueCreateRendered({
      ok: true,
      command: "issue create",
      repo,
      dryRun: true,
      planned: true,
      title: options.title,
      labels,
      ...writeBodyPlan("issue create", repo, body, bodySource, { title: options.title, labels }),
    });
  }
  const { owner, name } = repoParts(repo);
  const payload: Record<string, unknown> = { title: options.title, body };
  if (labels.length > 0) payload.labels = labels;
  const issue = await githubRequest<GitHubIssue>(token, "POST", `/repos/${owner}/${name}/issues`, payload);
  if (isGitHubError(issue)) return withIssueCreateRendered(commandError("issue create", repo, issue, { title: options.title, labels }));
  const appliedLabels = issueLabelNames(issue);
  const missingLabels = labels.filter((label) => !appliedLabels.includes(label));
  if (missingLabels.length > 0) {
    return withIssueCreateRendered(validationError("issue create", repo, "GitHub created the issue but did not return all requested labels; refusing to report silent label success", {
      issue: issueSummary(issue),
      requestedLabels: labels,
      appliedLabels,
      missingLabels,
    }));
  }
  return withIssueCreateRendered({ ok: true, command: "issue create", repo, issue: issueSummary(issue), labels, bodySource, rest: true });
}

export function withIssueCreateRendered(result: GitHubCommandResult): GitHubCommandResult {
  return {
    ...result,
    contentType: "text/plain",
    renderedText: renderIssueCreateTable(result),
  };
}

export function renderIssueCreateTable(result: GitHubCommandResult): string {
  const issue = isRecord(result.issue) ? result.issue : {};
  const number = ghText(issue.number ?? result.number);
  const title = ghText(issue.title ?? result.title);
  const ok = result.ok !== false;
  const details = isRecord(result.details) ? result.details : {};
  const status = ok ? (result.dryRun === true ? "dry-run" : "created") : "failed";
  const reason = ok ? "-" : ghText(result.degradedReason ?? details.degradedReason ?? "validation-failed");
  const message = ghText(result.message ?? details.message);
  const labels = Array.isArray(result.labels) ? result.labels.map(String).join(",") : ghText(result.labels);
  const lines = [
    `gh issue create (${status})`,
    "",
    ghTable(["ISSUE", "STATUS", "REASON", "LABELS", "TITLE"], [[
      number === "-" ? "-" : `#${number}`,
      status,
      ghShort(reason, 32),
      ghShort(labels, 40),
      ghShort(title, 96),
    ]]),
    "",
    "Summary:",
    `  repo=${ghText(result.repo)} url=${ghText(issue.url ?? result.url)}`,
    "",
    "Next:",
  ];
  if (!ok) {
    const nextIndex = lines.indexOf("Next:");
    if (nextIndex >= 0) lines.splice(nextIndex, 0, `  message=${ghShort(message, 160)}`, "");
    lines.push("  use --body-stdin with a quoted heredoc for Markdown issue bodies");
  } else if (status === "created" && number !== "-") {
    lines.push(`  trans gh:/${ghText(result.repo)}/issue/${number} cat`);
    lines.push(`  trans gh:/${ghText(result.repo)}/issue/${number} rg <pattern>`);
    lines.push(`  bun scripts/cli.ts gh issue view ${number} --repo ${ghText(result.repo)}`);
  } else {
    lines.push("  rerun without --dry-run to create the issue");
  }
  lines.push("", "Disclosure:");
  lines.push("  default view is bounded; use trans gh: cat/rg for human body verification and --json body/--full/--raw only for explicit structured disclosure.");
  return lines.join("\n");
}

export async function issuePatch(repo: string, token: string, issueNumber: number, options: GitHubOptions): Promise<GitHubCommandResult> {
  const commandName = "issue patch";
  const concurrencyOptionError = assertConcurrencyOptions(options, commandName);
  if (concurrencyOptionError !== null) return concurrencyOptionError;
  let patchInput: { patch: string; patchSource: Record<string, unknown> };
  try {
    patchInput = readBodyPatch(options, commandName);
  } catch (error) {
    return validationError(commandName, repo, error instanceof Error ? error.message : String(error), { issueNumber });
  }
  const { patch, patchSource } = patchInput;
  const oldIssue = await getIssue(token, repo, issueNumber);
  if (isGitHubError(oldIssue)) return commandError(commandName, repo, oldIssue, { issueNumber, phase: "read-before-patch" });
  const concurrencyError = validateIssueConcurrency(repo, issueNumber, oldIssue, options, commandName);
  if (concurrencyError !== null) return concurrencyError;
  const oldBody = oldIssue.body ?? "";
  let patched: { newBody: string; patchSummary: Record<string, unknown> };
  try {
    patched = applyVirtualBodyPatch("issue.md", oldBody, patch);
  } catch (error) {
    return validationError(commandName, repo, "body patch failed; no GitHub write performed", {
      issueNumber,
      patchSource,
      patchError: bodyPatchErrorSummary(error),
      oldBody: {
        bodyChars: oldBody.length,
        bodySha: bodySha(oldBody),
        updatedAt: oldIssue.updated_at ?? null,
      },
      noWrite: true,
    });
  }
  const profileContext: IssueProfileValidationContext = { issueTitle: oldIssue.title, issueBody: oldIssue.body, issueMetadataFetched: true };
  const guard = validateIssueBodyGuard(repo, issueNumber, patched.newBody, options, profileContext, commandName, patchSource);
  if (guard !== null) return guard;
  const guardSummary = issueEditGuardSummary(issueNumber, patched.newBody, options, profileContext);
  const bodyChange = {
    oldBodyChars: oldBody.length,
    newBodyChars: patched.newBody.length,
    deltaChars: patched.newBody.length - oldBody.length,
    oldBodySha: bodySha(oldBody),
    newBodySha: bodySha(patched.newBody),
    oldUpdatedAt: oldIssue.updated_at ?? null,
  };
  const concurrency = {
    checked: true,
    oldIssueUpdatedAt: oldIssue.updated_at ?? null,
    oldBodySha: bodySha(oldBody),
    expectUpdatedAt: options.expectUpdatedAt ?? null,
    expectBodySha: options.expectBodySha ?? null,
  };
  if (options.dryRun) {
    return {
      ok: true,
      command: commandName,
      repo,
      dryRun: true,
      planned: true,
      issueNumber,
      patchSource,
      patch: patched.patchSummary,
      ...dryRunBody(repo, undefined, patched.newBody),
      disclosure: issueWriteDisclosure(options, repo, issueNumber, true),
      readCommands: issueBodyReadCommands(repo, issueNumber),
      guard: guardSummary,
      concurrency: { ...concurrency, dryRunNoWrite: true },
      bodyChange,
      wouldPatch: {
        issueNumber,
        bodyChars: patched.newBody.length,
        bodySha: bodySha(patched.newBody),
        method: "PATCH",
        path: `/repos/{owner}/{repo}/issues/${issueNumber}`,
      },
    };
  }
  const { owner, name } = repoParts(repo);
  const issue = await githubRequest<GitHubIssue>(token, "PATCH", `/repos/${owner}/${name}/issues/${issueNumber}`, { body: patched.newBody });
  if (isGitHubError(issue)) return commandError(commandName, repo, issue, { issueNumber });
  return {
    ok: true,
    command: commandName,
    repo,
    issue: issueSummary(issue, { includeBody: options.raw || options.full }),
    patchSource,
    patch: patched.patchSummary,
    bodyChange: {
      ...bodyChange,
      newUpdatedAt: issue.updated_at ?? null,
    },
    guard: guardSummary,
    concurrency,
    disclosure: issueWriteDisclosure(options, repo, issueNumber, false),
    readCommands: issueBodyReadCommands(repo, issueNumber),
    request: {
      method: "PATCH",
      path: `/repos/${owner}/${name}/issues/${issueNumber}`,
      bodyChars: patched.newBody.length,
    },
    rest: true,
  };
}

export async function issueEdit(repo: string, token: string, issueNumber: number, options: GitHubOptions, commandName = "issue edit"): Promise<GitHubCommandResult> {
  if (options.bodyFile === undefined) {
    return validationError(commandName, repo, `${commandName} requires --body-stdin or --body-file <file|->`, { issueNumber });
  }
  let bodyInput: { body: string; bodySource: Record<string, unknown> };
  try {
    bodyInput = readMarkdownBodyFileOrStdin(options.bodyFile);
  } catch (error) {
    return validationError(commandName, repo, error instanceof Error ? error.message : String(error), { issueNumber });
  }
  const { body, bodySource } = bodyInput;
  const needsProfileMetadata = issueProfileNeedsMetadata(issueNumber, options.bodyProfile);
  if (options.mode === "replace" && !needsProfileMetadata) {
    const bodyGuard = validateIssueBodyGuard(repo, issueNumber, body, options);
    if (bodyGuard !== null) return bodyGuard;
  }
  const concurrencyOptionError = assertConcurrencyOptions(options, commandName);
  if (concurrencyOptionError !== null) return concurrencyOptionError;
  let oldIssue: GitHubIssue | null = null;
  let briefDiff: CommanderBriefDiff | null = null;
  let profileContext: IssueProfileValidationContext = {};
  const claudeQqConfig = commanderBriefClaudeQqConfig();
  if (options.notifyClaudeQqBriefDiff && issueNumber !== COMMANDER_BRIEF_TARGET_ISSUE) {
    return validationError(commandName, repo, "--notify-claudeqq-brief-diff is only supported for commander brief issue #24", { issueNumber });
  }
  const needsReadBeforeEdit = options.mode === "append"
    || needsProfileMetadata && token.length > 0
    || !options.dryRun;
  if (needsReadBeforeEdit) {
    const issue = await getIssue(token, repo, issueNumber);
    if (isGitHubError(issue)) return commandError(commandName, repo, issue, { issueNumber, phase: "read-before-update" });
    oldIssue = issue;
    profileContext = { issueTitle: issue.title, issueBody: issue.body, issueMetadataFetched: true };
    const concurrencyError = validateIssueConcurrency(repo, issueNumber, issue, options);
    if (concurrencyError !== null) return concurrencyError;
    if (options.notifyClaudeQqBriefDiff) briefDiff = commanderBriefDiff(issue.body ?? "", options.mode === "append" ? `${issue.body ?? ""}${body}` : body);
  }
  const finalBody = options.mode === "append" ? `${oldIssue?.body ?? ""}${body}` : body;
  if (options.mode === "append" || needsProfileMetadata) {
    const bodyGuard = validateIssueBodyGuard(repo, issueNumber, finalBody, options, profileContext);
    if (bodyGuard !== null) return bodyGuard;
  }
  if (options.dryRun) {
    const dryRunDiff = options.notifyClaudeQqBriefDiff ? commanderBriefDiff("", finalBody) : null;
    const guard = issueEditGuardSummary(issueNumber, finalBody, options, profileContext);
    let dryRunOldBody: Record<string, unknown> = {
      fetched: false,
      bodyChars: null,
      bodySha: null,
      updatedAt: null,
      reason: token.length > 0 ? "not-requested" : "no token supplied to dry-run",
    };
    if (oldIssue !== null) {
      const oldBody = oldIssue.body ?? "";
      dryRunOldBody = {
        fetched: true,
        bodyChars: oldBody.length,
        bodySha: bodySha(oldBody),
        updatedAt: oldIssue.updated_at ?? null,
      };
    } else if (token.length > 0) {
      const issue = await getIssue(token, repo, issueNumber);
      if (isGitHubError(issue)) {
        dryRunOldBody = {
          fetched: false,
          bodyChars: null,
          bodySha: null,
          updatedAt: null,
          readError: {
            degradedReason: issue.degradedReason,
            runnerDisposition: issue.runnerDisposition,
            message: issue.message,
            status: issue.status ?? null,
          },
        };
      } else {
        const oldBody = issue.body ?? "";
        dryRunOldBody = {
          fetched: true,
          bodyChars: oldBody.length,
          bodySha: bodySha(oldBody),
          updatedAt: issue.updated_at ?? null,
        };
      }
    }
    return {
      ok: true,
      command: commandName,
      repo,
      dryRun: true,
      issueNumber,
      mode: options.mode,
      ...dryRunBody(repo, options.title, finalBody),
      disclosure: issueWriteDisclosure(options, repo, issueNumber, true),
      readCommands: issueBodyReadCommands(repo, issueNumber),
      guard,
      update: bodyUpdatePlan(commandName, repo, issueNumber, options.mode, body, bodySource, oldIssue?.body ?? null),
      bodyOnlySafety: {
        oldBody: dryRunOldBody,
        newBody: {
          bodyChars: finalBody.length,
          bodySha: bodySha(finalBody),
          bodyPreview: preview(finalBody),
          bodyPreviewLines: previewLines(finalBody),
        },
      },
      concurrency: {
        dryRunNoWrite: true,
        expectUpdatedAt: options.expectUpdatedAt ?? null,
        expectBodySha: options.expectBodySha ?? null,
        note: "non-dry-run checks --expect-updated-at and --expect-body-sha against the current GitHub issue before PATCH",
      },
      wouldPatch: {
        issueNumber,
        title: options.title ?? null,
        bodySource,
        mode: options.mode,
        bodyChars: finalBody.length,
        bodySha: bodySha(finalBody),
      },
      ...(options.notifyClaudeQqBriefDiff
        ? {
          commanderBriefNotification: commanderBriefNotificationPlan(issueNumber, finalBody, dryRunDiff ?? commanderBriefDiff("", finalBody), claudeQqConfig),
          dryRunOldBodySource: "not-fetched",
          dryRunDiffReliability: "new-body-only preview; non-dry-run reads the GitHub issue body before PATCH and sends only sections absent from the old body",
          dryRunNoWrite: true,
          dryRunNoClaudeQqSend: true,
        }
        : {}),
    };
  }
  const { owner, name } = repoParts(repo);
  const payload: Record<string, unknown> = { body: finalBody };
  if (options.title !== undefined) payload.title = options.title;
  const issue = await githubRequest<GitHubIssue>(token, "PATCH", `/repos/${owner}/${name}/issues/${issueNumber}`, payload);
  if (isGitHubError(issue)) return commandError(commandName, repo, issue, { issueNumber });
  const guard = issueEditGuardSummary(issueNumber, finalBody, options, profileContext);
  const concurrency = oldIssue === null
    ? { checked: false, expectUpdatedAt: options.expectUpdatedAt ?? null, expectBodySha: options.expectBodySha ?? null }
    : { checked: true, oldIssueUpdatedAt: oldIssue.updated_at ?? null, oldBodySha: bodySha(oldIssue.body ?? ""), expectUpdatedAt: options.expectUpdatedAt ?? null, expectBodySha: options.expectBodySha ?? null };
  if (!options.notifyClaudeQqBriefDiff) {
    return {
      ok: true,
      command: commandName,
      repo,
      issue: issueSummary(issue, { includeBody: options.raw || options.full }),
      mode: options.mode,
      bodySource,
      guard,
      concurrency,
      disclosure: issueWriteDisclosure(options, repo, issueNumber, false),
      readCommands: issueBodyReadCommands(repo, issueNumber),
      rest: true,
    };
  }

  const diff = briefDiff ?? commanderBriefDiff(oldIssue?.body ?? "", finalBody);
  const claudeqq = diff.ok
    ? await sendCommanderBriefClaudeQq(claudeQqConfig, diff.message)
    : {
      ok: true,
      attempted: false,
      skipped: true,
      skippedReason: diff.skippedReason ?? "no commander brief diff to send",
      target: maskedTarget(claudeQqConfig),
    } satisfies ClaudeQqSendResult;
  return {
    ok: true,
    command: commandName,
    repo,
    issue: issueSummary(issue, { includeBody: options.raw || options.full }),
    guard,
    concurrency,
    disclosure: issueWriteDisclosure(options, repo, issueNumber, false),
    readCommands: issueBodyReadCommands(repo, issueNumber),
    rest: true,
    commanderBriefNotification: {
      issueNumber,
      oldIssueUpdatedAt: oldIssue?.updated_at ?? null,
      diff: {
        ok: diff.ok,
        mode: diff.mode,
        chars: diff.chars,
        sectionCount: diff.sectionCount,
        skippedReason: diff.skippedReason ?? null,
        preview: diff.ok ? preview(diff.message) : "",
        previewLines: diff.ok ? previewLines(diff.message) : [],
        containsLiteralBackslashN: diff.message.includes("\\n"),
        containsBackticks: diff.message.includes("`"),
        containsMarkdownTable: /^\s*\|.+\|\s*$/m.test(diff.message),
      },
    },
    claudeqq,
  };
}
