#!/bin/sh
set -eu
umask 077

if [ "$#" -ne 1 ]; then
  echo "usage: $0 <github-app-client-id-or-empty>" >&2
  exit 2
fi

client_id=$1
[ -n "$client_id" ] || { echo "GitHub App client ID is required" >&2; exit 2; }
[ -n "${HOME:-}" ] || { echo "HOME is required for the shared GitHub App authorization" >&2; exit 2; }
store_root=$HOME/.cp9/data/shared/github-app
client_key=$(printf '%s' "$client_id" | python3 -c 'import hashlib, sys; print(hashlib.sha256(sys.stdin.buffer.read()).hexdigest())')
cache_file=$store_root/$client_key.json
mkdir -p "$store_root"
chmod 700 "$store_root"
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
lock_helper=$script_dir/remote-source-lock.sh
[ -f "$lock_helper" ] || { echo "GitHub authorization lock helper is missing" >&2; exit 2; }

read_cached_authorization() {
  python3 - "$cache_file" "$client_id" <<'PY'
import json
import os
import pathlib
import shlex
import stat
import sys
import time

path = pathlib.Path(sys.argv[1])
try:
    metadata = path.lstat()
    cache = json.loads(path.read_text(encoding="utf-8"))
except FileNotFoundError:
    raise SystemExit(1)
except (OSError, ValueError):
    raise SystemExit(2)
if not stat.S_ISREG(metadata.st_mode) or stat.S_IMODE(metadata.st_mode) != 0o600 or metadata.st_uid != os.geteuid():
    raise SystemExit(2)
token = cache.get("access_token")
expires_at = cache.get("expires_at")
if cache.get("version") != 1 or cache.get("client_id") != sys.argv[2]:
    raise SystemExit(2)
if not isinstance(token, str) or not token.startswith("ghu_"):
    raise SystemExit(2)
if not isinstance(expires_at, int) or expires_at < 0:
    raise SystemExit(2)
user_id = cache.get("github_user_id")
login = cache.get("github_login")
if not isinstance(user_id, str) or not user_id.isdigit() or not isinstance(login, str) or not login:
    raise SystemExit(2)
refresh_token = cache.get("refresh_token")
refresh_expires_at = cache.get("refresh_expires_at")
refreshable = (
    isinstance(refresh_token, str)
    and refresh_token.startswith("ghr_")
    and isinstance(refresh_expires_at, int)
    and refresh_expires_at > int(time.time()) + 300
)
if expires_at == 0 or expires_at > int(time.time()) + 300:
    state = "usable"
elif refreshable:
    state = "refresh"
else:
    state = "expired"
for name, value in {
    "cache_state": state,
    "access_token": token,
    "cached_user_id": user_id,
}.items():
    print(f"{name}={shlex.quote(str(value))}")
PY
}

parse_oauth_token() {
  python3 - "$1" <<'PY'
import json
import pathlib
import sys

response = json.loads(pathlib.Path(sys.argv[1]).read_text(encoding="utf-8"))
error = response.get("error")
if error == "authorization_pending":
    raise SystemExit(10)
if error == "slow_down":
    raise SystemExit(11)
if error == "bad_refresh_token":
    raise SystemExit(12)
if error:
    raise SystemExit("GitHub device authorization failed: " + str(error))
token = response.get("access_token")
expires_in = response.get("expires_in")
refresh_token = response.get("refresh_token")
refresh_expires_in = response.get("refresh_token_expires_in")
if not isinstance(token, str) or not token.startswith("ghu_"):
    raise SystemExit("GitHub token endpoint returned an invalid user token")
if expires_in is not None and (not isinstance(expires_in, int) or expires_in <= 0 or expires_in > 28800):
    raise SystemExit("GitHub token endpoint returned an invalid user-token lifetime")
if (refresh_token is None) != (refresh_expires_in is None):
    raise SystemExit("GitHub token endpoint returned an incomplete refresh token")
if refresh_token is not None:
    if not isinstance(refresh_token, str) or not refresh_token.startswith("ghr_"):
        raise SystemExit("GitHub token endpoint returned an invalid refresh token")
    if not isinstance(refresh_expires_in, int) or refresh_expires_in <= 0:
        raise SystemExit("GitHub token endpoint returned an invalid refresh-token lifetime")
print(token)
PY
}

