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

import { repoParts } from "./auth-and-safety";
import { commandError, githubRequest, isGitHubError, runnerDisposition } from "./client";
import { issueListPaginationSummary, listIssues } from "./issue-read";
import { issueLifecycleBatchSummary } from "./issue-summary";
import { MAX_ISSUE_LIST_LIMIT } from "./types";
import type { GitHubCommandResult, GitHubDegradedReason, GitHubErrorPayload, GitHubIssue, GitHubOptions, RunnerDisposition } from "./types";

export function parseGitHubTimestamp(value: string | undefined): number | null {
  if (value === undefined) return null;
  const millis = Date.parse(value);
  return Number.isFinite(millis) ? millis : null;
}

export function inactiveIssueCandidates(issues: GitHubIssue[], cutoffMs: number): GitHubIssue[] {
  return issues.filter((issue) => {
    const updatedAtMs = parseGitHubTimestamp(issue.updated_at);
    return updatedAtMs !== null && updatedAtMs < cutoffMs;
  });
}

export async function closeIssueForBatch(repo: string, token: string, issueNumber: number): Promise<GitHubIssue | GitHubErrorPayload> {
  const { owner, name } = repoParts(repo);
  return githubRequest<GitHubIssue>(token, "PATCH", `/repos/${owner}/${name}/issues/${issueNumber}`, { state: "closed" });
}

export async function issueStaleClose(repo: string, token: string, options: GitHubOptions): Promise<GitHubCommandResult> {
  const command = "issue stale-close";
  const observedAt = new Date();
  const cutoffMs = observedAt.getTime() - Math.round(options.inactiveHours * 60 * 60 * 1000);
  const cutoffAt = new Date(cutoffMs).toISOString();
  const result = await listIssues(token, repo, "open", options.limit, "", options.labels);
  if (isGitHubError(result)) {
    return commandError(command, repo, result, {
      state: "open",
      limit: options.limit,
      inactiveHours: options.inactiveHours,
      cutoffAt,
      labels: options.labels,
    });
  }
  const staleIssues = inactiveIssueCandidates(result.items, cutoffMs);
  const base = {
    ok: true,
    command,
    repo,
    dryRun: options.dryRun,
    state: "open",
    inactiveHours: options.inactiveHours,
    observedAt: observedAt.toISOString(),
    cutoffAt,
    limit: options.limit,
    labels: options.labels,
    scannedCount: result.items.length,
    rawCount: result.rawCount,
    staleCount: staleIssues.length,
    pagination: issueListPaginationSummary(result),
    hasMore: result.hasMore,
    stale: issueLifecycleBatchSummary(staleIssues),
    policy: {
      basis: "GitHub issue updatedAt",
      selectedWhen: "updatedAt is older than observedAt - inactiveHours",
      commentsAndStateChangesCountAsActivity: true,
      pullRequestsFiltered: true,
    },
    readCommands: {
      dryRun: `bun scripts/cli.ts gh issue stale-close --repo ${repo} --inactive-hours ${options.inactiveHours} --limit ${options.limit} --dry-run`,
      openList: `bun scripts/cli.ts gh issue list --repo ${repo} --state open --limit ${options.limit} --json number,title,state,url,updatedAt`,
    },
    ...(result.hasMore && options.limit < MAX_ISSUE_LIST_LIMIT
      ? {
        next: {
          command: `bun scripts/cli.ts gh issue stale-close --repo ${repo} --inactive-hours ${options.inactiveHours} --limit ${Math.min(options.limit * 2, MAX_ISSUE_LIST_LIMIT)} --dry-run`,
          note: "The scan reached the bounded --limit before GitHub pagination was exhausted; increase --limit before treating the cleanup as complete.",
        },
      }
      : result.hasMore
        ? {
          scanLimit: {
            maxLimitReached: true,
            maxLimit: MAX_ISSUE_LIST_LIMIT,
            note: "The cleanup reached the maximum bounded issue scan; narrow with --label or split the policy before treating it as complete.",
          },
        }
      : {}),
  };
  if (options.dryRun) {
    return {
      ...base,
      planned: true,
      wouldCloseCount: staleIssues.length,
      wouldCloseNumbers: staleIssues.map((issue) => issue.number),
      note: staleIssues.length === 0
        ? "No open issues matched the inactive-hours policy; no GitHub issue would be closed."
        : "Dry-run only; no GitHub issue was closed.",
    };
  }

  const closed: GitHubIssue[] = [];
  const failures: Array<Record<string, unknown>> = [];
  for (const issue of staleIssues) {
    const closedIssue = await closeIssueForBatch(repo, token, issue.number);
    if (isGitHubError(closedIssue)) {
      failures.push({
        number: issue.number,
        title: issue.title,
        url: issue.html_url,
        updatedAt: issue.updated_at ?? null,
        degradedReason: closedIssue.degradedReason,
        runnerDisposition: closedIssue.runnerDisposition,
        message: closedIssue.message,
        status: closedIssue.status ?? null,
      });
      continue;
    }
    closed.push(closedIssue);
  }

  return {
    ...base,
    dryRun: false,
    planned: false,
    closedCount: closed.length,
    failedCount: failures.length,
    closed: issueLifecycleBatchSummary(closed),
    failures,
    rest: true,
    ok: failures.length === 0,
    ...(failures.length > 0
      ? {
        degradedReason: "github-transient" as GitHubDegradedReason,
        runnerDisposition: "infra-blocked" as RunnerDisposition,
        retryable: true,
      }
      : {}),
    note: failures.length === 0
      ? "Closed all open issues that matched the inactive-hours policy."
      : "Some stale issue close operations failed; rerun the same command after checking failures.",
  };
}
