#!/usr/bin/env bash

set -Eeuo pipefail

readonly PROGRAM_NAME="tobserver-agent"
readonly STATE_VERSION=2
readonly REPOSITORY="oibot/Tobserver"
readonly FETCH_URL="https://codeberg.org/oibot/Tobserver.git"
readonly PUSH_URL="ssh://git@codeberg.org/oibot/Tobserver.git"
readonly CODEBERG_API_URL="https://codeberg.org/api/v1/repos/oibot/Tobserver"
readonly CODEBERG_REPO_URL="https://codeberg.org/oibot/Tobserver"
readonly BASE_BRANCH="main"
readonly BRANCH_PREFIX="agent/"
readonly TOPIC_PREFIX="tobserver-agent-"
readonly GIT_AUTHOR_NAME="Tobserver Agent"
readonly GIT_AUTHOR_EMAIL="oibot@noreply.codeberg.org"
readonly MAX_TITLE_BYTES=200
readonly MAX_BODY_BYTES=32768
readonly MAX_REVIEW_BUNDLE_BYTES=262144
readonly API_PAGE_SIZE=50
readonly MAX_API_PAGES=20

STATE_HOME="${XDG_STATE_HOME:-${HOME}/.local/state}/${PROGRAM_NAME}"
ACTIVE_FILE="${STATE_HOME}/active"
START_WORKSPACE_PATH=""

log() {
  printf '%s\n' "$*"
}

warn() {
  printf '%s: %s\n' "$PROGRAM_NAME" "$*" >&2
}

die() {
  warn "$*"
  exit 1
}

preserve_start_failure() {
  local exit_code=$?

  trap - ERR
  warn "Start failed; preserving workspace: ${START_WORKSPACE_PATH}"
  exit "$exit_code"
}