verify_token() {
  token_file=$state_dir/access-token
  auth_config=$state_dir/curl-auth
  user_response=$state_dir/user-response
  printf '%s' "$1" >"$token_file"
  chmod 600 "$token_file"
  python3 - "$token_file" "$auth_config" <<'PY'
import pathlib
import sys

token = pathlib.Path(sys.argv[1]).read_text(encoding="utf-8")
if not token.startswith("ghu_") or any(character.isspace() or character in {'"', '\\'} for character in token):
    raise SystemExit("invalid GitHub user token")
path = pathlib.Path(sys.argv[2])
path.write_text(
    'header = "Accept: application/vnd.github+json"\n'
    f'header = "Authorization: Bearer {token}"\n'
    'header = "X-GitHub-Api-Version: 2026-03-10"\n',
    encoding="utf-8",
)
path.chmod(0o600)
PY
  if ! http_status=$(curl --silent --show-error --request GET \
    --max-time 30 \
    --config "$auth_config" \
    --output "$user_response" \
    --write-out '%{http_code}' \
    https://api.github.com/user); then
    echo "validate cached GitHub App authorization: GitHub API request failed" >&2
    return 30
  fi
  case "$http_status" in
    200)
      python3 - "$user_response" <<'PY'
import json
import pathlib
import shlex
import sys

user = json.loads(pathlib.Path(sys.argv[1]).read_text(encoding="utf-8"))
user_id = user.get("id")
login = user.get("login")
if not isinstance(user_id, int) or user_id <= 0 or not isinstance(login, str) or not login:
    raise SystemExit("GitHub user endpoint returned an invalid identity")
print("verified_user_id=" + shlex.quote(str(user_id)))
PY
      ;;
    401) return 10 ;;
    *)
      echo "validate cached GitHub App authorization: GitHub API returned HTTP $http_status" >&2
      return 30
      ;;
  esac
}

save_authorization() {
  python3 - "$cache_file" "$client_id" "$oauth_response" "$state_dir/user-response" "$1" <<'PY'
import json
import os
import pathlib
import sys
import tempfile
import time

path = pathlib.Path(sys.argv[1])
client_id = sys.argv[2]
response = json.loads(pathlib.Path(sys.argv[3]).read_text(encoding="utf-8"))
user = json.loads(pathlib.Path(sys.argv[4]).read_text(encoding="utf-8"))
expected_user_id = sys.argv[5]
user_id = str(user["id"])
login = user["login"]
if expected_user_id and user_id != expected_user_id:
    raise SystemExit("refreshed GitHub authorization changed identity")
expires_in = response.get("expires_in")
value = {
    "version": 1,
    "client_id": client_id,
    "github_user_id": user_id,
    "github_login": login,
    "access_token": response["access_token"],
    "expires_at": 0 if expires_in is None else int(time.time()) + expires_in,
}
refresh_token = response.get("refresh_token")
refresh_expires_in = response.get("refresh_token_expires_in")
if refresh_token is not None:
    value["refresh_token"] = refresh_token
    value["refresh_expires_at"] = int(time.time()) + refresh_expires_in
path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
os.chmod(path.parent, 0o700)
handle, temporary = tempfile.mkstemp(prefix=".github-app-user-", dir=path.parent)
try:
    with os.fdopen(handle, "w", encoding="utf-8") as stream:
        json.dump(value, stream, separators=(",", ":"))
        stream.flush()
        os.fsync(stream.fileno())
    os.chmod(temporary, 0o600)
    os.replace(temporary, path)
finally:
    try:
        os.unlink(temporary)
    except FileNotFoundError:
        pass
PY
}

refresh_cached_authorization() {
  refresh_request=$state_dir/refresh-request
  oauth_response=$state_dir/oauth-response
  python3 - "$cache_file" "$refresh_request" "$client_id" <<'PY'
import json
import pathlib
import sys
import urllib.parse

cache = json.loads(pathlib.Path(sys.argv[1]).read_text(encoding="utf-8"))
path = pathlib.Path(sys.argv[2])
path.write_text(urllib.parse.urlencode({
    "client_id": sys.argv[3],
    "grant_type": "refresh_token",
    "refresh_token": cache["refresh_token"],
}), encoding="utf-8")
path.chmod(0o600)
PY
  if ! curl --fail --silent --show-error --request POST \
    --max-time 30 \
    --header "Accept: application/json" \
    --data-binary "@$refresh_request" \
    --output "$oauth_response" \
    https://github.com/login/oauth/access_token; then
    echo "refresh cached GitHub App authorization: GitHub OAuth request failed" >&2
    return 30
  fi
  if token=$(parse_oauth_token "$oauth_response"); then
    :
  else
    status=$?
    [ "$status" -ne 12 ] || return 12
    return "$status"
  fi
  if identity=$(verify_token "$token"); then
    eval "$identity"
  else
    status=$?
    return "$status"
  fi
  save_authorization "$cached_user_id"
  printf '%s' "$token"
}

lock_dir=$store_root/$client_key.lock
lock_acquired=
state_dir=
lock_token=$(python3 -c 'import secrets; print(secrets.token_hex(16))')
cleanup() {
  [ -z "$state_dir" ] || rm -rf "$state_dir"
  if [ -n "$lock_acquired" ]; then
    sh "$lock_helper" release "$lock_dir" "$lock_token" >/dev/null 2>&1 || true
  fi
}
trap cleanup EXIT HUP INT TERM
sh "$lock_helper" acquire "$lock_dir" "$lock_token" 120 900 "github-user-auth pid=$$" >/dev/null
lock_acquired=true

state_dir=$(mktemp -d "${TMPDIR:-/tmp}/cp9-github-user.XXXXXX")

cache_state=missing
if cached=$(read_cached_authorization 2>/dev/null); then
  eval "$cached"
