#!/usr/bin/env bash
set -euo pipefail
source "$SHELLOPS_DIR/.envrc"

:<<'DOC'
Blizz auto-triage pipeline.

Collects untriaged Linear issues org-wide, invokes a Cursor cloud agent
to assess each one, and writes results back to Linear (labels, comment,
optional team migration).

Triage-eligible repos are non-archived Forgejo repos that set
"blizz": { "triage": true } in their manifest.json.
DOC

__triage__ACTION_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
__triage__BATCH_PROMPT_YML="$__triage__ACTION_DIR/triage.batch-prompt.yml"
__triage__SYSTEM_MD="$__triage__ACTION_DIR/triage.system.md"
__triage__BATCH_SCHEMA_JSON="$SHELLOPS_DIR/schema/triage-batch-output.schema.json"
__triage__BOOTSTRAP_REPO="${CURSOR_BOOTSTRAP_REPO:-https://github.com/kernelle-soft/cursor-bootstrap}"
__triage__REPO_LABEL_COLOR="#668899"
# shellcheck disable=SC2016
__triage__COMMENT_LABEL_BULLET_TEMPLATE='"- `\(.)`"'

__triage_active_agent_id=""
__triage_active_cursor_options=""

import \
  "$SHELLOPS_DIR/lib/blizz.api.sh" \
  "$SHELLOPS_DIR/lib/forgejo.api.sh" \
  "$SHELLOPS_DIR/lib/linear.api.sh" \
  "$SHELLOPS_DIR/lib/cursor.api.sh"

function __triage__scalar_field() {
  local json="$1"
  local path="$2"
  local query

  if ! jq_is_valid "$json"; then
    fatal "__triage__scalar_field: invalid JSON"
    return 1
  fi

  query="$(__json_api__normalize_path "$path")"
  jq -r "${query} | if type == \"array\" then .[0] // \"\" elif type == \"string\" then . elif . == null then \"\" else tostring end" <<< "$json"
}

function __triage__object_keys_prefixed() {
  local obj="$1"
  local prefix="$2"
  local unexpected_type

  unexpected_type=$(__json_api__validate_path_type "$obj" "." "^object$")
  if [[ -n "$unexpected_type" ]]; then
    fatal "__triage__object_keys_prefixed: expected object, got '$unexpected_type'"
    return 1
  fi

  jq -c --arg p "$prefix" '[keys[] | $p + .]' <<< "$obj"
}

USAGE="$(cat <<EOF
Blizz auto-triage pipeline.

Usage: action blizz triage [options]

Flags:
  --dry-run          Print prompt per issue, skip agent invocation and
                     Linear write-back.
  --limit N          Cap issues per run (default: 50).
  -h, --help         Show this help text.

Environment:
  LINEAR_API_KEY           Required. API token for Linear.
  FORGE_ADMIN_TOKEN            Required. API token for Forgejo repo discovery.
  CURSOR_AGENT_API_KEY     Required unless --dry-run. Cursor Cloud Agents API key.
  CURSOR_AGENT_MODEL       Optional. Cloud agent model id (default: composer-2).
                           Override if your workspace uses different ids; list via
                           GET https://api.cursor.com/v0/models with the same API key.
EOF
)"

ARG_DRY_RUN=false
ARG_LIMIT=50

function main() {
  parse_args "$@"
  validate_env

  local linear_options repos base_labels teams label_map team_map issues ctx

  linear_options="$(jq_set "$(new_linear_options)" '.token' "$LINEAR_API_KEY")"
  repos="$(discover_repos)"
  base_labels="$(load_base_labels)"
  teams="$(linear_list_teams "$linear_options")"
  label_map="$(ensure_linear_labels "$linear_options" "$repos" "$base_labels")"
  team_map="$(build_repo_team_map "$repos" "$teams")"
  issues="$(collect_issues "$linear_options" "$team_map")"

  ctx="$(jq_set '{}' \
    '.repos'       "$repos" \
    '.base_labels' "$base_labels" \
    '.label_map'   "$label_map" \
    '.team_map'    "$team_map")"

  if [[ "$ARG_DRY_RUN" == "true" ]]; then
    local count to_process batch prompt
    count="$(jq_len "$issues")"
    to_process=$(( count < ARG_LIMIT ? count : ARG_LIMIT ))

    log "DRY RUN — rendering batch prompt for $to_process issue(s)..."
    batch="$(jq_take "$issues" "$to_process")"
    prompt="$(render_batch_prompt "$batch" "$ctx")"
    printf '%s\n' "$prompt"
    return
  fi

  process_issues "$issues" "$linear_options" "$ctx"
}

