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

import { repoParts, resolveToken } from "./auth-and-safety";
import { authStatus } from "./auth-pr-read";
import { authRequired, commandError, errorPayload, githubRequest, isGitHubError, runnerDisposition, validationError } from "./client";
import { isRecord } from "./notify-claudeqq";
import { compactAuthCapability, deleteHeadBranchAfterMerge, loadPrMergeUnknownRetryConfig, mergeabilityHasUnknownPending, nextPrMergeRetryDelayMs, prCloseoutMetadata, prCloseoutSummary, preflightPullRequestSummary, prGraphqlMetadata, prMergeRetryCommand, prMetadataSummary, prPreflightPolicy, prSummary, sleepMs, statusRollupSummary } from "./pr-summary";
import { ghShort, ghTable, ghText } from "./render";
import { UNIDESK_CLI_CONFIG_PATH } from "./types";
import type { GitHubCommandResult, GitHubErrorPayload, GitHubOptions, GitHubPullRequest, GitHubPullRequestGraphqlMetadata, PrMergeUnknownRetryConfig } from "./types";

export async function prMerge(repo: string, token: string, number: number, options: GitHubOptions): Promise<GitHubCommandResult> {
  const { owner, name } = repoParts(repo);
  let retryConfig: PrMergeUnknownRetryConfig;
  try {
    retryConfig = loadPrMergeUnknownRetryConfig();
  } catch (error) {
    return validationError("pr merge", repo, error instanceof Error ? error.message : String(error), { number, configPath: UNIDESK_CLI_CONFIG_PATH });
  }
  const pr = await githubRequest<GitHubPullRequest>(token, "GET", `/repos/${owner}/${name}/pulls/${number}`);
  if (isGitHubError(pr)) return commandError("pr merge", repo, pr, { number, phase: "fetch-pr" });
  const summary = prSummary(pr);
  if (summary.merged === true) {
    return withPrMergeRendered({
      ok: true,
      command: "pr merge",
      repo,
      number,
      method: options.mergeMethod,
      alreadyMerged: true,
      pullRequest: summary,
      branchDeletion: { attempted: false, skippedReason: "already-merged" },
      rest: true,
    });
  }
  let metadata: GitHubPullRequestGraphqlMetadata | GitHubErrorPayload | null = null;
  let statusChecks: Record<string, unknown> = {};
  let mergeability: Record<string, unknown> = {};
  let delayMs = retryConfig.initialDelayMs;
  const retryAttempts: Array<Record<string, unknown>> = [];
  for (let attempt = 1; attempt <= retryConfig.maxAttempts; attempt += 1) {
    metadata = await prGraphqlMetadata(repo, token, number);
    if (isGitHubError(metadata)) return commandError("pr merge", repo, metadata, { number, phase: "fetch-pr-closeout-metadata", pullRequest: summary, retry: { attempt, maxAttempts: retryConfig.maxAttempts } });
    statusChecks = statusRollupSummary(repo, number, metadata.statusCheckRollup, false);
    mergeability = prCloseoutSummary(summary, prMetadataSummary(metadata), statusChecks);
    const shouldRetryUnknown = mergeability.readyForCommanderMerge !== true && mergeabilityHasUnknownPending(mergeability);
    const waitMs = shouldRetryUnknown && attempt < retryConfig.maxAttempts ? delayMs : 0;
    retryAttempts.push({
      attempt,
      maxAttempts: retryConfig.maxAttempts,
      mergeable: metadata.mergeable ?? null,
      mergeStateStatus: metadata.mergeStateStatus ?? null,
      statusCheckRollup: metadata.statusCheckRollup?.state ?? null,
      conclusion: mergeability.conclusion ?? null,
      blockers: Array.isArray(mergeability.blockers) ? mergeability.blockers : [],
      pending: Array.isArray(mergeability.pending) ? mergeability.pending : [],
      waitMs,
    });
    if (mergeability.readyForCommanderMerge === true || !shouldRetryUnknown || attempt >= retryConfig.maxAttempts) break;
    await sleepMs(delayMs);
    delayMs = nextPrMergeRetryDelayMs(retryConfig, delayMs);
  }
  if (metadata === null || isGitHubError(metadata)) return commandError("pr merge", repo, metadata ?? errorPayload("invalid-response", "PR merge metadata was not collected"), { number, phase: "fetch-pr-closeout-metadata", pullRequest: summary });
  const retry = {
    reason: "github-mergeability-unknown",
    maxAttempts: retryConfig.maxAttempts,
    attempts: retryAttempts.length,
    exhausted: mergeability.readyForCommanderMerge !== true && mergeabilityHasUnknownPending(mergeability) && retryAttempts.length >= retryConfig.maxAttempts,
    config: retryConfig,
    attemptsLog: retryAttempts,
  };
  if (mergeability.readyForCommanderMerge !== true) {
    const details = {
      number,
      method: options.mergeMethod,
      deleteBranch: options.deleteBranch,
      pullRequest: preflightPullRequestSummary(summary),
      mergeability,
      statusChecks,
      closeoutMetadata: prCloseoutMetadata(metadata),
      retry,
    };
    return withPrMergeRendered(validationError("pr merge", repo, retry.exhausted ? "PR mergeability is still UNKNOWN after automatic retry; no merge performed" : "PR is not ready for merge; blockers or pending states remain", details), details);
  }
  if (options.dryRun) {
    return withPrMergeRendered({
      ok: true,
      command: "pr merge",
      repo,
      number,
      dryRun: true,
      wouldMerge: true,
      method: options.mergeMethod,
      deleteBranch: options.deleteBranch,
      pullRequest: preflightPullRequestSummary(summary),
      mergeability,
      statusChecks,
      retry,
    });
  }
  const merged = await githubRequest<Record<string, unknown>>(token, "PUT", `/repos/${owner}/${name}/pulls/${number}/merge`, {
    merge_method: options.mergeMethod,
  });
  if (isGitHubError(merged)) return commandError("pr merge", repo, merged, { number, phase: "merge", method: options.mergeMethod, pullRequest: summary });
  const after = await githubRequest<GitHubPullRequest>(token, "GET", `/repos/${owner}/${name}/pulls/${number}`);
  if (isGitHubError(after)) return commandError("pr merge", repo, after, { number, phase: "fetch-after-merge", mergeResult: merged });
  const branchDeletion = options.deleteBranch ? await deleteHeadBranchAfterMerge(repo, token, after) : { attempted: false, skippedReason: "delete-branch-not-requested" };
  return withPrMergeRendered({
    ok: true,
    command: "pr merge",
    repo,
    number,
    method: options.mergeMethod,
    mergeResult: merged,
    pullRequest: prSummary(after),
    mergeability,
    statusChecks,
    retry,
    branchDeletion,
    rest: true,
  });
}

