#!/usr/bin/env bash
#
# Get latest release version, branch tag, or latest commit from GitHub
# Usage: x-gh-get-latest-version <repo> [options]
# Author: Ismo Vuorinen <https://github.com/ivuorinen> 2024

set -euo pipefail

# Environment variables, can be overridden by command line arguments
GITHUB_API_URL="${GITHUB_API_URL:-https://api.github.com/repos}"
VERBOSE="${VERBOSE:-0}"
INCLUDE_PRERELEASES="${INCLUDE_PRERELEASES:-0}"
OLDEST_RELEASE="${OLDEST_RELEASE:-0}"
BRANCH=""
LATEST_COMMIT="${LATEST_COMMIT:-0}"
LATEST_TAG="${LATEST_TAG:-0}"
OUTPUT="${OUTPUT:-text}"
SHOW_HELP=0
REPOSITORY=""
COMBINED=0

BIN=$(basename "$0")

# Prints a message if VERBOSE=1
msg()
{
  if [[ $VERBOSE -eq 1 ]]; then
    echo "$1" >&2
  fi
}

# Show usage information
usage()
{
  cat << EOF
Usage: $BIN <repo> [options]

Fetches the latest release version, latest branch tag, or latest commit SHA from GitHub.

Arguments:
  <repo>                   Repository in format 'owner/repo' (e.g. ivuorinen/dotfiles)

Options:
  -h, --help               Show this help message and exit
  -v, --verbose            Enable verbose output
  -p, --prereleases        Include prerelease versions (default: only stable releases)
  -o, --oldest             Fetch the oldest release instead of the latest
  -b, --branch <branch>    Fetch the latest tag from a specific branch (default: main)
  -c, --commit             Fetch the latest commit SHA from the specified branch
  -t, --tag                Fetch the latest Git tag (any branch)
  -j, --json               Return output as JSON (default: plain text)
  -a, --all                Fetch all information types in a combined output

Environment Variables (can be used instead of command line options):
  - INCLUDE_PRERELEASES=1  Same as --prereleases
  - OLDEST_RELEASE=1       Same as --oldest
  - BRANCH=<branch>        Same as --branch <branch>
  - LATEST_COMMIT=1        Same as --commit
  - LATEST_TAG=1           Same as --tag
  - OUTPUT=json            Same as --json
  - GITHUB_API_URL=<url>   Override GitHub API URL (useful for GitHub Enterprise)
  - GITHUB_TOKEN=<token>   Use GitHub API token to increase rate limits (default: unauthenticated)
  - VERBOSE=1              Same as --verbose

Requirements:
  - curl
  - jq (for JSON processing)

Examples:
  # Fetch the latest stable release
  $BIN ivuorinen/dotfiles

  # Fetch the latest release including prereleases
  $BIN ivuorinen/dotfiles --prereleases

  # Fetch the oldest release
  $BIN ivuorinen/dotfiles --oldest

  # Fetch the latest tag from the 'develop' branch
  $BIN ivuorinen/dotfiles --branch develop

  # Fetch the latest commit SHA from 'main' branch
  $BIN ivuorinen/dotfiles --commit

  # Fetch the latest Git tag (any branch)
  $BIN ivuorinen/dotfiles --tag

  # Fetch all information types in a combined output
  $BIN ivuorinen/dotfiles --all

  # Output result in JSON format
  $BIN ivuorinen/dotfiles --json

  # Use GitHub API token for higher rate limits
  GITHUB_TOKEN="your_personal_access_token" $BIN ivuorinen/dotfiles

  # Use GitHub Enterprise API
  GITHUB_API_URL="https://github.example.com/api/v3/repos" $BIN ivuorinen/dotfiles
EOF
  exit 0
}

# Check that required dependencies are installed
check_dependencies()
{
  for cmd in curl jq; do
    if ! command -v "$cmd" &> /dev/null; then
      echo "Error: '$cmd' is required but not installed." >&2
      exit 1
    fi
  done
}

