/// <reference types="bun-types" />
/**
 * Reply to + resolve all unresolved review threads on a PR.
 *
 * Usage: bun /tmp/resolve-threads.ts <owner> <repo> <prNumber> "<replyBody>"
 *
 * Token: read from GH_TOKEN env, or `gh auth token`.
 */
import { spawnSync } from "node:child_process";

const [owner, repo, prStr, replyBody] = process.argv.slice(2);
if (!owner || !repo || !prStr || !replyBody) {
  console.error("usage: bun resolve-threads.ts <owner> <repo> <pr> <reply>");
  process.exit(2);
}
const pr = Number(prStr);

let token = process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN ?? "";
if (!token) {
  const r = spawnSync("gh", ["auth", "token"], { encoding: "utf8" });
  token = (r.stdout ?? "").trim();
}
if (!token) {
  console.error("no GitHub token");
  process.exit(1);
}

async function gql<T>(query: string, variables: Record<string, unknown>): Promise<T> {
  const res = await fetch("https://api.github.com/graphql", {
    method: "POST",
    headers: {
      Authorization: `bearer ${token}`,
      "Content-Type": "application/json",
      "User-Agent": "resolve-threads",
    },
    body: JSON.stringify({ query, variables }),
  });
  const body = (await res.json()) as { data?: T; errors?: unknown };
  if (body.errors) {
    throw new Error(`GraphQL error: ${JSON.stringify(body.errors)}`);
  }
  return body.data as T;
}

interface ThreadsResp {
  repository: {
    pullRequest: {
      reviewThreads: {
        nodes: Array<{
          id: string;
          isResolved: boolean;
          isOutdated: boolean;
          comments: { nodes: Array<{ id: string; body: string }> };
        }>;
        pageInfo: { hasNextPage: boolean; endCursor: string | null };
      };
    };
  };
}

const THREADS_Q = `
query($owner:String!,$repo:String!,$pr:Int!,$cursor:String){
  repository(owner:$owner,name:$repo){
    pullRequest(number:$pr){
      reviewThreads(first:50, after:$cursor){
        nodes{ id isResolved isOutdated comments(first:1){ nodes{ id body } } }
        pageInfo{ hasNextPage endCursor }
      }
    }
  }
}`;

const REPLY_M = `
mutation($threadId:ID!,$body:String!){
  addPullRequestReviewThreadReply(input:{pullRequestReviewThreadId:$threadId, body:$body}){
    comment{ id }
  }
}`;

const RESOLVE_M = `
mutation($threadId:ID!){
  resolveReviewThread(input:{threadId:$threadId}){ thread{ id isResolved } }
}`;

async function main(): Promise<void> {
  let cursor: string | null = null;
  const threads: ThreadsResp["repository"]["pullRequest"]["reviewThreads"]["nodes"] = [];
  for (;;) {
    const d = await gql<ThreadsResp>(THREADS_Q, { owner, repo, pr, cursor });
    const rt = d.repository.pullRequest.reviewThreads;
    threads.push(...rt.nodes);
    if (!rt.pageInfo.hasNextPage) break;
    cursor = rt.pageInfo.endCursor;
  }

  let resolved = 0;
  for (const t of threads) {
    if (t.isResolved) continue;
    try {
      await gql(REPLY_M, { threadId: t.id, body: replyBody });
    } catch (e) {
      console.error(`reply failed on ${t.id}: ${String(e)}`);
    }
    try {
      await gql(RESOLVE_M, { threadId: t.id });
      resolved += 1;
    } catch (e) {
      console.error(`resolve failed on ${t.id}: ${String(e)}`);
    }
  }
  console.log(`threads total=${threads.length} resolved-now=${resolved}`);
}

main().catch((e) => {
  console.error(e);
  process.exit(1);
});