else
  cache_status=$?
  if [ "$cache_status" -ne 1 ]; then
    echo "Cached GitHub App authorization is invalid; starting a new Device Flow" >&2
    rm -f "$cache_file"
  fi
fi

if [ "$cache_state" = "usable" ]; then
  if identity=$(verify_token "$access_token"); then
    eval "$identity"
    if [ "$verified_user_id" = "$cached_user_id" ]; then
      printf '%s' "$access_token"
      exit 0
    fi
  else
    verify_status=$?
    if [ "$verify_status" -ne 10 ]; then
      exit "$verify_status"
    fi
  fi
  echo "Cached GitHub App authorization is revoked or invalid; starting a new Device Flow" >&2
  rm -f "$cache_file"
elif [ "$cache_state" = "refresh" ]; then
  if token=$(refresh_cached_authorization); then
    printf '%s' "$token"
    exit 0
  else
    refresh_status=$?
    case "$refresh_status" in
      10|12)
        echo "Cached GitHub App authorization expired or was revoked; starting a new Device Flow" >&2
        rm -f "$cache_file"
        ;;
      *) exit "$refresh_status" ;;
    esac
  fi
elif [ "$cache_state" = "expired" ]; then
  echo "Cached GitHub App authorization expired; starting a new Device Flow" >&2
  rm -f "$cache_file"
fi

eval "$(curl --fail --silent --show-error --request POST \
  --max-time 30 \
  --header "Accept: application/json" \
  --data-urlencode "client_id=$client_id" \
  https://github.com/login/device/code |
  python3 -c '
import json
import shlex
import sys

response = json.load(sys.stdin)
values = {
    "device_code": response.get("device_code"),
    "user_code": response.get("user_code"),
    "verification_uri": response.get("verification_uri"),
    "device_expires_in": response.get("expires_in"),
    "poll_interval": response.get("interval", 5),
}
if not all(values.values()) or values["verification_uri"] != "https://github.com/login/device":
    raise SystemExit("GitHub device endpoint returned an invalid response")
if not isinstance(values["user_code"], str):
    raise SystemExit("GitHub device endpoint returned an invalid user code")
if not isinstance(values["device_expires_in"], int) or values["device_expires_in"] <= 0:
    raise SystemExit("GitHub device endpoint returned an invalid expiry")
if not isinstance(values["poll_interval"], int) or values["poll_interval"] <= 0:
    raise SystemExit("GitHub device endpoint returned an invalid poll interval")
values["human_action"] = json.dumps({
    "version": 1,
    "type": "github_device_flow",
    "verification_uri": values["verification_uri"],
    "user_code": values["user_code"],
    "expires_in_seconds": values["device_expires_in"],
    "continuation": "automatic",
}, separators=(",", ":"))
for name, value in values.items():
    print(f"{name}={shlex.quote(str(value))}")
'
)"

oauth_request=$state_dir/oauth-request
oauth_response=$state_dir/oauth-response
printf '%s' "$device_code" | python3 -c '
import pathlib
import sys
import urllib.parse

pathlib.Path(sys.argv[1]).write_text(urllib.parse.urlencode({
    "client_id": sys.argv[2],
    "device_code": sys.stdin.read(),
    "grant_type": "urn:ietf:params:oauth:grant-type:device_code",
}), encoding="utf-8")
' "$oauth_request" "$client_id"
chmod 600 "$oauth_request"

printf '\n%s\n' '========== CP9 HUMAN ACTION REQUIRED ==========' >&2
printf '%s\n' "CP9_HUMAN_ACTION $human_action" >&2
printf '%s\n' 'CP9 is waiting and will continue automatically after authorization.' >&2
printf '%s\n\n' '================================================' >&2
if command -v open >/dev/null 2>&1; then
  open "$verification_uri" >/dev/null 2>&1 || true
fi

deadline=$(( $(date +%s) + device_expires_in ))
while [ "$(date +%s)" -lt "$deadline" ]; do
  sh "$lock_helper" heartbeat "$lock_dir" "$lock_token" 120
  sleep "$poll_interval"
  if curl --fail --silent --show-error --request POST \
    --max-time 30 \
    --header "Accept: application/json" \
    --data-binary "@$oauth_request" \
    --output "$oauth_response" \
    https://github.com/login/oauth/access_token; then
    if token=$(parse_oauth_token "$oauth_response"); then
      if identity=$(verify_token "$token"); then
        eval "$identity"
      else
        verify_status=$?
        if [ "$verify_status" -eq 10 ]; then
          echo "GitHub returned a revoked or invalid user token" >&2
          exit 1
        fi
        exit "$verify_status"
      fi
      save_authorization ""
      printf '%s' "$token"
      exit 0
    else
      status=$?
      case "$status" in
        10) ;;
        11)
          poll_interval=$((poll_interval + 5))
          [ "$poll_interval" -le 60 ] || poll_interval=60
          ;;
        *) exit "$status" ;;
      esac
    fi
  else
    curl_status=$?
    case "$curl_status" in
      6|7|18|28|35|52|55|56) ;;
      *) exit "$curl_status" ;;
    esac
  fi
done

echo "GitHub device authorization expired; rerun the build to try again" >&2
exit 1
