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

import path from "node:path";
import { ghBinaryPath, repoParts, resolveToken } from "./auth-and-safety";
import { commandError, errorPayload, githubRequest, isGitHubError, prBodyReadCommands, runnerDisposition } from "./client";
import { issueRead } from "./issue-read";
import { needsPrGraphqlMetadata, numberOrNull, prCloseoutMetadata, prCloseoutMetadataError, prCompactSummary, prFileSummary, prGraphqlMetadata, prMetadataSummary, prSummary, selectedPrJson, sumPrFileStats } from "./pr-summary";
import { probeRepositoryWriteCapability } from "./repository-capability";
import { MAX_PR_FILES_LIMIT, PR_LIST_JSON_FIELDS } from "./types";
import type { GitHubCommandResult, GitHubDegradedReason, GitHubIssue, GitHubPullRequest, GitHubPullRequestFile, GitHubRepository, PrListJsonField, PrListState, PrReadJsonField } from "./types";

type RepositoryCapabilityStatus = "available" | "unavailable" | "unknown";

function capabilityStatus(available: boolean | null): RepositoryCapabilityStatus {
  if (available === true) return "available";
  if (available === false) return "unavailable";
  return "unknown";
}

function repositoryPermissionLevel(permissions: Record<string, boolean> | undefined): "admin" | "maintain" | "write" | "read" | "unknown" {
  if (permissions === undefined) return "unknown";
  if (permissions.admin === true) return "admin";
  if (permissions.maintain === true) return "maintain";
  if (permissions.push === true) return "write";
  if (permissions.pull === true || permissions.triage === true) return "read";
  return "unknown";
}

function repositoryCapabilities(repo: GitHubRepository, issueReadable: boolean, repositoryWrite: ReturnType<typeof probeRepositoryWriteCapability>): Record<string, unknown> {
  const permissionLevel = repositoryPermissionLevel(repo.permissions);
  return {
    repositoryRead: {
      status: capabilityStatus(issueReadable),
      available: issueReadable,
      evidence: "repository-and-issue-read",
    },
    repositoryWrite: {
      ...repositoryWrite,
      permissionLevel,
    },
    pullRequestMerge: {
      status: repositoryWrite.status,
      available: repositoryWrite.available,
      permissionLevel,
      inferredFrom: "repository-write-git-push-dry-run",
      requiredPermission: "contents:write",
      writesRemote: false,
      mutation: false,
      caveat: "guarded merge still checks branch protection, checks, and PR state",
    },
    valuesPrinted: false,
  };
}