export function withPrMergeRendered(result: GitHubCommandResult, fallbackDetails?: Record<string, unknown>): GitHubCommandResult {
  return {
    ...result,
    contentType: "text/plain",
    renderedText: renderPrMergeTable(result, fallbackDetails),
  };
}

export function renderPrMergeTable(result: GitHubCommandResult, fallbackDetails?: Record<string, unknown>): string {
  const details = fallbackDetails ?? {};
  const pullRequest = isRecord(result.pullRequest)
    ? result.pullRequest
    : isRecord(details.pullRequest)
      ? details.pullRequest
      : {};
  const mergeability = isRecord(result.mergeability)
    ? result.mergeability
    : isRecord(details.mergeability)
      ? details.mergeability
      : {};
  const statusChecks = isRecord(result.statusChecks)
    ? result.statusChecks
    : isRecord(details.statusChecks)
      ? details.statusChecks
      : {};
  const counts = isRecord(statusChecks.counts) ? statusChecks.counts : {};
  const branchDeletion = isRecord(result.branchDeletion) ? result.branchDeletion : {};
  const retry = isRecord(result.retry)
    ? result.retry
    : isRecord(details.retry)
      ? details.retry
      : {};
  const mergeMethod = result.method ?? result.mergeMethod ?? details.method;
  const deleteBranch = result.deleteBranch ?? details.deleteBranch;
  const status = result.ok === true
    ? result.alreadyMerged === true
      ? "already-merged"
      : result.dryRun === true
        ? "dry-run"
        : "merged"
    : ghText(mergeability.conclusion) === "pending"
      ? "pending"
      : "blocked";
  const blockers = Array.isArray(mergeability.blockers) ? mergeability.blockers.map(String) : [];
  const pending = Array.isArray(mergeability.pending) ? mergeability.pending.map(String) : [];
  const rows = [[
    `#${ghText(result.number ?? details.number)}`,
    status,
    ghText(mergeMethod),
    ghText(mergeability.mergeable),
    ghText(mergeability.mergeStateStatus),
    retry.attempts !== undefined && retry.maxAttempts !== undefined ? `${ghText(retry.attempts)}/${ghText(retry.maxAttempts)}${retry.exhausted === true ? " exhausted" : ""}` : "-",
    `${ghText(statusChecks.totalContexts)} total ${ghText(counts.success)} ok ${ghText(counts.failure)} fail ${ghText(counts.pending)} pending`,
  ]];
  const lines = [
    `gh pr merge (${status})`,
    "",
    ghTable(["PR", "STATUS", "METHOD", "MERGEABLE", "MERGE_STATE", "RETRY", "CHECKS"], rows),
    "",
    "Summary:",
    `  repo=${ghText(result.repo)} title=${ghShort(ghText(pullRequest.title), 96)}`,
    `  blockers=${blockers.length === 0 ? "-" : blockers.join(",")} pending=${pending.length === 0 ? "-" : pending.join(",")}`,
    `  retry=${retry.attempts !== undefined && retry.maxAttempts !== undefined ? `${ghText(retry.attempts)}/${ghText(retry.maxAttempts)} exhausted=${ghText(retry.exhausted)}` : "-"}`,
    `  branchDeletion=${ghText(branchDeletion.ok ?? branchDeletion.skippedReason ?? branchDeletion.attempted)}`,
    "",
    "Next:",
  ];
  if (result.ok === true && result.alreadyMerged !== true && result.dryRun !== true) {
    lines.push(`  bun scripts/cli.ts gh pr view ${ghText(result.number ?? details.number)} --repo ${ghText(result.repo)}`);
  } else {
    lines.push(`  ${prMergeRetryCommand(ghText(result.repo), result.number ?? details.number, mergeMethod, deleteBranch)}`);
    lines.push(`  bun scripts/cli.ts gh pr preflight ${ghText(result.number ?? details.number)} --repo ${ghText(result.repo)} --full`);
  }
  lines.push("", "Disclosure:");
  lines.push("  default view is a bounded table; use --full or --raw on read/preflight commands for structured metadata.");
  return lines.join("\n");
}

