#!/usr/bin/env bash
set -euo pipefail

USAGE="$(cat <<EOF
Finds v* tags on a GitHub repository that have no corresponding GitHub Release,
then dispatches a release workflow for each one.

Tags with >= N total failed attempts (across runs and retries) are skipped (default: 3).
When a failed run exists for a tag, it is rerun rather than dispatching fresh.

Usage: github/release-catchup [-h,--help] [-w,--workflow FILE] [-d,--dry-run] [-f,--max-failures N] [-n,--no-wait] <owner> <repo>

Arguments:
  owner           GitHub org or user (e.g. kernelle-soft).
  repo            GitHub repository name (e.g. kaitos-toolchain).

Flags:
  -w, --workflow FILE    Workflow to dispatch (default: release.yaml).
  -d, --dry-run          List actionable tags without dispatching.
  -f, --max-failures N   Skip tags with >= N failed attempts (default: 3).
  -n, --no-wait          Dispatch without waiting for completion.
  -h, --help             Show this help text.

Environment:
  GITHUB_TOKEN    Required. API token with actions:write scope.

EOF
)"

import "$SHELLOPS_DIR/lib/github.api.sh"

RUN_POLL_ATTEMPTS=10
RUN_POLL_INTERVAL=5
DISPATCH_DELAY=5

ARG_OWNER=""
ARG_REPO=""
FLAG_WORKFLOW="release.yaml"
FLAG_DRY_RUN=false
FLAG_MAX_FAILURES=3
FLAG_NO_WAIT=false

SUMMARY_LINES=""

main() {
  parse_args "$@"
  require_token

  local repo_opts missing
  repo_opts="$(jq_set "$(new_github_repo_options)" \
    '.owner' "$ARG_OWNER" \
    '.repo'  "$ARG_REPO" \
    '.token' "$GITHUB_TOKEN"
  )"

  missing="$(find_missing_releases "$repo_opts")"

  if [[ -z "$missing" ]]; then
    log "All tags have releases."
    exit 0
  fi

  log "Tags missing releases:"
  echo "$missing" >&2

  if [[ "$FLAG_DRY_RUN" = true ]]; then
    exit 0
  fi

  dispatch_eligible "$missing"
  print_summary
}

parse_args() {
  local positional=()

  while [[ $# -gt 0 ]]; do
    case "$1" in
      -w|--workflow)
        FLAG_WORKFLOW="${2:?--workflow requires a value}"
        shift
        ;;
      -d|--dry-run)
        FLAG_DRY_RUN=true
        ;;
      -f|--max-failures)
        FLAG_MAX_FAILURES="${2:?--max-failures requires a value}"
        shift
        ;;
      -n|--no-wait)
        FLAG_NO_WAIT=true
        ;;
      -h|--help)
        log "$USAGE"
        exit 0
        ;;
      -*)
        error "Unknown option: $1"
        log "$USAGE"
        exit 1
        ;;
      *)
        positional+=("$1")
        ;;
    esac
    shift
  done

  ARG_OWNER="${positional[0]:-}"
  ARG_REPO="${positional[1]:-}"

  if [[ -z "$ARG_OWNER" || -z "$ARG_REPO" ]]; then
    error "<owner> and <repo> are required."
    log "$USAGE"
    exit 1
  fi
}

require_token() {
  if [[ -z "${GITHUB_TOKEN:-}" ]]; then
    error "GITHUB_TOKEN is not set."
    exit 1
  fi
}

find_missing_releases() {
  local repo_opts="$1"
  local all_tags released_tags

  all_tags="$(jq_split \
    "$(github_list_tags "$repo_opts")"
  )"

  released_tags="$(jq_split \
    "$(github_list_release_tags "$repo_opts")"
  )"

  jq_join "$(jq_difference "$all_tags" "$released_tags")" "\n"
}

get_runs_for_tag() {
  local dispatch_opts="$1"
  local runs_json

  runs_json="$(github_list_workflow_runs "$dispatch_opts")"
  jq_get "$runs_json" '.workflow_runs'
}

count_total_failures() {
  local runs="$1"
  local total_attempts successful_runs

  total_attempts="$(jq_sum "$(jq_pluck "$runs" '.run_attempt')")"
  successful_runs="$(jq_len "$(jq_filter "$runs" '.conclusion == "success"')")"

  echo $(( total_attempts - successful_runs ))
}

most_recent_failed_run_id() {
  local runs="$1"
  local failed

  failed="$(jq_filter "$runs" '.conclusion == "failure" or .conclusion == "cancelled"')"
  jq_get "$failed" '.[0].id // empty'
}

dispatch_eligible() {
  local missing="$1"

  for tag in $missing; do
    process_tag "$tag"
    sleep "$DISPATCH_DELAY"
  done
}

