#!/usr/bin/env bash
# nerf-az-pipelines-run-timeline -- Fetch the stage/job/task tree for a pipeline run, highlighting failures. Failed tasks include their log ID for use with az-pipelines-run-log. Pass --json for raw timeline output instead of the formatted tree.
# Generated from az-pipelines manifest. Do not edit directly.
# nerf:threat:read=remote
# nerf:threat:write=none

if [[ "${BASH_VERSINFO[0]:-0}" -lt 4 ]]; then
  echo "error: nerf-az-pipelines-run-timeline requires bash 4+. Found bash ${BASH_VERSION:-unknown}" >&2
  echo "  hint: on macOS, install a newer bash via 'brew install bash'" >&2
  exit 1
fi

set -euo pipefail

_NERF_DRY_RUN=""

usage() {
  cat >&2 <<'EOF'
Usage: nerf-az-pipelines-run-timeline [--json] [--project|-p <project>] <run_id>

Switches:
  --json
      Output raw timeline JSON instead of the formatted tree

Options:
  --project, -p <project>
      Azure DevOps project name or ID (auto-detected from the run if omitted)

Arguments:
  <run_id> (required)
      Pipeline run ID (numeric, from az-pipelines-runs or az-pipelines-check)
      Must match: ^[0-9]+$

Fetch the stage/job/task tree for a pipeline run, highlighting failures. Failed tasks include their log ID for use with az-pipelines-run-log. Pass --json for raw timeline output instead of the formatted tree.
EOF
  exit 1
}

JSON=""
PROJECT=""
_PROJECT_SET=""

while [[ $# -gt 0 ]]; do
  case "$1" in
    --json) if [[ -n "${JSON}" ]]; then echo "error: --json can only be specified once" >&2; exit 1; fi; JSON="true"; shift 1 ;;
    --project|-p) if [[ -n "${_PROJECT_SET}" ]]; then echo "error: --project can only be specified once" >&2; exit 1; fi; PROJECT="$2"; _PROJECT_SET=true; shift 2 ;;
    --nerf-dry-run) _NERF_DRY_RUN="true"; shift 1 ;;
    -h|--help) usage ;;
    --) shift; break ;;
    *) break ;;
  esac
done

_RUN_ID_SET=""
if [[ $# -gt 0 ]]; then
  RUN_ID="$1"
  _RUN_ID_SET=true
  shift
else
  RUN_ID=""
fi
if [[ $# -gt 0 ]]; then
  echo "error: nerf-az-pipelines-run-timeline: unexpected extra arguments: $*" >&2
  echo "  hint: switches and options must come before positional arguments" >&2
  exit 1
fi

if [[ -n "${_RUN_ID_SET}" ]] && [[ "${RUN_ID}" == -* ]]; then
  echo "error: nerf-az-pipelines-run-timeline: <run_id> cannot start with '-'" >&2
  echo "  hint: use -- before positional arguments if needed" >&2
  exit 1
fi

if [[ -z "${RUN_ID}" ]]; then
  echo "error: nerf-az-pipelines-run-timeline: missing required argument <run_id>" >&2
  echo "  hint: provide a value for <run_id>" >&2
  usage
fi

_NERF_PATTERN='^[0-9]+$'
if [[ -n "${_RUN_ID_SET}" ]] && ! [[ "${RUN_ID}" =~ $_NERF_PATTERN ]]; then
  echo "error: nerf-az-pipelines-run-timeline: argument <run_id> does not match required pattern" >&2
  echo "  value:   \"${RUN_ID}\"" >&2
  echo "  pattern: ^[0-9]+$" >&2
  echo "  hint: value must match ^[0-9]+$" >&2
  exit 1
fi

which jq > /dev/null 2>&1 || { echo 'error: nerf-az-pipelines-run-timeline: jq is required but not installed (e.g. apt-get install jq, brew install jq).' >&2; exit 1; }
which python3 > /dev/null 2>&1 || { echo 'error: nerf-az-pipelines-run-timeline: python3 is required but not installed.' >&2; exit 1; }

if [[ "$_NERF_DRY_RUN" == "true" ]]; then
  echo "dry-run: nerf-az-pipelines-run-timeline would run inline script"
  exit 0
fi

if [[ -n "${PROJECT}" ]]; then
  PROJECT_NAME="${PROJECT}"
else
  PROJECT_NAME=$(az pipelines runs show --id "${RUN_ID}" --output json | jq -r '.project.name')
  if [[ -z "${PROJECT_NAME}" || "${PROJECT_NAME}" == "null" ]]; then
    echo "error: az-pipelines-run-timeline: could not resolve project for run ${RUN_ID}" >&2
    exit 1
  fi
fi
TIMELINE=$(az devops invoke \
  --area build --resource timeline \
  --route-parameters "project=${PROJECT_NAME}" "buildId=${RUN_ID}" \
  --output json) || {
  echo "error: az-pipelines-run-timeline: failed to fetch timeline for run ${RUN_ID}" >&2
  exit 1
}
if [[ -n "${JSON}" ]]; then
  echo "${TIMELINE}"
  exit 0
fi
python3 - <(printf '%s' "${TIMELINE}") <<'PYEOF'
import json, sys
with open(sys.argv[1]) as f:
    data = json.load(f)
records = data.get("records", [])
stages = sorted([r for r in records if r.get("type") == "Stage"], key=lambda r: r.get("order", 0))
jobs = [r for r in records if r.get("type") in ("Job", "Phase")]
tasks = [r for r in records if r.get("type") == "Task"]
by_parent = {}
for r in jobs + tasks:
    by_parent.setdefault(r.get("parentId", ""), []).append(r)

def icon(result):
    return {"failed": "FAILED", "succeeded": "ok", "skipped": "skipped",
            "canceled": "canceled"}.get(result, result or "?")

for s in stages:
    print(f"[{icon(s.get('result'))}] Stage: {s['name']}")
    for j in by_parent.get(s["id"], []):
        print(f"  [{icon(j.get('result'))}] Job: {j['name']}")
        for t in by_parent.get(j["id"], []):
            log_id = (t.get("log") or {}).get("id", "")
            hint = f" (log:{log_id})" if log_id and t.get("result") == "failed" else ""
            print(f"    [{icon(t.get('result'))}] {t['name']}{hint}")

failed = [t for t in tasks if t.get("result") == "failed"]
if failed:
    print()
    print("=== Failed Tasks ===")
    for t in failed:
        print(f"  {t['name']} -- log ID: {(t.get('log') or {}).get('id', '')}")
PYEOF
