#!/usr/bin/env bash
# pr-state — emit a raw JSON snapshot of PR CI and review state.
#
# Usage:
#   scripts/pr-state [--summary] [--all] <PR>
#
# Raw output:
#   JSON object with:
#   - pr: { number, branch }
#   - since: { head_oid, committed_at } or null
#   - checks: [{ type, name, workflow, status, conclusion, details_url, target_url, run_id, job_id }]
#   - review_threads: [{ thread_id, is_resolved, path, comment_id, comment_created_at, author, body_preview }]
#   - unresolved_review_thread_count: <N>
#   - filtered_review_thread_count: <N>
#   - reviews: [{ author, state, submitted_at, html_url }]
#   - issue_comments: [{ author, body, created_at, url }]
#   - errors: [{ source, message }]
#
# Summary output (--summary):
#   JSON object with:
#   - pr: { number, branch }
#   - since: { head_oid, committed_at } or null
#   - failed_checks: [{ name, workflow, status, conclusion, run_id, job_id, details_url, target_url }]
#   - pending_checks: [{ name, workflow, status, run_id, job_id, details_url, target_url }]
#   - unresolved_review_thread_count: <N>
#   - filtered_review_thread_count: <N>
#   - unresolved_threads: [{ thread_id, comment_id, author, path, body_preview }]
#   - errors: [{ source, message }]
#
# By default, comments/reviews/review threads are limited to items created after
# the PR head commit. Checks are already scoped by GitHub to the current head.
# Pass --all to include historical comments and reviews.
#
# Requires: gh (authenticated), jq.

set -euo pipefail

usage() {
  sed -n '2,/^# Requires:/p' "$0" | sed 's|^# \{0,1\}||'
  exit "${1:-1}"
}

INCLUDE_ALL=0
SUMMARY=0
while [[ $# -gt 0 ]]; do
  case "$1" in
    --all)
      INCLUDE_ALL=1
      shift
      ;;
    --summary)
      SUMMARY=1
      shift
      ;;
    -h | --help)
      usage 0
      ;;
    --)
      shift
      break
      ;;
    -*)
      usage 1
      ;;
    *)
      break
      ;;
  esac
done