# Check GitHub API rate limits and warn if they're getting low
check_rate_limits()
{
  local auth_status="unauthenticated"
  local auth_header=()

  if [[ -n ${GITHUB_TOKEN:-} ]]; then
    auth_status="authenticated"
    auth_header=(-H "Authorization: token $GITHUB_TOKEN")
  fi

  msg "Making $auth_status GitHub API requests"

  local rate_limit_info
  rate_limit_info=$(curl -sSL "${auth_header[@]}" "https://api.github.com/rate_limit")

  local remaining
  local reset_timestamp
  local reset_time

  remaining=$(echo "$rate_limit_info" | jq -r '.resources.core.remaining')
  reset_timestamp=$(echo "$rate_limit_info" | jq -r '.resources.core.reset')

  # Handle date command differences between Linux and macOS
  if date --version > /dev/null 2>&1; then
    # GNU date (Linux)
    reset_time=$(date -d "@$reset_timestamp" "+%H:%M:%S %Z" 2> /dev/null)
  else
    # BSD date (macOS)
    reset_time=$(date -r "$reset_timestamp" "+%H:%M:%S %Z" 2> /dev/null)
  fi

  msg "Rate limit status: $remaining requests remaining, reset at $reset_time"

  if [[ $remaining -le 5 ]]; then
    echo "Warning: GitHub API rate limit nearly reached ($remaining requests left)" >&2
    echo "Rate limits will reset at: $reset_time" >&2

    if [[ $auth_status == "unauthenticated" ]]; then
      echo "Tip: Set GITHUB_TOKEN to increase your rate limits (60 → 5000 requests/hour)" >&2
    fi
  fi
}

# Make a GitHub API request with proper error handling
api_request()
{
  local url="$1"
  local auth_header=()

  if [[ -n ${GITHUB_TOKEN:-} ]]; then
    auth_header=(-H "Authorization: token $GITHUB_TOKEN")
  fi

  local response
  local status_code

  # Use a temporary file to capture both headers and body
  local tmp_file
  tmp_file=$(mktemp)

  msg "Making API request to: $url"

  status_code=$(curl -sSL -w "%{http_code}" -o "$tmp_file" "${auth_header[@]}" "$url")
  response=$(< "$tmp_file")
  rm -f "$tmp_file"

  # Check for HTTP errors
  if [[ $status_code -ge 400 ]]; then
    local error_msg
    error_msg=$(echo "$response" | jq -r '.message // "Unknown error"')

    if [[ $status_code -eq 403 && $error_msg == *"API rate limit exceeded"* ]]; then
      # Extract rate limit reset info
      local reset_timestamp
      reset_timestamp=$(echo "$response" | jq -r '.rate.reset // empty')

      local reset_time
      if date --version > /dev/null 2>&1; then
        # GNU date (Linux)
        reset_time=$(date -d "@$reset_timestamp" "+%H:%M:%S %Z" 2> /dev/null \
          || echo "unknown time")
      else
        # BSD date (macOS)
        reset_time=$(date -r "$reset_timestamp" "+%H:%M:%S %Z" 2> /dev/null \
          || echo "unknown time")
      fi

      echo "Error: GitHub API rate limit exceeded" >&2
      echo "Rate limit will reset at: $reset_time" >&2

      if [[ -z ${GITHUB_TOKEN:-} ]]; then
        echo "Tip: Set GITHUB_TOKEN to increase your rate limits (60 → 5000 requests/hour)" >&2
      else
        echo "You've exceeded even authenticated rate limits (5000 requests/hour)" >&2
      fi

      exit 3
    elif [[ $status_code -eq 404 ]]; then
      echo "Error: Repository not found or no access permission: $url" >&2
      exit 2
    else
      echo "GitHub API error ($status_code): $error_msg" >&2
      exit 1
    fi
  fi

  echo "$response"
}

# Check if repository exists before proceeding
check_repository()
{
  local repo="$1"
  local api_url="${GITHUB_API_URL}/${repo}"

  msg "Checking if repository exists: $api_url"

  local response
  response=$(api_request "$api_url")

  # If we got here, the repository exists (otherwise api_request would have exited)
  msg "Repository found: $(echo "$response" | jq -r '.full_name')"

  # Get default branch if no branch is specified
  if [[ -z ${BRANCH} ]]; then
    BRANCH=$(echo "$response" | jq -r '.default_branch')
    msg "Using default branch: $BRANCH"
  fi

  # Return the repository full name (in case it differs from input due to redirects)
  echo "$response" | jq -r '.full_name'
}