usage() {
  cat <<'EOF'
Usage:
  tobserver-agent start "task description"
  tobserver-agent inspect PR_NUMBER
  tobserver-agent revise PR_NUMBER
  tobserver-agent resume /tmp/tobserver-agent.XXXXXX
  tobserver-agent status [workspace]
  tobserver-agent check [workspace]
  tobserver-agent submit --title "title" --body-file PATH [workspace]
  tobserver-agent cleanup WORKSPACE

inspect reads the public Codeberg API and writes a bounded review bundle to
stdout without creating a workspace. revise creates an isolated workspace and
stores the same bundle under .git for inspection before editing.

The helper is intentionally limited to oibot/Tobserver on Codeberg. It submits
only AGit pull requests from local agent/* branches, never pushes main, never
uses a Codeberg API token, and never accesses or deploys to the live server.
EOF
}

require_command() {
  command -v "$1" >/dev/null 2>&1 || die "Required command not found: $1"
}

ensure_state_home() {
  mkdir -p "$STATE_HOME"
  chmod 700 "$STATE_HOME"
}

canonical_directory() {
  local directory=$1
  [[ -d "$directory" ]] || die "Workspace does not exist: $directory"
  (cd "$directory" && pwd -P)
}

state_file_for() {
  printf '%s/.git/tobserver-agent-state.json\n' "$1"
}

write_active_workspace() {
  local workspace=$1
  local temporary

  ensure_state_home
  temporary="${ACTIVE_FILE}.tmp.$$"
  printf '%s\n' "$workspace" >"$temporary"
  chmod 600 "$temporary"
  mv -f "$temporary" "$ACTIVE_FILE"
}

clear_active_workspace() {
  local workspace=$1
  local active=""

  if [[ -f "$ACTIVE_FILE" ]]; then
    IFS= read -r active <"$ACTIVE_FILE" || true
    if [[ "$active" == "$workspace" ]]; then
      rm -f "$ACTIVE_FILE"
    fi
  fi
}

active_workspace() {
  local workspace=""

  [[ -f "$ACTIVE_FILE" ]] || return 1
  IFS= read -r workspace <"$ACTIVE_FILE" || return 1
  [[ -n "$workspace" ]] || return 1
  printf '%s\n' "$workspace"
}

workspace_from_current_directory() {
  local root=""

  root=$(git rev-parse --show-toplevel 2>/dev/null) || return 1
  [[ -f "$(state_file_for "$root")" ]] || return 1
  canonical_directory "$root"
}

resolve_workspace() {
  local requested=${1:-}
  local workspace=""

  if [[ -n "$requested" ]]; then
    workspace=$(canonical_directory "$requested")
  elif workspace=$(workspace_from_current_directory); then
    :
  elif workspace=$(active_workspace); then
    workspace=$(canonical_directory "$workspace")
  else
    die "No active workspace. Run '${PROGRAM_NAME} start \"task\"' or provide a workspace path."
  fi

  printf '%s\n' "$workspace"
}

assert_single_config_value() {
  local key=$1
  local expected=$2
  local actual
  local count

  actual=$(git config --local --get "$key" || true)
  count=$(git config --local --get-all "$key" 2>/dev/null | wc -l | tr -d '[:space:]')
  [[ "$count" == "1" && "$actual" == "$expected" ]] \
    || die "Unsafe Git configuration for $key. Expected exactly: $expected"
}

is_valid_topic() {
  [[ "$1" =~ ^${TOPIC_PREFIX}[a-z0-9][a-z0-9-]*$ ]]
}

topic_for_branch() {
  local branch=$1
  local suffix

  [[ "$branch" == "${BRANCH_PREFIX}"* && "$branch" != "$BRANCH_PREFIX" ]] \
    || return 1
  suffix=${branch#"$BRANCH_PREFIX"}
  [[ "$suffix" =~ ^[a-z0-9][a-z0-9-]*$ ]] || return 1
  printf '%s%s\n' "$TOPIC_PREFIX" "$suffix"
}

branch_for_topic() {
  local topic=$1

  is_valid_topic "$topic" || return 1
  printf '%s%s\n' "$BRANCH_PREFIX" "${topic#"$TOPIC_PREFIX"}"
}

validate_workspace() {
  local workspace=$1
  local state_file
  local state_workspace
  local state_repository
  local state_status
  local state_branch
  local state_topic
  local state_mode
  local expected_topic
  local root
  local branch

  [[ -d "$workspace/.git" ]] || die "Not a helper-created Git workspace: $workspace"
  state_file=$(state_file_for "$workspace")
  [[ -f "$state_file" ]] || die "Missing helper state in workspace: $workspace"

  root=$(cd "$workspace" && git rev-parse --show-toplevel)
  root=$(canonical_directory "$root")
  [[ "$root" == "$workspace" ]] || die "Workspace root mismatch: $workspace"

  jq -e --argjson version "$STATE_VERSION" '.version == $version' "$state_file" >/dev/null \
    || die "Unsupported workspace state version."
  state_workspace=$(jq -er '.workspace' "$state_file") \
    || die "Invalid workspace state: $state_file"
  state_repository=$(jq -er '.repository' "$state_file") \
    || die "Invalid workspace repository state: $state_file"
  state_status=$(jq -er '.status' "$state_file") \
    || die "Invalid workspace status: $state_file"
  state_branch=$(jq -er '.branch' "$state_file") \
    || die "Invalid workspace branch state: $state_file"
  state_topic=$(jq -er '.topic' "$state_file") \
    || die "Invalid workspace topic state: $state_file"
  state_mode=$(jq -er '.mode' "$state_file") \
    || die "Invalid workspace mode: $state_file"

  [[ "$state_workspace" == "$workspace" ]] || die "Workspace state path mismatch."
  [[ "$state_repository" == "$REPOSITORY" ]] || die "Workspace is not for $REPOSITORY."
  [[ "$state_status" == "active" || "$state_status" == "submitted" ]] \
    || die "Unknown workspace status: $state_status"
  [[ "$state_mode" == "new" || "$state_mode" == "revision" ]] \
    || die "Unknown workspace mode: $state_mode"
  is_valid_topic "$state_topic" || die "Unsafe AGit topic in workspace state."
  expected_topic=$(topic_for_branch "$state_branch") \
    || die "Unsafe branch in workspace state: $state_branch"
  [[ "$state_topic" == "$expected_topic" ]] \
    || die "Workspace branch and AGit topic do not match."

  (
    cd "$workspace"
    assert_single_config_value remote.origin.url "$FETCH_URL"
    assert_single_config_value remote.origin.pushurl "$PUSH_URL"
    assert_single_config_value user.name "$GIT_AUTHOR_NAME"
    assert_single_config_value user.email "$GIT_AUTHOR_EMAIL"

    branch=$(git symbolic-ref --quiet --short HEAD) \
      || die "Detached HEAD is not allowed."
    [[ "$branch" == "$state_branch" ]] || die "Workspace branch does not match helper state."
    [[ "$branch" != "$BASE_BRANCH" ]] || die "Working directly on $BASE_BRANCH is forbidden."
  )
}

create_branch_name() {
  local task=$1
  local slug
  local timestamp
  local random_suffix

  slug=$(printf '%s' "$task" \
    | tr '[:upper:]' '[:lower:]' \
    | tr -cs 'a-z0-9' '-' \
    | sed -e 's/^-//' -e 's/-$//' \
    | cut -c1-40)
  [[ -n "$slug" ]] || slug="change"
  timestamp=$(date -u '+%Y%m%d-%H%M%S')
  random_suffix=$(od -An -N3 -tx1 /dev/urandom | tr -d '[:space:]')
  printf '%s%s-%s-%s\n' "$BRANCH_PREFIX" "$slug" "$timestamp" "$random_suffix"
}

write_initial_state() {
  local workspace=$1
  local task=$2
  local branch=$3
  local topic=$4
  local mode=$5
  local initial_head=$6
  local pr_number=${7:-}
  local pr_url=${8:-}
  local state_file
  local temporary

  state_file=$(state_file_for "$workspace")
  temporary="${state_file}.tmp.$$"
  jq -n \
    --argjson version "$STATE_VERSION" \
    --arg repository "$REPOSITORY" \
    --arg workspace "$workspace" \
    --arg task "$task" \
    --arg branch "$branch" \
    --arg topic "$topic" \
    --arg mode "$mode" \
    --arg initialHead "$initial_head" \
    --arg pullRequestNumber "$pr_number" \
    --arg pullRequestUrl "$pr_url" \
    --arg status "active" \
    --arg createdAt "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" \
    '{version: $version, repository: $repository, workspace: $workspace, task: $task, branch: $branch, topic: $topic, mode: $mode, initialHead: $initialHead, pullRequestNumber: $pullRequestNumber, pullRequestUrl: $pullRequestUrl, status: $status, createdAt: $createdAt}' \
    >"$temporary"
  chmod 600 "$temporary"
  mv -f "$temporary" "$state_file"
}

configure_workspace_git() {
  local workspace=$1

  (
    cd "$workspace"
    git remote set-url --push origin "$PUSH_URL"
    git config --local user.name "$GIT_AUTHOR_NAME"
    git config --local user.email "$GIT_AUTHOR_EMAIL"
  )
}

start_workspace() {
  local task=$1
  local temp_root
  local workspace
  local branch
  local topic
  local initial_head

  [[ -n "${task//[[:space:]]/}" ]] || die "Task description must not be empty."
  require_command git
  require_command jq

  temp_root=${TMPDIR:-/tmp}
  temp_root=${temp_root%/}
  workspace=$(mktemp -d "${temp_root}/tobserver-agent.XXXXXX")
  chmod 700 "$workspace"
  workspace=$(canonical_directory "$workspace")

  START_WORKSPACE_PATH=$workspace
  trap preserve_start_failure ERR

  git clone --quiet --origin origin --branch "$BASE_BRANCH" --single-branch \
    "$FETCH_URL" "$workspace"

  branch=$(create_branch_name "$task")
  topic=$(topic_for_branch "$branch") || die "Could not derive a safe AGit topic."
  configure_workspace_git "$workspace"
  (cd "$workspace" && git switch --quiet --create "$branch")

  initial_head=$(cd "$workspace" && git rev-parse HEAD)
  write_initial_state "$workspace" "$task" "$branch" "$topic" "new" "$initial_head"
  write_active_workspace "$workspace"
  validate_workspace "$workspace"
  trap - ERR
  unset START_WORKSPACE_PATH

  log "Workspace: $workspace"
  log "Branch:    $branch"
  log "AGit topic: $topic"
  log "Resume:    $PROGRAM_NAME resume $workspace"
}

assert_active_workspace() {
  local workspace=$1
  local state_file

  state_file=$(state_file_for "$workspace")
  jq -e '.status == "active"' "$state_file" >/dev/null \
    || die "Workspace is not active: $workspace"
}

refresh_base_branch() {
  local workspace=$1
  local mode

  mode=$(jq -r '.mode' "$(state_file_for "$workspace")")
  (
    cd "$workspace"
    git fetch --quiet origin \
      "refs/heads/${BASE_BRANCH}:refs/remotes/origin/${BASE_BRANCH}"
    git merge-base "origin/${BASE_BRANCH}" HEAD >/dev/null \
      || die "The agent branch has no common history with origin/${BASE_BRANCH}."
    if [[ "$mode" == "new" ]]; then
      git merge-base --is-ancestor "origin/${BASE_BRANCH}" HEAD \
        || die "The new agent branch is not based on the latest origin/${BASE_BRANCH}."
    elif ! git merge-base --is-ancestor "origin/${BASE_BRANCH}" HEAD; then
      warn "origin/${BASE_BRANCH} advanced after this pull request was opened; Codeberg must verify mergeability."
    fi
  )
}

changed_paths() {
  git diff --name-only --diff-filter=ACDMRTUXB -z "origin/${BASE_BRANCH}" --
  git ls-files --others --exclude-standard -z
}

assert_no_secret_payload_changes() {
  local workspace=$1
  local path

  while IFS= read -r -d '' path; do
    case "$path" in
      secrets/* | .sops.yaml)
        die "Agent changes to SOPS payload/configuration are forbidden: $path"
        ;;
    esac
  done < <(cd "$workspace" && changed_paths)
}

run_nix_formatter_check() {
  local workspace=$1
  local file_list
  local path

  file_list="$workspace/.git/tobserver-agent-nix-files.$$"
  (
    cd "$workspace"
    while IFS= read -r -d '' path; do
      if [[ "$path" == *.nix && -f "$path" ]]; then
        printf '%s\0' "$path"
      fi
    done < <(changed_paths) >"$file_list"
    if [[ -s "$file_list" ]]; then
      xargs -0 nixpkgs-fmt --check <"$file_list"
    fi
  )
  rm -f "$file_list"
}

emit_proposed_patch() {
  local workspace=$1
  local path

  (
    cd "$workspace"
    git diff --binary --no-ext-diff "origin/${BASE_BRANCH}" --
    while IFS= read -r -d '' path; do
      printf '\ndiff --git a/%s b/%s\nnew file mode 100644\n--- /dev/null\n+++ b/%s\n' \
        "$path" "$path" "$path"
      cat -- "$path"
      printf '\n'
    done < <(git ls-files --others --exclude-standard -z)
  )
}

run_gitleaks_check() {
  local workspace=$1

  emit_proposed_patch "$workspace" \
    | gitleaks stdin --no-banner --no-color --redact
}

strict_nixos_apply_expression() {
  cat <<'EOF'
config:
let
  failedAssertions = builtins.filter (item: !item.assertion) config.assertions;
  checkedAssertions =
    if failedAssertions == [ ] then
      true
    else
      throw (
        "Failed NixOS assertions:\n"
        + builtins.concatStringsSep "\n" (map (item: "- " + item.message) failedAssertions)
      );
  unitTexts = builtins.mapAttrs (_: unit: unit.text) config.systemd.units;
in
builtins.deepSeq checkedAssertions (
  builtins.deepSeq unitTexts config.system.build.toplevel.drvPath
)
EOF
}

run_strict_nixos_evaluation() {
  local workspace=$1
  local apply_expression

  apply_expression=$(strict_nixos_apply_expression)
  (
    cd "$workspace"
    nix eval --raw --no-write-lock-file \
      "path:.#nixosConfigurations.tobserver.config" \
      --apply "$apply_expression" \
      >/dev/null
  )
}

run_checks() {
  local workspace=$1

  validate_workspace "$workspace"
  assert_active_workspace "$workspace"
  refresh_base_branch "$workspace"
  assert_no_secret_payload_changes "$workspace"

  log "[1/5] Checking Nix formatting"
  run_nix_formatter_check "$workspace"

  log "[2/5] Checking Git whitespace"
  (cd "$workspace" && git diff --check "origin/${BASE_BRANCH}" --)

  log "[3/5] Evaluating the flake without building"
  (cd "$workspace" && nix flake check --no-build --no-write-lock-file "path:.")

  log "[4/5] Strictly evaluating NixOS assertions and systemd units"
  run_strict_nixos_evaluation "$workspace"

  log "[5/5] Scanning the proposed patch with Gitleaks"
  run_gitleaks_check "$workspace"

  log "All checks passed."
}

show_status() {
  local workspace=$1
  local state_file
  local branch

  validate_workspace "$workspace"
  state_file=$(state_file_for "$workspace")
  branch=$(cd "$workspace" && git symbolic-ref --quiet --short HEAD)

  log "Workspace:  $workspace"
  log "Repository: $(jq -r '.repository' "$state_file")"
  log "Branch:     $branch"
  log "AGit topic: $(jq -r '.topic' "$state_file")"
  log "State:      $(jq -r '.status' "$state_file")"
  log "Mode:       $(jq -r '.mode' "$state_file")"
  log "Task:       $(jq -r '.task' "$state_file")"
  if [[ "$(jq -r '.mode' "$state_file")" == "revision" ]]; then
    log "Pull request: $(jq -r '.pullRequestUrl' "$state_file")"
    if [[ -f "$workspace/.git/tobserver-agent-review.md" ]]; then
      log "Review bundle: $workspace/.git/tobserver-agent-review.md"
    fi
  fi
  log "Fetch URL:  $FETCH_URL"
  log "Push URL:   $PUSH_URL"
  log "Changes:"
  (cd "$workspace" && git status --short)
}

resume_workspace() {
  local workspace=$1

  workspace=$(canonical_directory "$workspace")
  validate_workspace "$workspace"
  assert_active_workspace "$workspace"
  write_active_workspace "$workspace"
  log "Resumed workspace: $workspace"
  show_status "$workspace"
}

fetch_json_url() {
  local url=$1
  local response_file
  local api_error

  ensure_state_home
  response_file="$STATE_HOME/api-response.$$"
  umask 077

  if ! curl --silent --show-error --fail-with-body \
    --proto '=https' --tlsv1.2 \
    --request GET \
    --url "$url" \
    --output "$response_file"; then
    api_error=$(jq -r '.message // "Codeberg rejected the public API request."' \
      "$response_file" 2>/dev/null || true)
    rm -f "$response_file"
    die "$api_error"
  fi

  cat "$response_file"
  rm -f "$response_file"
}

fetch_pull_request_json() {
  local pr_number=$1
  fetch_json_url "${CODEBERG_API_URL}/pulls/${pr_number}"
}

fetch_open_pull_requests_json() {
  fetch_json_url "${CODEBERG_API_URL}/pulls?state=open&limit=50"
}

fetch_paginated_json_array() {
  local base_url=$1
  local separator='?'
  local page=1
  local response
  local count
  local combined='[]'

  [[ "$base_url" == *\?* ]] && separator='&'
  while :; do
    response=$(fetch_json_url \
      "${base_url}${separator}limit=${API_PAGE_SIZE}&page=${page}")
    jq -e 'type == "array"' <<<"$response" >/dev/null \
      || die "Codeberg returned an unexpected paginated API response."
    count=$(jq 'length' <<<"$response")

    if ((page > MAX_API_PAGES)); then
      ((count == 0)) \
        || die "Codeberg review data exceeds the ${MAX_API_PAGES}-page inspection limit."
      break
    fi

    combined=$(jq -cn \
      --argjson accumulated "$combined" \
      --argjson pageItems "$response" \
      '$accumulated + $pageItems')
    ((count < API_PAGE_SIZE)) && break
    page=$((page + 1))
  done

  printf '%s\n' "$combined"
}

assert_pull_request_identity() {
  local pr_json=$1
  local pr_number=$2
  local pr_url

  jq -e --argjson number "$pr_number" '.number == $number' <<<"$pr_json" >/dev/null \
    || die "Codeberg returned the wrong pull request."
  pr_url=$(jq -er '.html_url' <<<"$pr_json") \
    || die "Codeberg returned an invalid pull request."
  [[ "$pr_url" == "${CODEBERG_REPO_URL}/pulls/${pr_number}" ]] \
    || die "Pull request #$pr_number returned an unexpected URL."
}

pull_request_review_bundle_json() {
  local pr_number=$1
  local pr_json=$2
  local issue_comments
  local reviews
  local reviews_with_comments='[]'
  local review_id
  local review
  local inline_comments

  issue_comments=$(fetch_paginated_json_array \
    "${CODEBERG_API_URL}/issues/${pr_number}/comments")
  reviews=$(fetch_paginated_json_array \
    "${CODEBERG_API_URL}/pulls/${pr_number}/reviews")

  while IFS= read -r review_id; do
    [[ -n "$review_id" ]] || continue
    inline_comments=$(fetch_paginated_json_array \
      "${CODEBERG_API_URL}/pulls/${pr_number}/reviews/${review_id}/comments")
    review=$(jq -c --argjson id "$review_id" \
      '.[] | select(.id == $id)' <<<"$reviews")
    reviews_with_comments=$(jq -cn \
      --argjson accumulated "$reviews_with_comments" \
      --argjson review "$review" \
      --argjson comments "$inline_comments" \
      '$accumulated + [($review + {inline_comments: $comments})]')
  done < <(jq -r '.[].id' <<<"$reviews")

  jq -cn \
    --arg fetchedAt "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" \
    --argjson pullRequest "$pr_json" \
    --argjson issueComments "$issue_comments" \
    --argjson reviews "$reviews_with_comments" \
    '{fetched_at: $fetchedAt, pull_request: $pullRequest, issue_comments: $issueComments, reviews: $reviews}'
}

render_pull_request_review_bundle() {
  jq -r '
    def quoted:
      if . == null or . == "" then "_None._"
      else tostring | split("\n") | map("> " + .) | join("\n")
      end;
    .pull_request as $pr |
    "# Tobserver pull request #\($pr.number) review bundle\n\n" +
    "Fetched: \(.fetched_at)\n\n" +
    "URL: \($pr.html_url)\n\n" +
    "State: \($pr.state)\n\n" +
    "Head: \($pr.head.label // $pr.head.ref) @ \($pr.head.sha)\n\n" +
    "## Title\n\n\($pr.title | quoted)\n\n" +
    "## Description\n\n\($pr.body | quoted)\n\n" +
    "## General discussion\n\n" +
    (([.issue_comments[] |
      "### \(.user.login // "unknown") at \(.created_at // "unknown time")\n\n" +
      "URL: \(.html_url // "unavailable")\n\n" +
      (.body | quoted)
    ] | if length == 0 then ["_No general comments._"] else . end) | join("\n\n")) +
    "\n\n## Reviews\n\n" +
    (([.reviews[] |
      "### Review \(.id) — \(.state // "unknown")\n\n" +
      "Reviewer: \(.user.login // "unknown")\n\n" +
      "Submitted: \(.submitted_at // "unknown time")\n\n" +
      "Commit: \(.commit_id // "unknown")\n\n" +
      "Official: \(.official // false); stale: \(.stale // false); dismissed: \(.dismissed // false)\n\n" +
      "Review body:\n\n" + (.body | quoted) + "\n\n" +
      (([.inline_comments[] |
        "#### Inline comment on `\(.path // "unknown path")`\n\n" +
        "Author: \(.user.login // "unknown")\n\n" +
        "URL: \(.html_url // "unavailable")\n\n" +
        "Commit: \(.commit_id // "unknown"); position: \(.position // "unknown")\n\n" +
        "Resolution: \(if .resolver then "resolved by " + (.resolver.login // "unknown") else "unresolved" end)\n\n" +
        "Diff context:\n\n" + (.diff_hunk | quoted) + "\n\n" +
        "Comment:\n\n" + (.body | quoted)
      ] | if length == 0 then ["_No inline comments._"] else . end) | join("\n\n"))
    ] | if length == 0 then ["_No reviews._"] else . end) | join("\n\n"))
  '
}

validate_review_bundle_size() {
  local review_bundle=$1
  local bundle_bytes

  bundle_bytes=$(printf '%s' "$review_bundle" | wc -c | tr -d '[:space:]')
  ((bundle_bytes <= MAX_REVIEW_BUNDLE_BYTES)) \
    || die "Pull-request review bundle exceeds ${MAX_REVIEW_BUNDLE_BYTES} bytes."
}

inspect_pull_request() {
  local pr_number=$1
  local pr_json
  local bundle_json
  local review_bundle

  [[ "$pr_number" =~ ^[1-9][0-9]*$ ]] \
    || die "inspect requires a positive numeric Codeberg pull-request number."
  require_command curl
  require_command jq

  pr_json=$(fetch_pull_request_json "$pr_number")
  assert_pull_request_identity "$pr_json" "$pr_number"
  bundle_json=$(pull_request_review_bundle_json "$pr_number" "$pr_json")
  review_bundle=$(render_pull_request_review_bundle <<<"$bundle_json")
  validate_review_bundle_size "$review_bundle"
  printf '%s\n' "$review_bundle"
}

pr_topic_from_json() {
  local pr_json=$1
  local label
  local topic

  label=$(jq -er '.head.label' <<<"$pr_json") || return 1
  [[ "$label" == "oibot/"* ]] || return 1
  topic=${label#oibot/}
  is_valid_topic "$topic" || return 1
  printf '%s\n' "$topic"
}

assert_revisable_pull_request() {
  local pr_json=$1
  local pr_number=$2
  local expected_topic=${3:-}
  local topic

  assert_pull_request_identity "$pr_json" "$pr_number"
  [[ "$(jq -r '.state' <<<"$pr_json")" == "open" ]] \
    || die "Pull request #$pr_number is not open."
  [[ "$(jq -r '.merged // false' <<<"$pr_json")" == "false" ]] \
    || die "Pull request #$pr_number is already merged."
  [[ "$(jq -r '.base.ref' <<<"$pr_json")" == "$BASE_BRANCH" ]] \
    || die "Pull request #$pr_number does not target $BASE_BRANCH."
  [[ "$(jq -r '.head.repo.full_name' <<<"$pr_json")" == "$REPOSITORY" ]] \
    || die "Pull request #$pr_number comes from another repository or fork."
  [[ "$(jq -r '.user.login' <<<"$pr_json")" == "oibot" ]] \
    || die "Pull request #$pr_number was not opened by oibot."
  [[ "$(jq -r '.flow // 0' <<<"$pr_json")" == "1" ]] \
    || die "Pull request #$pr_number was not created through AGit."
  [[ "$(jq -r '.head.ref' <<<"$pr_json")" == "refs/pull/${pr_number}/head" ]] \
    || die "Pull request #$pr_number has an unexpected head ref."

  topic=$(pr_topic_from_json "$pr_json") \
    || die "Pull request #$pr_number does not use a helper-managed AGit topic."
  [[ -z "$expected_topic" || "$topic" == "$expected_topic" ]] \
    || die "Pull request #$pr_number no longer uses the expected AGit topic."
}

revise_pull_request() {
  local pr_number=$1
  local pr_json
  local pr_title
  local pr_url
  local topic
  local branch
  local task
  local temp_root
  local workspace
  local initial_head
  local bundle_json
  local review_bundle
  local review_bundle_path

  [[ "$pr_number" =~ ^[1-9][0-9]*$ ]] \
    || die "revise requires a positive numeric Codeberg pull-request number."
  require_command curl
  require_command git
  require_command jq

  pr_json=$(fetch_pull_request_json "$pr_number")
  assert_revisable_pull_request "$pr_json" "$pr_number"
  pr_title=$(jq -r '.title' <<<"$pr_json")
  pr_url=$(jq -r '.html_url' <<<"$pr_json")
  topic=$(pr_topic_from_json "$pr_json")
  branch=$(branch_for_topic "$topic") \
    || die "Could not derive a safe local branch for pull request #$pr_number."
  task="Revise Codeberg PR #${pr_number}: ${pr_title}"
  bundle_json=$(pull_request_review_bundle_json "$pr_number" "$pr_json")
  review_bundle=$(render_pull_request_review_bundle <<<"$bundle_json")
  validate_review_bundle_size "$review_bundle"

  temp_root=${TMPDIR:-/tmp}
  temp_root=${temp_root%/}
  workspace=$(mktemp -d "${temp_root}/tobserver-agent.XXXXXX")
  chmod 700 "$workspace"
  workspace=$(canonical_directory "$workspace")

  START_WORKSPACE_PATH=$workspace
  trap preserve_start_failure ERR

  git clone --quiet --origin origin --branch "$BASE_BRANCH" --single-branch \
    "$FETCH_URL" "$workspace"
  configure_workspace_git "$workspace"
  (
    cd "$workspace"
    git fetch --quiet origin "refs/pull/${pr_number}/head"
    git switch --quiet --create "$branch" FETCH_HEAD
  )

  initial_head=$(cd "$workspace" && git rev-parse HEAD)
  write_initial_state "$workspace" "$task" "$branch" "$topic" "revision" \
    "$initial_head" "$pr_number" "$pr_url"
  review_bundle_path="$workspace/.git/tobserver-agent-review.md"
  printf '%s\n' "$review_bundle" >"$review_bundle_path"
  chmod 600 "$review_bundle_path"
  write_active_workspace "$workspace"
  validate_workspace "$workspace"
  trap - ERR
  unset START_WORKSPACE_PATH

  log "Workspace:    $workspace"
  log "Branch:       $branch"
  log "AGit topic:   $topic"
  log "Pull request: $pr_url"
  log "Review bundle: $review_bundle_path"
  log "Resume:       $PROGRAM_NAME resume $workspace"
}

encode_push_option_string() {
  local value=$1
  printf '{base64}'
  printf '%s' "$value" | base64 | tr -d '\r\n'
}

encode_push_option_file() {
  local path=$1
  printf '{base64}'
  base64 <"$path" | tr -d '\r\n'
}

absolute_file() {
  local path=$1
  local directory
  local filename

  [[ -f "$path" ]] || die "File does not exist: $path"
  directory=$(dirname "$path")
  filename=$(basename "$path")
  directory=$(canonical_directory "$directory")
  printf '%s/%s\n' "$directory" "$filename"
}

validate_submission_text() {
  local title=$1
  local body_file=$2
  local title_bytes
  local body_bytes

  [[ -n "$title" ]] || die "Commit title must not be empty."
  [[ "$title" != *$'\n'* && "$title" != *$'\r'* ]] \
    || die "Commit title must be one line."
  title_bytes=$(printf '%s' "$title" | wc -c | tr -d '[:space:]')
  ((title_bytes <= MAX_TITLE_BYTES)) \
    || die "Commit title must be at most ${MAX_TITLE_BYTES} bytes."

  body_bytes=$(wc -c <"$body_file" | tr -d '[:space:]')
  ((body_bytes > 0)) || die "Pull-request body must not be empty."
  ((body_bytes <= MAX_BODY_BYTES)) \
    || die "Pull-request body must be at most ${MAX_BODY_BYTES} bytes."
}

pr_number_from_url() {
  local url=$1
  local number

  [[ "$url" =~ ^${CODEBERG_REPO_URL}/pulls/([1-9][0-9]*)$ ]] || return 1
  number=${BASH_REMATCH[1]}
  printf '%s\n' "$number"
}

pr_url_from_push_output() {
  local output_file=$1
  grep -Eo "${CODEBERG_REPO_URL}/pulls/[1-9][0-9]*" "$output_file" \
    | tail -n 1
}

find_open_pr_number_by_topic_and_sha() {
  local topic=$1
  local head_sha=$2
  local pulls_json
  local matches

  pulls_json=$(fetch_open_pull_requests_json)
  matches=$(jq -r \
    --arg repository "$REPOSITORY" \
    --arg topic "oibot/${topic}" \
    --arg sha "$head_sha" \
    --arg base "$BASE_BRANCH" \
    '[.[] | select(.user.login == "oibot" and .head.repo.full_name == $repository and .head.label == $topic and .head.sha == $sha and .base.ref == $base and (.flow // 0) == 1)] | if length == 1 then .[0].number else empty end' \
    <<<"$pulls_json")
  [[ "$matches" =~ ^[1-9][0-9]*$ ]] || return 1
  printf '%s\n' "$matches"
}

wait_for_pull_request_head() {
  local pr_number=$1
  local topic=$2
  local head_sha=$3
  local pr_json

  for _ in 1 2 3 4 5; do
    pr_json=$(fetch_pull_request_json "$pr_number")
    if [[ "$(jq -r '.head.sha // ""' <<<"$pr_json")" == "$head_sha" ]]; then
      assert_revisable_pull_request "$pr_json" "$pr_number" "$topic"
      printf '%s\n' "$(jq -r '.html_url' <<<"$pr_json")"
      return 0
    fi
    sleep 1
  done
  return 1
}

push_agit_change() {
  local workspace=$1
  local mode=$2
  local topic=$3
  local title=$4
  local body_file=$5
  local existing_pr_number=${6:-}
  local output_file
  local refspec
  local encoded_title
  local encoded_body
  local head_sha
  local pr_url=""
  local pr_number=""

  require_command base64
  refspec="HEAD:refs/for/${BASE_BRANCH}/${topic}"
  output_file="$workspace/.git/tobserver-agent-push.$$"
  head_sha=$(cd "$workspace" && git rev-parse HEAD)

  if [[ "$mode" == "new" ]]; then
    encoded_title=$(encode_push_option_string "$title")
    encoded_body=$(encode_push_option_file "$body_file")
    if ! (
      cd "$workspace"
      git push --porcelain origin "$refspec" \
        -o "title=${encoded_title}" \
        -o "description=${encoded_body}"
    ) >"$output_file" 2>&1; then
      cat "$output_file" >&2
      rm -f "$output_file"
      die "AGit submission failed. The workspace was preserved."
    fi
  else
    if ! (cd "$workspace" && git push --porcelain origin "$refspec") \
      >"$output_file" 2>&1; then
      cat "$output_file" >&2
      rm -f "$output_file"
      die "AGit revision failed. The workspace was preserved."
    fi
  fi

  cat "$output_file" >&2
  pr_url=$(pr_url_from_push_output "$output_file" || true)
  rm -f "$output_file"

  if [[ "$mode" == "revision" ]]; then
    pr_number=$existing_pr_number
  elif [[ -n "$pr_url" ]]; then
    pr_number=$(pr_number_from_url "$pr_url") \
      || die "Codeberg returned an unexpected pull-request URL."
  else
    for _ in 1 2 3 4 5; do
      if pr_number=$(find_open_pr_number_by_topic_and_sha "$topic" "$head_sha"); then
        break
      fi
      sleep 1
    done
  fi

  [[ "$pr_number" =~ ^[1-9][0-9]*$ ]] \
    || die "Could not identify the AGit pull request. The workspace was preserved."
  pr_url=$(wait_for_pull_request_head "$pr_number" "$topic" "$head_sha") \
    || die "Codeberg did not expose the expected pull-request head. The workspace was preserved."
  printf '%s\n' "$pr_url"
}

mark_submitted() {
  local workspace=$1
  local pr_url=$2
  local state_file
  local temporary

  state_file=$(state_file_for "$workspace")
  temporary="${state_file}.tmp.$$"
  jq --arg prUrl "$pr_url" \
    --arg submittedAt "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" \
    '.status = "submitted" | .prUrl = $prUrl | .submittedAt = $submittedAt' \
    "$state_file" >"$temporary"
  chmod 600 "$temporary"
  mv -f "$temporary" "$state_file"
}

submit_workspace() {
  local workspace=$1
  local title=$2
  local body_file=$3
  local state_file
  local mode
  local topic
  local initial_head
  local pr_number=""
  local pr_json
  local branch
  local new_commits
  local changes
  local pr_url

  body_file=$(absolute_file "$body_file")
  validate_submission_text "$title" "$body_file"

  state_file=$(state_file_for "$workspace")
  mode=$(jq -r '.mode' "$state_file")
  topic=$(jq -er '.topic' "$state_file") \
    || die "Workspace is missing its AGit topic."
  initial_head=$(jq -er '.initialHead' "$state_file") \
    || die "Workspace is missing its initial Git revision."
  branch=$(cd "$workspace" && git symbolic-ref --quiet --short HEAD)

  if [[ "$mode" == "revision" ]]; then
    pr_number=$(jq -er '.pullRequestNumber' "$state_file") \
      || die "Revision workspace is missing its pull-request number."
    pr_json=$(fetch_pull_request_json "$pr_number")
    assert_revisable_pull_request "$pr_json" "$pr_number" "$topic"
    [[ "$(jq -r '.head.sha' <<<"$pr_json")" == "$initial_head" ]] \
      || die "Pull request #$pr_number advanced after this workspace was created."
  fi

  run_checks "$workspace"
  changes=$(cd "$workspace" && git status --porcelain)

  if [[ -n "$changes" ]]; then
    (
      cd "$workspace"
      git add -A
      git diff --cached --check
      git commit --quiet -m "$title"
    )
  fi

  new_commits=$(cd "$workspace" && git rev-list --count "${initial_head}..HEAD")
  ((new_commits > 0)) || die "There are no new changes to submit."
  [[ -z "$(cd "$workspace" && git status --porcelain)" ]] \
    || die "The workspace changed during submission; it was preserved."
  [[ "$branch" == "$(branch_for_topic "$topic")" ]] \
    || die "The active branch no longer matches the AGit topic."

  pr_url=$(push_agit_change "$workspace" "$mode" "$topic" "$title" "$body_file" "$pr_number")
  mark_submitted "$workspace" "$pr_url"
  clear_active_workspace "$workspace"
  rm -rf -- "$workspace"

  log "Pull request: $pr_url"
  if [[ "$mode" == "revision" ]]; then
    log "Existing pull request updated."
  fi
  log "Successful workspace cleaned: $workspace"
}

cleanup_workspace() {
  local workspace=$1
  local basename

  workspace=$(canonical_directory "$workspace")
  validate_workspace "$workspace"
  basename=$(basename "$workspace")
  [[ "$basename" == tobserver-agent.* ]] \
    || die "Refusing to clean a path without the tobserver-agent.* workspace name."

  clear_active_workspace "$workspace"
  rm -rf -- "$workspace"
  log "Removed workspace: $workspace"
}

main() {
  local command=${1:-}
  local workspace=""
  local title=""
  local body_file=""

  [[ -n "$command" ]] || {
    usage
    exit 1
  }
  shift

  case "$command" in
    start)
      (($# == 1)) || die "start requires exactly one task description."
      start_workspace "$1"
      ;;
    inspect)
      (($# == 1)) || die "inspect requires exactly one pull-request number."
      inspect_pull_request "$1"
      ;;
    revise)
      (($# == 1)) || die "revise requires exactly one pull-request number."
      revise_pull_request "$1"
      ;;
    resume)
      (($# == 1)) || die "resume requires exactly one workspace path."
      resume_workspace "$1"
      ;;
    status)
      (($# <= 1)) || die "status accepts at most one workspace path."
      workspace=$(resolve_workspace "${1:-}")
      show_status "$workspace"
      ;;
    check)
      (($# <= 1)) || die "check accepts at most one workspace path."
      workspace=$(resolve_workspace "${1:-}")
      run_checks "$workspace"
      ;;
    submit)
      while (($# > 0)); do
        case "$1" in
          --title)
            (($# >= 2)) || die "--title requires a value."
            title=$2
            shift 2
            ;;
          --body-file)
            (($# >= 2)) || die "--body-file requires a value."
            body_file=$2
            shift 2
            ;;
          --*)
            die "Unknown submit option: $1"
            ;;
          *)
            [[ -z "$workspace" ]] || die "submit accepts at most one workspace path."
            workspace=$1
            shift
            ;;
        esac
      done
      [[ -n "$title" ]] || die "submit requires --title."
      [[ -n "$body_file" ]] || die "submit requires --body-file."
      workspace=$(resolve_workspace "$workspace")
      submit_workspace "$workspace" "$title" "$body_file"
      ;;
    cleanup)
      (($# == 1)) || die "cleanup requires exactly one workspace path."
      cleanup_workspace "$1"
      ;;
    help | --help | -h)
      usage
      ;;
    *)
      usage >&2
      die "Unknown command: $command"
      ;;
  esac
}

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