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

import path from "node:path";
import { ApplyPatchV2Error, deriveUpdatedContent, parseApplyPatchV2 } from "../apply-patch-v2";
import { existsSync, readFileSync } from "node:fs";
import type { PatchHunk } from "../apply-patch-v2";
import { shellPollutionEvidence } from "./auth-and-safety";
import { isRecord } from "./notify-claudeqq";
import { MAX_INLINE_ISSUE_COMMENT_BODY_CHARS } from "./types";
import type { GitHubOptions } from "./types";

export function readMarkdownBodyFileOrStdin(path: string): { body: string; bodySource: Record<string, unknown> } {
  if (path === "-") {
    return { body: readFileSync(0, "utf8"), bodySource: { kind: "stdin", path: "-" } };
  }
  if (!existsSync(path)) throw new Error(`body file not found: ${path}`);
  return { body: readFileSync(path, "utf8"), bodySource: { kind: "body-file", path } };
}

export function readBodyPatchFileOrStdin(path: string): { patch: string; patchSource: Record<string, unknown> } {
  if (path === "-") {
    return { patch: readFileSync(0, "utf8"), patchSource: { kind: "body-patch-stdin", path: "-" } };
  }
  if (!existsSync(path)) throw new Error(`body patch file not found: ${path}`);
  return { patch: readFileSync(path, "utf8"), patchSource: { kind: "body-patch-file", path } };
}

export function readBodyPatch(options: GitHubOptions, command: string): { patch: string; patchSource: Record<string, unknown> } {
  const conflictingSources: string[] = [];
  if (options.body !== undefined) conflictingSources.push("--body");
  if (options.bodyFile !== undefined) conflictingSources.push(options.bodyFile === "-" ? "--body-stdin" : "--body-file");
  if (options.comment !== undefined) conflictingSources.push("--comment");
  if (options.commentFile !== undefined) conflictingSources.push(options.commentFile === "-" ? "--comment-stdin" : "--comment-file");
  if (conflictingSources.length > 0) {
    throw new Error(`${command} accepts only a patch source; remove ${conflictingSources.join(",")} and use --body-patch-stdin or --body-patch-file <file|->`);
  }
  if (options.bodyPatchFile === undefined) throw new Error(`${command} requires --body-patch-stdin or --body-patch-file <file|->`);
  return readBodyPatchFileOrStdin(options.bodyPatchFile);
}

export function patchPathList(hunks: PatchHunk[]): string[] {
  return Array.from(new Set(hunks.map((hunk) => hunk.path)));
}

export function applyVirtualBodyPatch(virtualPath: "issue.md" | "comment.md", oldBody: string, patchText: string): { newBody: string; patchSummary: Record<string, unknown> } {
  const parsed = parseApplyPatchV2(patchText);
  let newBody = oldBody;
  let updateChunkCount = 0;
  for (let index = 0; index < parsed.hunks.length; index += 1) {
    const hunk = parsed.hunks[index] as PatchHunk;
    if (hunk.kind !== "update") {
      throw new ApplyPatchV2Error("GitHub body patch supports only Update File hunks", {
        hunk: index + 1,
        action: hunk.kind,
        path: hunk.path,
        expectedPath: virtualPath,
      });
    }
    if (hunk.path !== virtualPath) {
      throw new ApplyPatchV2Error("GitHub body patch target path mismatch", {
        hunk: index + 1,
        path: hunk.path,
        expectedPath: virtualPath,
      });
    }
    if (hunk.movePath !== null && hunk.movePath !== hunk.path) {
      throw new ApplyPatchV2Error("GitHub body patch does not support Move to", {
        hunk: index + 1,
        path: hunk.path,
        targetPath: hunk.movePath,
        expectedPath: virtualPath,
      });
    }
    updateChunkCount += hunk.chunks.length;
    newBody = deriveUpdatedContent(virtualPath, newBody, hunk.chunks).newContent;
  }
  if (newBody === oldBody) {
    throw new ApplyPatchV2Error("GitHub body patch did not change the body; no GitHub write will be performed", {
      path: virtualPath,
      hunkCount: parsed.hunks.length,
    });
  }
  return {
    newBody,
    patchSummary: {
      engine: "apply-patch-v2",
      virtualFile: virtualPath,
      patchBytes: Buffer.byteLength(patchText, "utf8"),
      hunkCount: parsed.hunks.length,
      updateChunkCount,
      paths: patchPathList(parsed.hunks),
      hints: parsed.hints,
    },
  };
}

export function sanitizedPatchDiagnostics(value: unknown): Record<string, unknown> | null {
  if (!isRecord(value)) return null;
  const diagnostics: Record<string, unknown> = {};
  const candidateLines = value.firstExpectedLineCandidates;
  if (Array.isArray(candidateLines)) diagnostics.firstExpectedLineCandidates = candidateLines.filter((item): item is number => typeof item === "number");
  for (const key of ["firstExpectedLineCandidatesTruncated", "bestPrefixMatchedLines", "bestPrefixStartLine", "likelyMissingAddedPrefixes", "likelyStaleOrOversizedContext"]) {
    const item = value[key];
    if (typeof item === "string" || typeof item === "number" || typeof item === "boolean" || item === null) diagnostics[key] = item;
  }
  return Object.keys(diagnostics).length > 0 ? diagnostics : null;
}