[ $# -eq 1 ] || usage
PR=$1

errors='[]'
pr_json='{}'
checks_json='[]'
review_threads_json='[]'
reviews_json='[]'
issue_comments_json='[]'
since_json='null'
unresolved_review_thread_count=null
filtered_review_thread_count=null

add_error() {
  local source=$1
  local message=$2
  errors="$(jq -cn --argjson prev "$errors" --arg source "$source" --arg message "$message" \
    '$prev + [{source: $source, message: $message}]')"
}

gh_json() {
  env -u GH_DEBUG -u GITHUB_DEBUG -u GH_TRACE -u GITHUB_TRACE \
    GH_PAGER=cat GH_PROMPT_DISABLED=1 GH_NO_UPDATE_NOTIFIER=1 \
    gh "$@" 2>/dev/null
}

extract_checks_json() {
  local status_json=$1
  jq -cn --argjson checks_in "$status_json" '
    def lower_or_null:
      if . == null or . == "" then null else ascii_downcase end;
    def extract($re; $field):
      if . == null then null else ((capture($re)? | .[$field]) // null) end;
    def normalized_conclusion:
      if has("conclusion") then
        (.conclusion | lower_or_null)
      else
        ((.state // "") | lower_or_null) as $state
        | if $state == "pending" or $state == "expected" then null else $state end
      end;
    $checks_in | map({
      type: (.["__typename"] // null),
      name: (.name // .context // "unknown"),
      workflow: (.workflowName // .workflow // .checkSuite.workflowRun.workflow.name // null),
      status: ((.status // .state // "unknown") | ascii_downcase),
      conclusion: normalized_conclusion,
      details_url: (.detailsUrl // null),
      target_url: (.targetUrl // null),
      run_id: ((.detailsUrl // null) | extract("/actions/runs/(?<id>[0-9]+)"; "id")),
      job_id: ((.detailsUrl // null) | extract("/job/(?<id>[0-9]+)"; "id"))
    })
  '
}

extract_issue_comments_json() {
  local comments_json=$1
  local since=$2
  jq -cn --argjson comments "$comments_json" --arg since "$since" '
    $comments
    | map(select($since == "" or ((.createdAt // .created_at // "") > $since)))
    | map({
      author: (.author.login // "unknown"),
      body: (.body // ""),
      created_at: (.createdAt // .created_at // null),
      url: (.url // null)
    })
  '
}

extract_reviews_json() {
  local reviews_api_json=$1
  local since=$2
  jq -cn --argjson reviews "$reviews_api_json" --arg since "$since" '
    $reviews
    | map(select($since == "" or ((.submitted_at // "") > $since)))
    | map({
      author: (.user.login // "unknown"),
      state: ((.state // "unknown") | ascii_downcase),
      submitted_at: (.submitted_at // null),
      html_url: (.html_url // null)
    })
  '
}

extract_review_threads_json() {
  local threads_nodes_json=$1
  local since=$2
  jq -cn --argjson threads "$threads_nodes_json" --arg since "$since" '
    def relevant_comments:
      if $since == "" then
        (.comments.nodes // [])
      else
        [(.comments.nodes // [])[] | select((.createdAt // "") > $since)]
      end;
    $threads
    | map(. as $thread | (relevant_comments) as $comments | select($comments | length > 0) | {
      thread_id: .id,
      is_resolved: (.isResolved // false),
      path: (.path // ""),
      comment_id: ($comments[0].databaseId // null),
      comment_created_at: ($comments[0].createdAt // null),
      author: ($comments[0].author.login // "unknown"),
      body_preview: (($comments[0].body // "") | gsub("\n"; " ") | .[0:120])
    })
  '
}

summarize_state_json() {
  jq -c '
    def bad_conclusion:
      .conclusion != null
      and .conclusion != "success"
      and .conclusion != "skipped"
      and .conclusion != "neutral";

    def terminal_failure_status:
      .status == "completed"
      or .status == "failure"
      or .status == "error";

    def pending_status:
      .status != "completed"
      and .status != "success"
      and .status != "failure"
      and .status != "error";

    {
      pr,
      since,
      failed_checks: [
        .checks[]
        | select(terminal_failure_status and bad_conclusion)
        | {name, workflow, status, conclusion, run_id, job_id, details_url, target_url}
      ],
      pending_checks: [
        .checks[]
        | select(pending_status and .conclusion == null)
        | {name, workflow, status, run_id, job_id, details_url, target_url}
      ],
      unresolved_review_thread_count,
      filtered_review_thread_count,
      unresolved_threads: [
        .review_threads[]
        | select(.is_resolved == false)
        | {thread_id, comment_id, author, path, body_preview}
      ],
      errors
    }
  '
}

fetch_head_commit_json() {
  local owner=$1
  local repo=$2
  local pr_number=$3

  gh_json api graphql \
    -f query='
      query($owner: String!, $repo: String!, $pr: Int!) {
        repository(owner: $owner, name: $repo) {
          pullRequest(number: $pr) {
            headRefOid
            commits(last: 1) {
              nodes {
                commit {
                  oid
                  committedDate
                }
              }
            }
          }
        }
      }' \
    -f owner="$owner" -f repo="$repo" -F pr="$pr_number" |
    jq -c '{
      head_oid: (.data.repository.pullRequest.headRefOid // .data.repository.pullRequest.commits.nodes[0].commit.oid // null),
      committed_at: (.data.repository.pullRequest.commits.nodes[0].commit.committedDate // null)
    }'
}

fetch_review_threads_nodes_json() {
  local owner=$1
  local repo=$2
  local pr_number=$3
  local cursor=''
  local nodes='[]'

  while :; do
    local cursor_args=()
    if [[ -n "$cursor" ]]; then
      cursor_args=(-f cursor="$cursor")
    fi

    local page_json
    if ! page_json="$(gh_json api graphql \
      -f query='
        query($owner: String!, $repo: String!, $pr: Int!, $cursor: String) {
          repository(owner: $owner, name: $repo) {
            pullRequest(number: $pr) {
              reviewThreads(first: 100, after: $cursor) {
                pageInfo {
                  hasNextPage
                  endCursor
                }
                nodes {
                  id
                  isResolved
                  path
                  comments(last: 100) {
                    nodes {
                      databaseId
                      createdAt
                      author { login }
                      body
                    }
                  }
                }
              }
            }
          }
        }' \
      -f owner="$owner" -f repo="$repo" -F pr="$pr_number" "${cursor_args[@]}")"; then
      return 1
    fi

    local page_nodes
    page_nodes="$(jq -c '.data.repository.pullRequest.reviewThreads.nodes // []' <<<"$page_json")"
    nodes="$(jq -cn --argjson prev "$nodes" --argjson page "$page_nodes" '$prev + $page')"

    local has_next
    has_next="$(jq -r '.data.repository.pullRequest.reviewThreads.pageInfo.hasNextPage // false' <<<"$page_json")"
    if [[ "$has_next" != "true" ]]; then
      break
    fi

    cursor="$(jq -r '.data.repository.pullRequest.reviewThreads.pageInfo.endCursor // ""' <<<"$page_json")"
    if [[ -z "$cursor" ]]; then
      add_error "review_threads" "pagination stopped: missing endCursor"
      break
    fi
  done

  printf '%s\n' "$nodes"
}

repo_owner=''
repo_name=''
if repo_json="$(gh_json repo view --json owner,name)"; then
  repo_owner="$(jq -r '.owner.login // ""' <<<"$repo_json")"
  repo_name="$(jq -r '.name // ""' <<<"$repo_json")"
else
  add_error "repo" "gh repo view failed"
fi

comments_json='[]'
status_json='[]'
resolved_pr_number=''
if pr_view_json="$(gh_json pr view "$PR" --json number,headRefName,statusCheckRollup,comments,url)"; then
  resolved_pr_number="$(jq -r '.number // ""' <<<"$pr_view_json")"
  if pr_url_owner="$(jq -r '.url | capture("github.com/(?<owner>[^/]+)/(?<repo>[^/]+)/pull/[0-9]+").owner // ""' <<<"$pr_view_json" 2>/dev/null)" && [[ -n "$pr_url_owner" ]]; then
    repo_owner="$pr_url_owner"
    repo_name="$(jq -r '.url | capture("github.com/(?<owner>[^/]+)/(?<repo>[^/]+)/pull/[0-9]+").repo // ""' <<<"$pr_view_json")"
  fi
  pr_json="$(jq -c '{number: .number, branch: (.headRefName // "unknown")}' <<<"$pr_view_json")"
  comments_json="$(jq -c '.comments // []' <<<"$pr_view_json")"
  status_json="$(jq -c '.statusCheckRollup // []' <<<"$pr_view_json")"
  checks_json="$(extract_checks_json "$status_json")"
else
  if [[ "$PR" =~ ^[0-9]+$ ]]; then
    pr_json="$(jq -cn --argjson number "$PR" '{number: $number, branch: "unknown"}')"
  else
    pr_json="$(jq -cn --arg branch "$PR" '{number: null, branch: $branch}')"
  fi
  add_error "pr_view" "gh pr view failed"
fi

since=''
if [[ "$INCLUDE_ALL" != "1" && -n "$repo_owner" && -n "$repo_name" && -n "$resolved_pr_number" ]]; then
  if since_json="$(fetch_head_commit_json "$repo_owner" "$repo_name" "$resolved_pr_number")"; then
    since="$(jq -r '.committed_at // ""' <<<"$since_json")"
    if [[ -z "$since" ]]; then
      add_error "since" "head commit timestamp unavailable; including historical comments"
      since_json='null'
    fi
  else
    add_error "since" "gh api graphql head commit failed; including historical comments"
    since_json='null'
  fi
fi

issue_comments_json="$(extract_issue_comments_json "$comments_json" "$since")"

if [[ -n "$repo_owner" && -n "$repo_name" && -n "$resolved_pr_number" ]]; then
  if reviews_pages_json="$(gh_json api --paginate -X GET "repos/$repo_owner/$repo_name/pulls/$resolved_pr_number/reviews")"; then
    reviews_api_json="$(jq -cs 'add // []' <<<"$reviews_pages_json")"
    reviews_json="$(extract_reviews_json "$reviews_api_json" "$since")"
  else
    add_error "reviews" "gh api pulls reviews failed"
  fi
else
  add_error "reviews" "skipped: resolved PR identity unavailable"
fi

if [[ -n "$repo_owner" && -n "$repo_name" && -n "$resolved_pr_number" ]]; then
  if threads_nodes_json="$(fetch_review_threads_nodes_json "$repo_owner" "$repo_name" "$resolved_pr_number")"; then
    review_threads_json="$(extract_review_threads_json "$threads_nodes_json" "$since")"
    unresolved_review_thread_count="$(jq -r '[.[] | select(.isResolved == false)] | length' <<<"$threads_nodes_json")"
    filtered_review_thread_count="$(jq -r 'length' <<<"$review_threads_json")"
  else
    add_error "review_threads" "gh api graphql reviewThreads failed"
  fi
elif [[ -z "$repo_owner" || -z "$repo_name" ]]; then
  add_error "review_threads" "skipped: repo lookup failed"
elif [[ -z "$resolved_pr_number" ]]; then
  add_error "review_threads" "skipped: resolved PR identity unavailable"
fi

state_json="$(jq -cn \
  --argjson pr "$pr_json" \
  --argjson since "$since_json" \
  --argjson checks "$checks_json" \
  --argjson review_threads "$review_threads_json" \
  --argjson reviews "$reviews_json" \
  --argjson issue_comments "$issue_comments_json" \
  --argjson unresolved_review_thread_count "$unresolved_review_thread_count" \
  --argjson filtered_review_thread_count "$filtered_review_thread_count" \
  --argjson errors "$errors" \
  '{
    pr: $pr,
    since: $since,
    checks: $checks,
    review_threads: $review_threads,
    unresolved_review_thread_count: $unresolved_review_thread_count,
    filtered_review_thread_count: $filtered_review_thread_count,
    reviews: $reviews,
    issue_comments: $issue_comments,
    errors: $errors
  }')"

if [[ "$SUMMARY" == "1" ]]; then
  summarize_state_json <<<"$state_json"
else
  printf '%s\n' "$state_json"
fi