function render_batch_prompt() {
  local batch="$1" ctx="$2"
  local repos base_labels system_prompt schema count rendered issues_text

  repos="$(jq_get "$ctx" '.repos')"
  base_labels="$(jq_get "$ctx" '.base_labels')"
  system_prompt="$(render_system_prompt "$repos" "$base_labels")"
  schema="$(cat "$__triage__BATCH_SCHEMA_JSON")"
  count="$(jq_len "$batch")"
  issues_text="$(jq_dump "$batch")"

  if ! jq_is_valid "$schema"; then
    error "render_batch_prompt: batch schema is not valid JSON: $__triage__BATCH_SCHEMA_JSON"
    return 1
  fi

  if [[ ! -f "$__triage__BATCH_PROMPT_YML" ]]; then
    error "Batch prompt template not found: $__triage__BATCH_PROMPT_YML"
    return 1
  fi

  rendered="$(cat "$__triage__BATCH_PROMPT_YML")"
  rendered="$(string_replace "$rendered" '{{system_prompt}}' "$system_prompt")"
  rendered="$(string_replace "$rendered" '{{count}}' "$count")"
  rendered="$(string_replace "$rendered" '{{issues}}' "$issues_text")"
  rendered="$(string_replace "$rendered" '{{schema}}' "$schema")"
  printf '%s\n' "$rendered"
}

function validate_env() {
  if [[ -z "${LINEAR_API_KEY:-}" ]]; then
    error "LINEAR_API_KEY is not set"; exit 1
  fi

  if [[ -z "${FORGE_ADMIN_TOKEN:-}" ]]; then
    error "FORGE_ADMIN_TOKEN is not set"; exit 1
  fi

  if [[ "$ARG_DRY_RUN" != "true" && -z "${CURSOR_AGENT_API_KEY:-}" ]]; then
    error "CURSOR_AGENT_API_KEY is not set (required unless --dry-run)"; exit 1
  fi
}

function discover_repos() {
  local org_options repos repo_names eligible
  org_options="$(jq_set "$(new_org_options)" '.token' "$FORGE_ADMIN_TOKEN")"

  log "Discovering triage-eligible repos..."
  repos="$(forgejo_list_org_repos "$org_options")"
  repo_names="$(jq_pluck "$repos" '.name')"

  eligible="$(jq_reduce __triage__reduce_eligible "$repo_names" '[]')"

  log "Found $(jq_len "$eligible") eligible repo(s)."
  echo "$eligible"
}

function __triage__reduce_eligible() {
  local acc="$1" name_json="$2"
  local name repo_entry

  name="$(jq_unwrap "$name_json")"
  repo_entry="$(try_eligible_repo "$name")" || {
    echo "$acc"
    return 0
  }

  jq_push "$acc" "$repo_entry"
}

function try_eligible_repo() {
  local repo_name="$1"
  local file_options manifest

  file_options="$(jq_set '{}' \
    '.owner' "$(manifest_get org)" \
    '.repo'  "$repo_name" \
    '.token' "$FORGE_ADMIN_TOKEN")"

  manifest="$(forgejo_get_raw_file "$file_options" "manifest.json" 2>/dev/null)" || return 1

  if ! jq_is_valid "$manifest"; then
    warn "Skipping $repo_name: manifest.json is not valid JSON"
    return 1
  fi

  if ! blizz_triage_enabled "$manifest"; then
    return 1
  fi

  build_repo_entry "$repo_name" "$manifest"
}