export async function prPreflight(repo: string, number: number, commandName: "preflight" | "pr preflight" | "pr closeout", includeRaw: boolean): Promise<GitHubCommandResult> {
  const auth = await authStatus(repo);
  const authCapability = compactAuthCapability(auth);
  const policy = prPreflightPolicy(repo, number);
  if (auth.ok === false) {
    return {
      ok: false,
      command: commandName,
      repo,
      number,
      degradedReason: auth.degradedReason,
      runnerDisposition: auth.runnerDisposition,
      authCapability,
      policy,
      details: auth,
    };
  }
  const { token, probe } = resolveToken(true);
  const missing = authRequired(repo, commandName, probe);
  if (missing !== null || token === null) {
    const missingResult = missing ?? authRequired(repo, commandName, { present: false, source: null, ghFallbackAttempted: true });
    return {
      ...(missingResult ?? commandError(commandName, repo, errorPayload("missing-token", "GH_TOKEN or GITHUB_TOKEN is required"))),
      command: commandName,
      number,
      authCapability,
      policy,
    } as GitHubCommandResult;
  }
  const { owner, name } = repoParts(repo);
  const pr = await githubRequest<GitHubPullRequest>(token, "GET", `/repos/${owner}/${name}/pulls/${number}`);
  if (isGitHubError(pr)) return commandError(commandName, repo, pr, { number, authCapability, policy });
  const metadata = await prGraphqlMetadata(repo, token, number);
  const summary = prSummary(pr);
  const pullRequest = preflightPullRequestSummary(summary);
  if (isGitHubError(metadata)) return commandError(commandName, repo, metadata, { number, phase: "fetch-pr-closeout-metadata", pullRequest, authCapability, policy });
  const metadataSummary = prMetadataSummary(metadata);
  const statusChecks = statusRollupSummary(repo, number, metadata.statusCheckRollup, includeRaw);
  const closeout = prCloseoutSummary(summary, metadataSummary, statusChecks);
  const result: GitHubCommandResult = {
    ok: true,
    command: commandName,
    canonicalCommand: `bun scripts/cli.ts gh pr preflight ${number} --repo ${repo}`,
    aliases: ["bun scripts/cli.ts gh preflight <number>", "bun scripts/cli.ts gh pr closeout <number>"],
    repo,
    number,
    readOnly: true,
    writesRemote: false,
    valuesPrinted: false,
    authCapability,
    pullRequest,
    mergeability: {
      mergeable: metadataSummary.mergeable,
      mergeStateStatus: metadataSummary.mergeStateStatus,
      ...closeout,
    },
    statusChecks,
    policy,
    ...(includeRaw ? { raw: { authStatus: auth, pullRequest, closeoutMetadata: metadataSummary } } : {}),
  };
  if (includeRaw) return result;
  return {
    ...result,
    contentType: "text/plain",
    renderedText: renderPrPreflightTable(result),
  };
}