# Fetches the latest release or the oldest if OLDEST_RELEASE=1
# $1 - GitHub repository (string)
get_release_version()
{
  local repo="$1"
  local include_prereleases="${INCLUDE_PRERELEASES:-0}"
  local oldest_release="${OLDEST_RELEASE:-0}"
  local api_url="${GITHUB_API_URL}/${repo}/releases"

  msg "Fetching release data from: $api_url " + \
    "(Include prereleases: $include_prereleases, Oldest: $oldest_release)"

  local json_response
  json_response=$(api_request "$api_url")

  local version=""
  local prerelease_version=""

  # Get stable release version
  if [[ $oldest_release -eq 1 ]]; then
    version=$(echo "$json_response" \
      | jq -r '[.[] | select(.tag_name != null and .prerelease == false)] | sort_by(.created_at) | first.tag_name // empty')
  else
    version=$(echo "$json_response" \
      | jq -r '[.[] | select(.tag_name != null and .prerelease == false)] | sort_by(.created_at) | reverse | first.tag_name // empty')
  fi

  # Get prerelease version if requested
  if [[ $include_prereleases -eq 1 ]]; then
    if [[ $oldest_release -eq 1 ]]; then
      prerelease_version=$(echo "$json_response" \
        | jq -r '[.[] | select(.tag_name != null and .prerelease == true)] | sort_by(.created_at) | first.tag_name // empty')
    else
      prerelease_version=$(echo "$json_response" \
        | jq -r '[.[] | select(.tag_name != null and .prerelease == true)] | sort_by(.created_at) | reverse | first.tag_name // empty')
    fi
  fi

  # Error if no releases found and we're not in combined mode
  if [[ -z $version && -z $prerelease_version && $COMBINED -eq 0 ]]; then
    echo "No releases found for repository: $repo" >&2
    exit 1
  fi

  # Return both values for combined output
  if [[ $COMBINED -eq 1 ]]; then
    echo "$version"
    echo "$prerelease_version"
  else
    # Return prerelease if specifically requested, otherwise stable
    if [[ $include_prereleases -eq 1 && -n $prerelease_version ]]; then
      msg "Found prerelease version: $prerelease_version"
      echo "$prerelease_version"
    else
      msg "Found stable release version: $version"
      echo "$version"
    fi
  fi
}

# Fetches the latest tag from the specified branch
get_latest_branch_tag()
{
  local repo="$1"
  local branch="${BRANCH:-main}"
  local api_url="${GITHUB_API_URL}/${repo}/git/refs/tags"

  msg "Fetching latest tag for branch '$branch' from: $api_url"

  local json_response
  json_response=$(api_request "$api_url")

  local version
  version=$(echo "$json_response" \
    | jq -r "[.[] | select(.ref | contains(\"refs/tags/$branch\"))] | sort_by(.ref) | reverse | first.ref | sub(\"refs/tags/\"; \"\") // empty")

  if [[ -z $version && $COMBINED -eq 0 ]]; then
    echo "No tags found for branch: $branch in repository: $repo" >&2
    exit 1
  fi

  msg "Found branch tag: $version"
  echo "$version"
}

# Fetches the latest Git tag (regardless of branch)
get_latest_git_tag()
{
  local repo="$1"
  local api_url="${GITHUB_API_URL}/${repo}/git/refs/tags"

  msg "Fetching latest Git tag from: $api_url"

  local json_response
  json_response=$(api_request "$api_url")

  local version
  version=$(echo "$json_response" \
    | jq -r '[.[] | select(.ref | startswith("refs/tags/"))] | sort_by(.ref) | reverse | first.ref | sub("refs/tags/"; "") // empty')

  if [[ -z $version && $COMBINED -eq 0 ]]; then
    echo "No Git tags found in repository: $repo" >&2
    exit 1
  fi

  msg "Found Git tag: $version"
  echo "$version"
}

# Fetches the latest commit SHA from the specified branch
get_latest_commit()
{
  local repo="$1"
  local branch="${BRANCH:-main}"
  local api_url="${GITHUB_API_URL}/${repo}/commits/$branch"

  msg "Fetching latest commit SHA from: $api_url"

  local json_response
  json_response=$(api_request "$api_url")

  local sha
  sha=$(echo "$json_response" | jq -r '.sha // empty')

  if [[ -z $sha && $COMBINED -eq 0 ]]; then
    echo "Failed to fetch latest commit SHA for branch: $branch in repository: $repo" >&2
    exit 1
  fi

  msg "Found commit SHA: $sha"
  echo "$sha"
}

# Format combined text output
format_combined_text()
{
  local repo="$1"
  local branch="$2"
  local tag="$3"
  local commit="$4"
  local release="$5"
  local prerelease="$6"

  echo "Repository: $repo"

  if [[ -n $branch ]]; then
    echo "Branch:     $branch"
  fi

  if [[ -n $tag ]]; then
    echo "Git Tag:    $tag"
  fi

  if [[ -n $commit ]]; then
    echo "Commit:     $commit"
  fi

  if [[ -n $prerelease ]]; then
    echo "Prerelease: $prerelease"
  fi

  if [[ -n $release ]]; then
    echo "Release:    $release"
  fi
}