process_tag() {
  local tag="$1"

  local dispatch_opts
  dispatch_opts="$(jq_set "$(new_github_dispatch_options)" \
    '.owner'    "$ARG_OWNER" \
    '.repo'     "$ARG_REPO" \
    '.workflow' "$FLAG_WORKFLOW" \
    '.ref'      "$tag" \
    '.token'    "$GITHUB_TOKEN"
  )"

  local runs
  runs="$(get_runs_for_tag "$dispatch_opts")"

  if has_active_runs "$runs"; then
    log "Skipping ${tag} (run already in progress)"
    add_summary "$tag" "skipped (in progress)" ""
    return 0
  fi

  local failures
  failures="$(count_total_failures "$runs")"

  if [[ "$failures" -ge "$FLAG_MAX_FAILURES" ]]; then
    log "Skipping ${tag} (${failures} total failed attempts, threshold: ${FLAG_MAX_FAILURES})"
    add_summary "$tag" "skipped (${failures} failures)" ""
    return 0
  fi

  local failed_run_id
  failed_run_id="$(most_recent_failed_run_id "$runs")"

  if [[ -n "$failed_run_id" && "$failed_run_id" != "null" ]]; then
    rerun_tag "$tag" "$failed_run_id"
  else
    dispatch_tag "$tag" "$dispatch_opts"
  fi
}

has_active_runs() {
  local runs="$1"
  local active
  active="$(jq_len "$(jq_filter "$runs" '.status != "completed"')")"
  [[ "$active" -gt 0 ]]
}

dispatch_tag() {
  local tag="$1" dispatch_opts="$2"

  log "Dispatching ${FLAG_WORKFLOW} for ${tag}..."
  github_dispatch_workflow "$dispatch_opts"

  if [[ "$FLAG_NO_WAIT" = true ]]; then
    add_summary "$tag" "dispatched" ""
    return 0
  fi

  wait_and_record "$dispatch_opts" "$tag"
}

rerun_tag() {
  local tag="$1" run_id="$2"

  local run_opts
  run_opts="$(jq_set "$(new_github_run_options)" \
    '.owner'  "$ARG_OWNER" \
    '.repo'   "$ARG_REPO" \
    '.run_id' "$run_id" \
    '.token'  "$GITHUB_TOKEN"
  )"

  log "Rerunning existing failed run ${run_id} for ${tag}..."
  github_rerun_workflow "$run_opts" || {
    error "Failed to rerun ${run_id} for ${tag}."
    add_summary "$tag" "rerun failed" ""
    return 0
  }

  if [[ "$FLAG_NO_WAIT" = true ]]; then
    add_summary "$tag" "rerun triggered" ""
    return 0
  fi

  local run_url conclusion
  run_url="$(jq_get "$(github_get_run "$run_opts")" '.html_url')"
  log "Run for ${tag}: ${run_url}"

  conclusion="$(await_run "$run_opts" "$tag")"
  record_result "$tag" "$conclusion" "$run_url"
}

wait_and_record() {
  local dispatch_opts="$1" tag="$2"

  local run_json
  run_json="$(github_wait_for_run_start "$dispatch_opts" "$RUN_POLL_ATTEMPTS" "$RUN_POLL_INTERVAL")" || {
    log "Could not locate run for ${tag}. Check GitHub Actions manually."
    add_summary "$tag" "dispatched" "run not found"
    return 0
  }

  local run_url run_id
  run_url="$(jq_get "$run_json" '.html_url')"
  run_id="$(jq_get "$run_json" '.id')"
  log "Run for ${tag}: ${run_url}"

  local run_opts conclusion
  run_opts="$(jq_set "$(new_github_run_options)" \
    '.owner'  "$ARG_OWNER" \
    '.repo'   "$ARG_REPO" \
    '.run_id' "$run_id" \
    '.token'  "$GITHUB_TOKEN"
  )"

  conclusion="$(await_run "$run_opts" "$tag")"
  record_result "$tag" "$conclusion" "$run_url"
}

await_run() {
  local run_opts="$1" tag="$2"

  local final_json
  final_json="$(github_wait_for_run_result "$run_opts")" || {
    log "Timed out waiting for ${tag}. Cancelling..."
    if github_cancel_run "$run_opts"; then
      log "Run cancelled."
    else
      error "Failed to cancel run."
    fi
    echo "cancelled (timeout)"
    return 0
  }

  jq_get "$final_json" '.conclusion'
}

record_result() {
  local tag="$1" conclusion="$2" run_url="$3"

  log "${tag}: ${conclusion}"
  add_summary "$tag" "$conclusion" "$run_url"
}

add_summary() {
  local tag="$1" status="$2" url="$3"

  if [[ -n "$url" ]]; then
    SUMMARY_LINES="${SUMMARY_LINES}  - ${tag}: ${status}, url: ${url}\n"
  else
    SUMMARY_LINES="${SUMMARY_LINES}  - ${tag}: ${status}\n"
  fi
}

print_summary() {
  if [[ -z "$SUMMARY_LINES" ]]; then
    return 0
  fi

  log ""
  log "=== Release Catchup Summary ==="
  echo -e "$SUMMARY_LINES" >&2

  if [[ -n "${GITHUB_STEP_SUMMARY:-}" ]]; then
    echo -e "## Release Catchup\n\n${SUMMARY_LINES}" >> "$GITHUB_STEP_SUMMARY"
  fi
}

main "$@"