function build_repo_entry() {
  local repo_name="$1" manifest="$2"
  local description areas linear_team

  description="$(jq_get "$manifest" '.description' '')"
  areas="$(jq_get "$manifest" '.labels.area' '{}')"
  linear_team="$(__triage__normalize_linear_team "$manifest")"

  jq_set '{}' \
    '.name'        "$repo_name" \
    '.description' "$description" \
    '.areas'       "$areas" \
    '.linear_team' "$linear_team"
}

function __triage__normalize_linear_team() {
  local manifest="$1"
  __triage__scalar_field "$manifest" 'linear-team'
}

function load_base_labels() {
  local base_file linear_blizz base_json linear_json merged

  base_file="$SHELLOPS_DIR/schema/labels.base.json"
  linear_blizz="$SHELLOPS_DIR/schema/labels.linear-blizz.json"

  if [[ ! -f "$base_file" ]]; then
    error "Base label file not found: $base_file"
    return 1
  fi
  if [[ ! -f "$linear_blizz" ]]; then
    error "Linear Blizz label file not found: $linear_blizz"
    return 1
  fi

  base_json=$(<"$base_file")
  linear_json=$(<"$linear_blizz")
  if ! jq_is_valid "$base_json"; then
    error "Base label file is not valid JSON: $base_file"
    return 1
  fi
  if ! jq_is_valid "$linear_json"; then
    error "Linear Blizz label file is not valid JSON: $linear_blizz"
    return 1
  fi

  merged="$(jq_merge "$base_json" "$linear_json")" || return 1
  jq_compact "$merged"
}

function collect_issues() {
  local linear_options="$1" team_map="$2"
  local team_ids issues count

  team_ids="$(__triage__extract_team_ids "$team_map")"
  if [[ "$(jq_len "$team_ids")" -eq 0 ]]; then
    log "No teams to triage (no repos opted in)."
    echo "[]"
    return 0
  fi

  log "Collecting untriaged issues for $(jq_len "$team_ids") team(s)..."
  issues="$(linear_list_untriaged_issues "$linear_options" "$team_ids")"
  count="$(jq_len "$issues")"
  log "Found $count untriaged issue(s)."

  echo "$issues"
}

function __triage__extract_team_ids() {
  local team_map="$1"
  jq -c '[.[].team_id] | unique' <<< "$team_map"
}

function ensure_linear_labels() {
  local linear_options="$1" repos="$2" base_labels="$3"
  local existing needed label_map

  log "Ensuring operational labels exist in Linear..."
  existing="$(linear_list_labels "$linear_options")"
  needed="$(build_needed_label_names "$repos" "$base_labels")"

  label_map="$(jq_reduce __triage__reduce_ensure_label "$needed" '{}' \
    "$linear_options" "$existing" "$base_labels")"

  echo "$label_map"
}

function __triage__reduce_ensure_label() {
  local acc="$1" name_json="$2" linear_options="$3" existing="$4" base_labels="$5"
  local name

  name="$(jq_unwrap "$name_json")"
  ensure_single_label "$linear_options" "$existing" "$name" "$base_labels" "$acc"
}

function build_needed_label_names() {
  local repos="$1" base_labels="$2"
  local repo_names blizz_items blizz_names

  repo_names="$(jq_pluck "$repos" '.name')"
  blizz_items="$(jq_compact "$(jq_dump "$base_labels" '.blizz.items')")"
  blizz_names="$(__triage__object_keys_prefixed "$blizz_items" 'blizz/')"

  jq_concat "$repo_names" "$blizz_names"
}

function ensure_single_label() {
  local linear_options="$1" existing="$2" name="$3" base_labels="$4" label_map="$5"
  local existing_id color created new_id

  existing_id="$(__triage__find_label_id "$existing" "$name")"

  if jq_defined "$existing_id"; then
    jq_set "$label_map" ".\"$name\"" "$existing_id"
    return
  fi

  color="$(__triage__label_color_for "$name" "$base_labels")"
  created="$(linear_create_label "$name" "$color" "$linear_options")" || {
    warn "Failed to create label: $name"
    echo "$label_map"
    return
  }

  new_id="$(jq_get "$created" '.id')"
  log "Created label: $name"
  jq_set "$label_map" ".\"$name\"" "$new_id"
}