export function sanitizedPatchDetails(value: unknown): Record<string, unknown> | null {
  if (!isRecord(value)) return null;
  const result: Record<string, unknown> = {};
  for (const key of ["path", "targetPath", "expectedPath", "hunk", "chunk", "action", "status", "line", "hunkCount"]) {
    const item = value[key];
    if (typeof item === "string" || typeof item === "number" || typeof item === "boolean" || item === null) result[key] = item;
  }
  if (typeof value.expected === "string") {
    result.expectedLineCount = value.expected.length === 0 ? 0 : value.expected.split(/\r?\n/u).length;
    result.expectedChars = value.expected.length;
  }
  const diagnostics = sanitizedPatchDiagnostics(value.diagnostics);
  if (diagnostics !== null) result.diagnostics = diagnostics;
  if (Array.isArray(value.partialChanges)) result.partialChanges = value.partialChanges.filter((item): item is string => typeof item === "string");
  if (Array.isArray(value.outcomes)) {
    result.outcomes = value.outcomes
      .filter(isRecord)
      .map((outcome) => {
        const summary: Record<string, unknown> = {};
        for (const key of ["hunk", "action", "status", "path", "targetPath", "change"]) {
          const item = outcome[key];
          if (typeof item === "string" || typeof item === "number" || typeof item === "boolean" || item === null) summary[key] = item;
        }
        if (isRecord(outcome.error) && typeof outcome.error.message === "string") summary.errorMessage = outcome.error.message;
        return summary;
      });
  }
  if (isRecord(value.failed)) result.failed = sanitizedPatchDetails(value.failed);
  if (isRecord(value.cause)) result.cause = sanitizedPatchDetails(value.cause);
  return Object.keys(result).length > 0 ? result : null;
}

export function bodyPatchErrorSummary(error: unknown): Record<string, unknown> {
  const message = error instanceof Error ? error.message : String(error);
  const summary: Record<string, unknown> = {
    name: error instanceof Error ? error.name : "Error",
    message,
    bodyTextRedacted: true,
    note: "Patch diagnostics omit expected/context text so GitHub body contents and credentials are not echoed.",
  };
  const details = error instanceof ApplyPatchV2Error ? sanitizedPatchDetails(error.details) : null;
  if (details !== null) summary.details = details;
  return summary;
}

export function readOptionalMarkdownBody(options: GitHubOptions, command: string): { body: string; bodySource: Record<string, unknown> } | null {
  if (options.bodyFile !== undefined && options.body !== undefined) throw new Error(`${command} accepts only one body source: --body-file/--body-stdin or --body`);
  if (options.bodyFile !== undefined) {
    return readMarkdownBodyFileOrStdin(options.bodyFile);
  }
  if (options.body !== undefined) {
    return {
      body: options.body,
      bodySource: {
        kind: "inline",
        warning: options.body.includes("\n") ? "inline body contains real newlines; --body-stdin with a quoted heredoc is safer for generated Markdown" : "inline body is intended only for short single-line text",
      },
    };
  }
  return null;
}

export function readMarkdownBody(options: GitHubOptions, command: string): { body: string; bodySource: Record<string, unknown> } {
  const body = readOptionalMarkdownBody(options, command);
  if (body !== null) return body;
  throw new Error(`${command} requires --body-stdin, --body-file <file|->, or --body <text>`);
}

export function secretLikeInlineFindings(body: string): string[] {
  const findings: string[] = [];
  if (/\bgh[pousr]_[A-Za-z0-9_]{6,}\b/u.test(body)) findings.push("github-token-like-value");
  if (/\bgithub_pat_[A-Za-z0-9_]{6,}\b/u.test(body)) findings.push("github-pat-like-value");
  if (/\b(?:token|password|passwd|secret|api[_-]?key)\s*[:=]\s*["']?[^"'\s,;]{6,}/iu.test(body)) findings.push("credential-assignment-like-value");
  if (/\bBearer\s+[A-Za-z0-9._~+/=-]{12,}\b/iu.test(body)) findings.push("bearer-token-like-value");
  return findings;
}

export function readIssueCommentBody(options: GitHubOptions, command = "issue comment create"): { body: string; bodySource: Record<string, unknown> } {
  if (options.bodyFile !== undefined && options.body !== undefined) {
    throw new Error(`${command} accepts only one body source: --body-file/--body-stdin or --body`);
  }
  if (options.bodyFile !== undefined) {
    return readMarkdownBodyFileOrStdin(options.bodyFile);
  }
  if (options.body === undefined) throw new Error(`${command} requires --body-stdin, --body-file <file|->, or --body <text>`);
  const body = options.body;
  const trimmed = body.trim();
  const shellPollution = shellPollutionEvidence(body);
  const secretLike = secretLikeInlineFindings(body);
  if (trimmed.length === 0) {
    throw new Error(`${command} --body must not be blank; use --body-stdin with a quoted heredoc for reviewed Markdown`);
  }
  if (body.length > MAX_INLINE_ISSUE_COMMENT_BODY_CHARS) {
    throw new Error(`${command} --body is limited to ${MAX_INLINE_ISSUE_COMMENT_BODY_CHARS} characters; use --body-stdin with a quoted heredoc for long Markdown`);
  }
  if (body.includes("\n") || body.includes("\r")) {
    throw new Error(`${command} --body supports short single-line text only; use --body-stdin with a quoted heredoc for multiline Markdown`);
  }
  if (shellPollution.length > 0) {
    throw new Error(`${command} --body contains shell-pollution signals (${shellPollution.join(",")}); use --body-stdin with a quoted heredoc for reviewed Markdown bytes`);
  }
  if (secretLike.length > 0) {
    throw new Error(`${command} --body appears to contain secret-like text (${secretLike.join(",")}); refusing to print or submit it`);
  }
  return {
    body,
    bodySource: {
      kind: "inline",
      maxInlineBodyChars: MAX_INLINE_ISSUE_COMMENT_BODY_CHARS,
      warning: "inline issue comments are intended only for short single-line text; use --body-stdin with a quoted heredoc for Markdown or generated content",
    },
  };
}