export async function authStatus(repo: string): Promise<GitHubCommandResult> {
  const ghPath = ghBinaryPath();
  const { token, probe } = resolveToken(repo, ghPath !== null);
  const degraded: GitHubDegradedReason[] = [];
  if (ghPath === null) degraded.push("missing-binary");
  if (!probe.present || token === null) {
    if (ghPath === null) {
      return {
        ok: false,
        command: "auth status",
        repo,
        degradedReason: "missing-binary",
        degraded,
        runnerDisposition: runnerDisposition("missing-binary"),
        gh: { binaryFound: false, path: null },
        token: probe,
        probes: { restApi: "skipped", repo: "skipped", issueRead: "skipped" },
      };
    }
    degraded.push("missing-token");
    return commandError("auth status", repo, errorPayload("missing-token", "GH_TOKEN, GITHUB_TOKEN, or the YAML-declared GitHub token source is required", { details: probe }), {
      degraded,
      gh: { binaryFound: ghPath !== null, path: ghPath },
      token: probe,
      probes: { restApi: "skipped", repo: "skipped", issueRead: "skipped" },
    });
  }

  const { owner, name } = repoParts(repo);
  const api = await githubRequest<Record<string, unknown>>(token, "GET", "/rate_limit");
  if (isGitHubError(api)) {
    return commandError("auth status", repo, api, {
      degraded: [...degraded, api.degradedReason],
      gh: { binaryFound: ghPath !== null, path: ghPath },
      token: probe,
      probes: { restApi: api, repo: "skipped", issueRead: "skipped" },
    });
  }

  const repoProbe = await githubRequest<GitHubRepository>(token, "GET", `/repos/${owner}/${name}`);
  if (isGitHubError(repoProbe)) {
    return commandError("auth status", repo, repoProbe, {
      degraded: [...degraded, repoProbe.degradedReason],
      gh: { binaryFound: ghPath !== null, path: ghPath },
      token: probe,
      probes: { restApi: "ok", repo: repoProbe, issueRead: "skipped" },
    });
  }

  const issueProbe = await githubRequest<GitHubIssue[]>(token, "GET", `/repos/${owner}/${name}/issues?per_page=1&state=all`);
  if (isGitHubError(issueProbe)) {
    return commandError("auth status", repo, issueProbe, {
      degraded: [...degraded, issueProbe.degradedReason],
      gh: { binaryFound: ghPath !== null, path: ghPath },
      token: probe,
      probes: { restApi: "ok", repo: { ok: true, fullName: repoProbe.full_name ?? repo, private: repoProbe.private ?? null }, issueRead: issueProbe },
    });
  }

  const repositoryWrite = probeRepositoryWriteCapability(repo, token);
  const capabilities = repositoryCapabilities(repoProbe, true, repositoryWrite);

  return {
    ok: true,
    command: "auth status",
    repo,
    degraded,
    gh: { binaryFound: ghPath !== null, path: ghPath },
    token: probe,
    probes: {
      restApi: "ok",
      repo: { ok: true, fullName: repoProbe.full_name ?? repo, private: repoProbe.private ?? null },
      issueRead: { ok: true, readable: true, sampleCount: issueProbe.length },
    },
    capabilities,
    warnings: (capabilities.repositoryWrite as { available: boolean | null }).available === true ? [] : [{
      warning: true,
      blocking: false,
      code: "github-repository-write-capability-unavailable",
      capability: "pull-request-merge",
      status: (capabilities.pullRequestMerge as { status: RepositoryCapabilityStatus }).status,
    }],
    restFallback: true,
    valuesPrinted: false,
  };
}

export async function prList(repo: string, token: string, state: PrListState, limit: number, jsonFields: PrListJsonField[] | undefined, noDump = false): Promise<GitHubCommandResult> {
  const { owner, name } = repoParts(repo);
  const prs = await githubRequest<GitHubPullRequest[]>(token, "GET", `/repos/${owner}/${name}/pulls?state=${state}&per_page=${limit}`);
  if (isGitHubError(prs)) return commandError("pr list", repo, prs, { state, limit });
  const fields = jsonFields ?? PR_LIST_JSON_FIELDS.slice();
  return {
    ok: true,
    command: "pr list",
    repo,
    ...(noDump ? { noDump: true } : {}),
    state,
    limit,
    count: prs.length,
    jsonFields: fields,
    pullRequests: prs.map((pr) => selectedPrJson(prSummary(pr), fields)),
  };
}