function __triage__linear_label_name_match() {
  local label="$1" target_name="$2"
  [[ "$(jq_get "$label" '.name')" == "$target_name" ]]
}

function __triage__find_label_id() {
  local labels="$1" name="$2"
  local match

  match="$(jq_find __triage__linear_label_name_match "$labels" "$name")"
  if ! jq_defined "$match"; then
    echo ""
    return
  fi
  jq_get "$match" '.id'
}

function __triage__label_color_for() {
  local name="$1" base_labels="$2"
  if [[ "$name" == blizz/* ]]; then
    jq_get "$base_labels" '.blizz.color' '#6644aa'
  else
    echo "$__triage__REPO_LABEL_COLOR"
  fi
}

function build_repo_team_map() {
  local repos="$1" teams="$2"
  jq_reduce __triage__reduce_team_map "$repos" '{}' "$teams"
}

function __triage__reduce_team_map() {
  local acc="$1" repo="$2" teams="$3"
  local repo_name team_key entry

  repo_name="$(jq_get "$repo" '.name')"
  team_key="$(jq_get "$repo" '.linear_team' '')"
  if [[ -z "$team_key" ]]; then
    echo "$acc"
    return 0
  fi

  entry="$(resolve_team_entry "$teams" "$repo_name" "$team_key")" || {
    echo "$acc"
    return 0
  }

  jq_set "$acc" ".\"$repo_name\"" "$entry"
}

function resolve_team_entry() {
  local teams="$1" repo_name="$2" team_key="$3"
  local team_info team_id backlog_state_id

  team_info="$(jq_find __triage__match_team_key "$teams" "$team_key")"
  if ! jq_defined "$team_info"; then
    warn "No Linear team found for key '$team_key' (repo: $repo_name)"
    return 1
  fi

  team_id="$(jq_get "$team_info" '.id')"
  backlog_state_id="$(__triage__find_backlog_state "$team_info")"

  jq_set '{}' \
    '.team_id'          "$team_id" \
    '.team_key'         "$team_key" \
    '.backlog_state_id' "$backlog_state_id"
}

function __triage__match_team_key() {
  local team="$1" target_key="$2"
  [[ "$(jq_get "$team" '.key')" == "$target_key" ]]
}

function __triage__find_backlog_state() {
  local team="$1"
  local states match
  states="$(jq_get "$team" '.states.nodes')"
  match="$(jq_find __triage__match_backlog_type "$states")"
  jq_get "$match" '.id' ''
}

function __triage__match_backlog_type() {
  local state="$1"
  [[ "$(jq_get "$state" '.type')" == "backlog" ]]
}

function process_issues() {
  local issues="$1" linear_options="$2" ctx="$3"
  local count to_process batch prompt batch_result

  count="$(jq_len "$issues")"
  if (( count == 0 )); then
    log "No untriaged issues to process."
    return 0
  fi

  to_process=$(( count < ARG_LIMIT ? count : ARG_LIMIT ))
  log "Processing $to_process of $count untriaged issue(s)..."

  batch="$(jq_take "$issues" "$to_process")"
  prompt="$(render_batch_prompt "$batch" "$ctx")"

  batch_result="$(invoke_agent "$prompt")" || {
    warn "Batch agent failed; issues will be retried next run."
    return 0
  }

  batch_result="$(recover_missing_results "$batch" "$batch_result")"

  trap - INT TERM
  __triage_dispose_active_agent

  apply_batch_results "$batch" "$batch_result" "$linear_options" "$ctx"

  log "Triage complete: $to_process issue(s) processed."
}

function __triage__find_missing_ids() {
  local batch="$1" batch_result="$2"
  jq -r --argjson result "$batch_result" \
    '[.[].identifier] - [$result | keys[]] | .[]' <<< "$batch"
}

function recover_missing_results() {
  local batch="$1" batch_result="$2"
  local missing_ids missing_count followup_text followup_raw followup_result

  missing_ids="$(__triage__find_missing_ids "$batch" "$batch_result")"
  if [[ -z "$missing_ids" ]]; then
    echo "$batch_result"
    return 0
  fi

  missing_count="$(echo "$missing_ids" | wc -l)"
  log "Batch result missing $missing_count issue(s); sending followup..."

  followup_text="$(printf 'You missed %d issue(s). Triage these identifiers: %s' \
    "$missing_count" "$(echo "$missing_ids" | paste -sd', ')")"

  followup_raw="$(cursor_followup_agent \
    "$__triage_active_cursor_options" \
    "$__triage_active_agent_id" \
    "$followup_text")" || {
    warn "Followup failed; missing issues will be retried next run."
    echo "$batch_result"
    return 0
  }

  followup_result="$(parse_batch_response "$followup_raw")" || {
    warn "Followup response invalid; missing issues will be retried next run."
    echo "$batch_result"
    return 0
  }

  jq -c '. * $f' --argjson f "$followup_result" <<< "$batch_result"
}

function apply_batch_results() {
  local batch="$1" batch_result="$2" linear_options="$3" ctx="$4"

  jq_foreach __triage__apply_single "$batch" "$batch_result" "$linear_options" "$ctx"
}

function __triage__apply_single() {
  local issue="$1" batch_result="$2" linear_options="$3" ctx="$4"
  local identifier agent_result

  identifier="$(jq_get "$issue" '.identifier')"
  agent_result="$(jq_get "$batch_result" ".\"$identifier\"" 'null')"

  if [[ "$agent_result" == "null" ]]; then
    warn "No triage result for $identifier; will retry next run."
    return 0
  fi

  if ! apply_result "$issue" "$agent_result" "$linear_options" "$ctx"; then
    warn "Failed to apply result for $identifier"
  fi
}

function render_system_prompt() {
  local repos="$1" base_labels="$2"
  local org forgejo_base rendered

  org="$(manifest_get org)"
  forgejo_base="$(forgejo_api_base)"

  if [[ ! -f "$__triage__SYSTEM_MD" ]]; then
    error "System prompt template not found: $__triage__SYSTEM_MD"
    return 1
  fi

  rendered="$(cat "$__triage__SYSTEM_MD")"
  rendered="$(string_replace_json "$rendered" '{{repos}}' "$repos")"
  rendered="$(string_replace_json "$rendered" '{{base_labels}}' "$base_labels")"
  rendered="$(string_replace "$rendered" '{{forgejo_base}}' "$forgejo_base")"
  rendered="$(string_replace "$rendered" '{{org}}' "$org")"
  printf '%s\n' "$rendered"
}

function __triage_dispose_active_agent() {
  [[ -z "${__triage_active_agent_id:-}" ]] && return 0

  cursor_cleanup_agent \
    "$__triage_active_cursor_options" \
    "$__triage_active_agent_id" \
    2>/dev/null || true

  __triage_active_agent_id=""
}

function __triage_on_agent_signal() {
  local kind="$1"
  local had="${__triage_active_agent_id:-}"

  trap - INT TERM
  if [[ -n "$had" ]]; then
    warn "Interrupted; cancelling Cursor agent ${had}..."
  fi

  __triage_dispose_active_agent
  if [[ "$kind" == INT ]]; then
    exit 130
  fi

  exit 143
}

function invoke_agent() {
  local prompt="$1"
  local cursor_options agent_id status raw_result

  cursor_options="$(jq_set "$(new_cursor_options)" \
    '.api_key'    "$CURSOR_AGENT_API_KEY" \
    '.repository' "$__triage__BOOTSTRAP_REPO" \
    '.model'      "${CURSOR_AGENT_MODEL:-composer-2}")"

  agent_id="$(cursor_launch_agent "$cursor_options" "$prompt")" || return 1
  log "Agent launched: $agent_id"

  __triage_active_cursor_options="$cursor_options"
  __triage_active_agent_id="$agent_id"
  trap '__triage_on_agent_signal INT' INT
  trap '__triage_on_agent_signal TERM' TERM

  status="$(cursor_poll_agent "$cursor_options" "$agent_id")" || {
    trap - INT TERM
    __triage_dispose_active_agent
    return 1
  }

  if [[ "$status" != "FINISHED" ]]; then
    error "Agent $agent_id ended with status: $status"
    trap - INT TERM
    __triage_dispose_active_agent
    return 1
  fi

  raw_result="$(cursor_get_result "$cursor_options" "$agent_id")" || {
    trap - INT TERM
    __triage_dispose_active_agent
    return 1
  }

  parse_batch_response "$raw_result"
}

function parse_batch_response() {
  local raw="$1"
  local payload

  payload="$(jq_from_codeblock "$raw")" || {
    error "Agent response is not valid JSON (after optional markdown fence strip)"
    return 1
  }

  if ! jq_validate "$payload" "$__triage__BATCH_SCHEMA_JSON"; then
    error "Agent response failed batch schema validation"
    return 1
  fi

  jq_compact "$payload"
}

function apply_result() {
  local issue="$1" agent_result="$2" linear_options="$3" ctx="$4"
  local issue_id identifier target_repo blizz_label label_ids comment migration_plan

  issue_id="$(jq_get "$issue" '.id')"
  identifier="$(jq_get "$issue" '.identifier')"
  target_repo="$(jq_get "$agent_result" '.target_repo')"
  blizz_label="$(triage_linear_blizz_label "$agent_result")"

  label_ids="$(resolve_label_ids "$ctx" "$target_repo" "$blizz_label")"
  linear_add_labels "$issue_id" "$label_ids" "$linear_options" || \
    warn "Failed to add labels to $identifier"

  attach_triage_result "$linear_options" "$issue_id" "$agent_result" "$target_repo" "$blizz_label" || \
    warn "Failed to attach triage result to $identifier"

  comment="$(build_assessment_comment "$agent_result" "$blizz_label")"
  linear_add_comment "$issue_id" "$comment" "$linear_options" || \
    warn "Failed to add comment to $identifier"

  if migration_plan="$(triage_migration_plan "$issue" "$ctx" "$target_repo")"; then
    triage_migrate_issue_team "$issue" "$linear_options" "$migration_plan"
  fi

  log "Triaged $identifier -> $target_repo ($blizz_label)"
}

function triage_linear_blizz_label() {
  local agent_result="$1"
  local disp

  disp="$(jq_get "$agent_result" '.blizz_disposition')"
  echo "blizz/${disp}"
}

function __triage__suggested_labels_array_json() {
  local agent_result="$1"
  jq_compact "$(jq_dump "$agent_result" '.suggested_labels // []')"
}

function resolve_label_ids() {
  local ctx="$1" target_repo="$2" blizz_label="$3"
  local label_map repo_id blizz_id ids

  label_map="$(jq_get "$ctx" '.label_map')"
  repo_id="$(jq_get "$label_map" ".\"$target_repo\"" '')"
  blizz_id="$(jq_get "$label_map" ".\"$blizz_label\"" '')"

  ids="[]"
  [[ -n "$repo_id" ]] && ids="$(jq_push "$ids" "\"$repo_id\"")"
  [[ -n "$blizz_id" ]] && ids="$(jq_push "$ids" "\"$blizz_id\"")"

  echo "$ids"
}

function attach_triage_result() {
  local linear_options="$1" issue_id="$2" agent_result="$3" target_repo="$4" blizz_label="$5"
  local org forgejo_base repo_url attachment

  org="$(manifest_get org)"
  forgejo_base="$(forgejo_api_base)"
  repo_url="${forgejo_base}/${org}/${target_repo}"

  attachment="$(jq_set "$(new_attachment)" \
    '.title'    "$target_repo" \
    '.url'      "$repo_url" \
    '.subtitle' "$(build_attachment_subtitle "$agent_result" "$blizz_label")" \
    '.metadata' "$(build_triage_metadata "$agent_result" "$blizz_label")")"

  linear_add_attachment "$issue_id" "$attachment" "$linear_options"
}

function build_attachment_subtitle() {
  local agent_result="$1" blizz_label="$2"
  local confidence
  confidence="$(jq_get "$agent_result" '.agentic_confidence')"
  echo "${blizz_label} · confidence ${confidence}/100"
}

function build_triage_metadata() {
  local agent_result="$1" blizz_label="$2"
  local suggested_labels summary confidence rationale

  suggested_labels="$(__triage__suggested_labels_array_json "$agent_result")"
  summary="$(jq_get "$agent_result" '.changelog_summary')"
  confidence="$(jq_get "$agent_result" '.agentic_confidence')"
  rationale="$(jq_get "$agent_result" '.agentic_rationale')"

  jq_set '{}' \
    '.blizz_label'      "$blizz_label" \
    '.suggested_labels' "$suggested_labels" \
    '.changelog_summary' "$summary" \
    '.agentic_confidence' "$confidence" \
    '.agentic_rationale'  "$rationale" \
    '.blizz_disposition_rationale' "$(jq_get "$agent_result" '.blizz_disposition_rationale')"
}

function build_assessment_comment() {
  local agent_result="$1" blizz_label="$2"
  local target_repo summary confidence rationale disp_rationale labels_md suggested

  target_repo="$(jq_get "$agent_result" '.target_repo')"
  summary="$(jq_get "$agent_result" '.changelog_summary')"
  confidence="$(jq_get "$agent_result" '.agentic_confidence')"
  rationale="$(jq_get "$agent_result" '.agentic_rationale')"
  disp_rationale="$(jq_get "$agent_result" '.blizz_disposition_rationale')"
  suggested="$(__triage__suggested_labels_array_json "$agent_result")"
  labels_md="$(jq_join "$suggested" $'\n' "$__triage__COMMENT_LABEL_BULLET_TEMPLATE")"

  cat <<EOF
## Blizz Triage Assessment

**Status:** \`$blizz_label\`
**Target repo:** $target_repo
**Changelog:** $summary

### Disposition
$disp_rationale

### Suggested Labels
$labels_md

### Agentic Fitness: ${confidence}/100
$rationale

### Triage Output

\`\`\`
$(jq_dump "$agent_result")
\`\`\`
EOF
}

function triage_migration_plan() {
  local issue="$1" ctx="$2" target_repo="$3"
  local team_map target_info current_key target_key

  team_map="$(jq_get "$ctx" '.team_map')"
  target_info="$(jq_get "$team_map" ".\"$target_repo\"")"
  if ! jq_defined "$target_info"; then
    return 1
  fi

  current_key="$(jq_get "$issue" '.team.key')"
  target_key="$(jq_get "$target_info" '.team_key')"
  if [[ "$current_key" == "$target_key" ]]; then
    return 1
  fi

  jq_compact "$target_info"
}

function triage_migrate_issue_team() {
  local issue="$1" linear_options="$2" target_info="$3"
  local issue_id identifier current_key target_key target_team_id target_state_id

  issue_id="$(jq_get "$issue" '.id')"
  identifier="$(jq_get "$issue" '.identifier')"
  current_key="$(jq_get "$issue" '.team.key')"
  target_key="$(jq_get "$target_info" '.team_key')"
  target_team_id="$(jq_get "$target_info" '.team_id')"
  target_state_id="$(jq_get "$target_info" '.backlog_state_id')"

  log "Migrating $identifier from team $current_key to $target_key"
  linear_move_issue_to_team "$issue_id" "$target_team_id" "$target_state_id" "$linear_options" || \
    warn "Failed to migrate $identifier to team $target_key"
}

function parse_args() {
  while [[ $# -gt 0 ]]; do
    case "$1" in
      -h|--help)
        log "$USAGE"
        exit 0
        ;;
      --dry-run)
        ARG_DRY_RUN=true
        ;;
      --limit)
        if [[ -z "${2:-}" ]]; then
          error "Missing value for $1"; exit 1
        fi
        ARG_LIMIT="$2"
        shift
        ;;
      -*)
        log "Unknown option: $1"
        log "$USAGE"
        exit 1
        ;;
      *)
        log "Unexpected argument: $1"
        log "$USAGE"
        exit 1
        ;;
    esac
    shift
  done
}

if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
  main "$@"
fi