export function renderPrPreflightTable(result: GitHubCommandResult): string {
  const pullRequest = isRecord(result.pullRequest) ? result.pullRequest : {};
  const mergeability = isRecord(result.mergeability) ? result.mergeability : {};
  const statusChecks = isRecord(result.statusChecks) ? result.statusChecks : {};
  const counts = isRecord(statusChecks.counts) ? statusChecks.counts : {};
  const policy = isRecord(result.policy) ? result.policy : {};
  const blockers = Array.isArray(mergeability.blockers) ? mergeability.blockers.map(String) : [];
  const pending = Array.isArray(mergeability.pending) ? mergeability.pending.map(String) : [];
  const conclusion = ghText(mergeability.conclusion);
  const rows = [[
    `#${ghText(result.number)}`,
    ghText(pullRequest.stateDetail ?? pullRequest.state),
    ghText(mergeability.mergeable),
    ghText(mergeability.mergeStateStatus),
    `${ghText(statusChecks.totalContexts)} total ${ghText(counts.success)} ok ${ghText(counts.failure)} fail ${ghText(counts.pending)} pending`,
    conclusion,
  ]];
  const lines = [
    `gh pr preflight (${conclusion})`,
    "",
    ghTable(["PR", "STATE", "MERGEABLE", "MERGE_STATE", "CHECKS", "CONCLUSION"], rows),
    "",
    "Summary:",
    `  repo=${ghText(result.repo)} title=${ghShort(ghText(pullRequest.title), 96)}`,
    `  head=${ghText(isRecord(pullRequest.head) ? pullRequest.head.ref : null)} base=${ghText(isRecord(pullRequest.base) ? pullRequest.base.ref : null)}`,
    `  blockers=${blockers.length === 0 ? "-" : blockers.join(",")} pending=${pending.length === 0 ? "-" : pending.join(",")}`,
    "",
    "Next:",
  ];
  const mergeCommand = ghText(policy.mergeCommand);
  if (mergeability.readyForCommanderMerge === true && mergeCommand !== "-") {
    lines.push(`  ${mergeCommand}`);
  }
  lines.push(`  bun scripts/cli.ts gh pr preflight ${ghText(result.number)} --repo ${ghText(result.repo)} --full`);
  lines.push("", "Disclosure:");
  lines.push("  default view is a bounded table; use --full or --raw for structured PR/check metadata.");
  return lines.join("\n");
}