export async function prFiles(repo: string, token: string, number: number, limit: number, commandName = "pr files"): Promise<GitHubCommandResult> {
  const { owner, name } = repoParts(repo);
  const boundedLimit = Math.min(limit, MAX_PR_FILES_LIMIT);
  const pr = await githubRequest<GitHubPullRequest>(token, "GET", `/repos/${owner}/${name}/pulls/${number}`);
  if (isGitHubError(pr)) return commandError(commandName, repo, pr, { number });
  const perPage = Math.max(1, Math.min(100, boundedLimit));
  const files: GitHubPullRequestFile[] = [];
  let page = 1;
  while (files.length < boundedLimit) {
    const remaining = boundedLimit - files.length;
    const pageSize = Math.min(perPage, remaining);
    const path = `/repos/${owner}/${name}/pulls/${number}/files?per_page=${pageSize}&page=${page}`;
    const pageFiles = await githubRequest<GitHubPullRequestFile[]>(token, "GET", path);
    if (isGitHubError(pageFiles)) return commandError(commandName, repo, pageFiles, { number, phase: "fetch-pr-files", filesReturned: files.length });
    files.push(...pageFiles);
    if (pageFiles.length < pageSize || pageFiles.length === 0) break;
    page += 1;
  }
  const totalFiles = numberOrNull(pr.changed_files);
  const listedStats = sumPrFileStats(files);
  const totalAdditions = numberOrNull(pr.additions);
  const totalDeletions = numberOrNull(pr.deletions);
  const fullStatsAvailable = totalAdditions !== null && totalDeletions !== null;
  const totalChanges = fullStatsAvailable ? totalAdditions + totalDeletions : listedStats.changes;
  const truncated = totalFiles !== null ? files.length < totalFiles : files.length >= boundedLimit;
  const nextLimit = totalFiles === null ? MAX_PR_FILES_LIMIT : Math.min(totalFiles, MAX_PR_FILES_LIMIT);
  const nextCommand = truncated
    ? `bun scripts/cli.ts gh pr files ${number} --repo ${repo} --limit ${nextLimit}`
    : `trans gh:/${repo}/pr/${number} cat`;
  return {
    ok: true,
    command: commandName,
    repo,
    readCommands: prBodyReadCommands(repo, number),
    readOnly: true,
    rawDiffIncluded: false,
    pullRequest: prCompactSummary(pr),
    summary: {
      files: totalFiles ?? files.length,
      additions: totalAdditions ?? listedStats.additions,
      deletions: totalDeletions ?? listedStats.deletions,
      changes: totalChanges,
      commits: numberOrNull(pr.commits),
      source: fullStatsAvailable && totalFiles !== null ? "pull-request-rest" : "listed-files-rest",
    },
    files: files.map(prFileSummary),
    filesReturned: files.length,
    limit: boundedLimit,
    truncation: {
      truncated,
      requestedLimit: limit,
      appliedLimit: boundedLimit,
      returned: files.length,
      totalFiles,
      maxLimit: MAX_PR_FILES_LIMIT,
    },
    next: {
      command: nextCommand,
      purpose: truncated ? "Fetch a larger bounded file summary page." : "Read the full PR body through the human text route; use readCommands.metadata for bounded metadata.",
    },
    request: {
      method: "GET",
      paths: [
        `/repos/${owner}/${name}/pulls/${number}`,
        `/repos/${owner}/${name}/pulls/${number}/files`,
      ],
    },
  };
}

export async function prRead(repo: string, token: string, number: number, jsonFields: PrReadJsonField[] | undefined, commandName = "pr read", disclosure: Record<string, unknown> | null = null): Promise<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 });
  const fullSummary = prSummary(pr);
  const includeBody = jsonFields === undefined || jsonFields.includes("body");
  const summary = includeBody ? fullSummary : prSummary(pr, { includeBody: false, includePreview: false });
  const metadata = needsPrGraphqlMetadata(jsonFields) ? await prGraphqlMetadata(repo, token, number) : null;
  if (isGitHubError(metadata)) return commandError(commandName, repo, metadata, { number, phase: "fetch-pr-closeout-metadata", pullRequest: summary, requestedJsonFields: jsonFields ?? [], closeoutMetadata: prCloseoutMetadataError(metadata) });
  const selectionSummary = metadata === null ? fullSummary : { ...fullSummary, ...prMetadataSummary(metadata) };
  const selectedJson = jsonFields === undefined ? null : selectedPrJson(selectionSummary, jsonFields);
  if (includeBody && selectedJson !== null) delete selectedJson.body;
  return {
    ok: true,
    command: commandName,
    repo,
    readCommands: prBodyReadCommands(repo, number),
    ...(disclosure === null ? {} : { disclosure }),
    pullRequest: summary,
    ...(metadata === null ? {} : { closeoutMetadata: prCloseoutMetadata(metadata) }),
    ...(jsonFields === undefined ? {} : {
      jsonFields,
      json: selectedJson,
      compatibility: {
        legacyJsonBodyPath: includeBody ? ".data.pullRequest.body" : null,
        bodyPath: includeBody ? ".data.pullRequest.body" : null,
        omittedDuplicateBodyPath: includeBody ? ".data.json.body" : null,
        duplicateBodyOmitted: includeBody,
        bodyOmitted: !includeBody,
        bodyMigrationHint: includeBody
          ? "Read the full body from .data.pullRequest.body; .data.json.body is intentionally omitted so large bodies are emitted only once."
          : null,
        readCommands: prBodyReadCommands(repo, number),
      },
    }),
  };
}

export async function prView(repo: string, token: string, number: number, jsonFields: PrReadJsonField[] | undefined, disclosure: Record<string, unknown> | null = null): Promise<GitHubCommandResult> {
  return prRead(repo, token, number, jsonFields, "pr view", disclosure);
}