# Format combined JSON output
format_combined_json()
{
  local repo="$1"
  local branch="$2"
  local tag="$3"
  local commit="$4"
  local release="$5"
  local prerelease="$6"

  local json="{"
  json+="\"repository\":\"$repo\""

  if [[ -n $branch ]]; then
    json+=",\"branch\":\"$branch\""
  fi

  if [[ -n $tag ]]; then
    json+=",\"tag\":\"$tag\""
  fi

  if [[ -n $commit ]]; then
    json+=",\"commit\":\"$commit\""
  fi

  if [[ -n $prerelease ]]; then
    json+=",\"prerelease\":\"$prerelease\""
  fi

  if [[ -n $release ]]; then
    json+=",\"release\":\"$release\""
  fi

  json+="}"

  echo "$json"
}

# Parse command line arguments
parse_arguments()
{
  # If no arguments provided, show usage
  if [[ $# -eq 0 ]]; then
    usage
  fi

  # Parse arguments
  while [[ $# -gt 0 ]]; do
    case "$1" in
      -h | --help)
        SHOW_HELP=1
        shift
        ;;
      -v | --verbose)
        VERBOSE=1
        shift
        ;;
      -p | --prereleases)
        INCLUDE_PRERELEASES=1
        shift
        ;;
      -o | --oldest)
        OLDEST_RELEASE=1
        shift
        ;;
      -b | --branch)
        if [[ $# -lt 2 ]]; then
          echo "Error: --branch option requires a branch name" >&2
          exit 1
        fi
        BRANCH="$2"
        shift 2
        ;;
      -c | --commit)
        LATEST_COMMIT=1
        shift
        ;;
      -t | --tag)
        LATEST_TAG=1
        shift
        ;;
      -j | --json)
        OUTPUT="json"
        shift
        ;;
      -a | --all)
        COMBINED=1
        shift
        ;;
      -*)
        echo "Error: Unknown option: $1" >&2
        usage
        ;;
      *)
        # If repository is already set, this is an error
        if [[ -n $REPOSITORY ]]; then
          echo "Error: Unexpected argument: $1" >&2
          usage
        fi
        REPOSITORY="$1"
        shift
        ;;
    esac
  done

  # Validate that we have a repository
  if [[ -z $REPOSITORY && $SHOW_HELP -eq 0 ]]; then
    echo "Error: Repository argument is required" >&2
    usage
  fi
}

# Main function
main()
{
  # Parse command line arguments
  parse_arguments "$@"

  # Show help if requested
  if [[ $SHOW_HELP -eq 1 ]]; then
    usage
  fi

  check_dependencies

  # Check rate limits before making other API calls
  check_rate_limits

  # Validate repository existence and get normalized repository name
  local repo_fullname
  repo_fullname=$(check_repository "$REPOSITORY")

  # If --all specified, get all information types
  if [[ $COMBINED -eq 1 ]]; then
    local branch="${BRANCH:-main}"
    local git_tag=""
    local commit_sha=""
    local release_version=""
    local prerelease_version=""

    # Get Git tag if requested
    git_tag=$(get_latest_git_tag "$repo_fullname")

    # Get commit SHA
    commit_sha=$(get_latest_commit "$repo_fullname")

    # Get release versions (stable and prerelease)
    read -r release_version prerelease_version < <(get_release_version "$repo_fullname")

    # Format output based on selected format
    if [[ $OUTPUT == "json" ]]; then
      format_combined_json \
        "$repo_fullname" \
        "$branch" \
        "$git_tag" \
        "$commit_sha" \
        "$release_version" \
        "$prerelease_version"
    else
      format_combined_text \
        "$repo_fullname" \
        "$branch" \
        "$git_tag" \
        "$commit_sha" \
        "$release_version" \
        "$prerelease_version"
    fi

    exit 0
  fi

  # Not combined mode - get only the requested information type
  local result=""

  if [[ $LATEST_COMMIT -eq 1 ]]; then
    result=$(get_latest_commit "$repo_fullname")
  elif [[ $LATEST_TAG -eq 1 ]]; then
    result=$(get_latest_git_tag "$repo_fullname")
  elif [[ -n $BRANCH ]]; then
    result=$(get_latest_branch_tag "$repo_fullname")
  else
    result=$(get_release_version "$repo_fullname")
  fi

  # Output the result in the requested format
  if [[ $OUTPUT == "json" ]]; then
    echo "{\"repository\": \"$repo_fullname\", \"result\": \"$result\"}"
  else
    echo "$result"
  fi
}

main "$@"

# vim: set ts=2 sw=2 ft=sh et:
