#!/usr/bin/env bash

:<<'DOC'
Forgejo API client for releases, pull requests, and label management.
Depends on manifest.api.sh for default owner/repo values.
DOC

import \
  "$SHELLOPS_DIR/lib/manifest.api.sh" \
  "$SHELLOPS_DIR/lib/http.api.sh"

__forgejo_api__BASE="${FORGEJO_API_BASE:-https://creature-workshop.kernelle-soft.com}"
__forgejo_api__RELEASE_ENDPOINT="/api/v1/repos/:owner:/:repo:/releases"
__forgejo_api__UPLOAD_ENDPOINT="/api/v1/repos/:owner:/:repo:/releases/:release_id:/assets?name=:filename:"
__forgejo_api__PULL_REQUESTS_ENDPOINT="/api/v1/repos/:owner:/:repo:/pulls"
__forgejo_api__PULL_REQUEST_ENDPOINT="/api/v1/repos/:owner:/:repo:/pulls/:pr_number:"
__forgejo_api__LABELS_ENDPOINT="/api/v1/repos/:owner:/:repo:/labels"
__forgejo_api__LABEL_ENDPOINT="/api/v1/repos/:owner:/:repo:/labels/:label_id:"
__forgejo_api__ISSUE_COMMENTS_ENDPOINT="/api/v1/repos/:owner:/:repo:/issues/:issue_number:/comments"
__forgejo_api__COMMENT_PAGE_LIMIT=50
__forgejo_api__ORG_REPOS_ENDPOINT="/api/v1/orgs/:org:/repos"
__forgejo_api__RAW_FILE_ENDPOINT="/api/v1/repos/:owner:/:repo:/raw/:filepath:"

:<<'DOC'
Resolves the Forgejo API base URL from FORGEJO_API_BASE (same default as
internal requests use).

Usage: forgejo_api_base
DOC
function forgejo_api_base() {
  printf '%s\n' "$__forgejo_api__BASE"
}

:<<'DOC'
Creates default release options populated with owner/repo from manifest.json.

Usage: new_release_options
DOC
function new_release_options() {
  jq_set "{}" \
    '.owner'      "$(manifest_get org)" \
    '.repo'       "$(manifest_get repo)" \
    '.tag_name'   "" \
    '.name'       "" \
    '.body'       "" \
    '.prerelease' false \
    '.token'      ""
}

:<<'DOC'
Creates default upload options populated with owner/repo from manifest.json.

Usage: new_upload_options
DOC
function new_upload_options() {
  jq_set "{}" \
    '.owner'        "$(manifest_get org)" \
    '.repo'         "$(manifest_get repo)" \
    '.release_id'   "" \
    '.filename'     "" \
    '.filepath'     "" \
    '.token'        ""
}

:<<'DOC'
Creates default pull request options populated with owner/repo from manifest.json.

Usage: new_pull_request_options
DOC
function new_pull_request_options() {
  jq_set "{}" \
    '.owner'     "$(manifest_get org)" \
    '.repo'      "$(manifest_get repo)" \
    '.title'     "" \
    '.head'      "" \
    '.base'      "main" \
    '.body'      "" \
    '.assignees' "[]" \
    '.labels'    "[]" \
    '.token'     ""
}

function __forgejo_api__release_endpoint() {
  local options="$1"
  local endpoint owner repo

  owner="$(jq_get "$options" '.owner')"
  if [[ -z "$owner" ]]; then
    error "Cannot format release endpoint. No owner is defined: $(jq_dump "$options" 'del(.token)')"
    return 1
  fi

  repo="$(jq_get "$options" '.repo')"
  if [[ -z "$repo" ]]; then
    error "Cannot format release endpoint. No repo is defined: $(jq_dump "$options" 'del(.token)')"
    return 1
  fi

  endpoint="${__forgejo_api__BASE}${__forgejo_api__RELEASE_ENDPOINT}"
  endpoint="${endpoint//:owner:/$owner}"
  endpoint="${endpoint//:repo:/$repo}"
  echo "$endpoint"
}

function __forgejo_api__release_body() {
  local options="$1"
  jq_dump "$options" "{tag_name,name,body,prerelease}"
}

function __forgejo_api__upload_endpoint() {
  local options="$1"
  local endpoint owner repo release_id filename

  owner="$(jq_get "$options" '.owner' "$(manifest_get org)")"
  repo="$(jq_get "$options" '.repo' "$(manifest_get repo)")"
  release_id="$(jq_get "$options" '.release_id')"
  filename="$(jq_get "$options" '.filename')"

  endpoint="${__forgejo_api__BASE}${__forgejo_api__UPLOAD_ENDPOINT}"
  endpoint="${endpoint//:owner:/$owner}"
  endpoint="${endpoint//:repo:/$repo}"
  endpoint="${endpoint//:release_id:/$release_id}"
  endpoint="${endpoint//:filename:/$filename}"
  echo "$endpoint"
}

function __forgejo_api__pull_requests_endpoint() {
  local options owner repo endpoint
  options="$1"

  owner="$(jq_get "$options" '.owner' "$(manifest_get org)")"
  repo="$(jq_get "$options" '.repo' "$(manifest_get repo)")"

  if [[ -z "$owner" || -z "$repo" ]]; then
    error "Cannot create pull requests endpoint. Missing owner or repo: $(jq_dump "$options" 'del(.token)')"
    return 1
  fi

  endpoint="${__forgejo_api__BASE}${__forgejo_api__PULL_REQUESTS_ENDPOINT}"
  endpoint="${endpoint//:owner:/$owner}"
  endpoint="${endpoint//:repo:/$repo}"

  echo "$endpoint"
}

function __forgejo_api__pull_request_endpoint() {
  local options owner repo pr_number endpoint
  options="$1"

  owner="$(jq_get "$options" '.owner' "$(manifest_get org)")"
  repo="$(jq_get "$options" '.repo' "$(manifest_get repo)")"
  pr_number="$(jq_get "$options" '.pr_number')"

  if [[ -z "$pr_number" ]]; then
    error "Cannot create PR endpoint. Pull request number is undefined"
    return 1
  fi

  endpoint="${__forgejo_api__BASE}${__forgejo_api__PULL_REQUEST_ENDPOINT}"
  endpoint="${endpoint//:owner:/$owner}"
  endpoint="${endpoint//:repo:/$repo}"
  endpoint="${endpoint//:pr_number:/$pr_number}"

  echo "$endpoint"
}

function __forgejo_api__pull_request_body() {
  local options="$1"
  jq_dump "$options" '{title,head,base,body,assignees,labels} | with_entries(select(.value != "" and .value != null))'
}

:<<'DOC'
Fetches pull request metadata from the Forgejo API.

Usage: get_pull_request '{"pr_number":1, "token":"xxx"}'

  owner and repo fall back to manifest.json if not provided.
DOC
function get_pull_request() {
  local options="$1"
  local token endpoint http_opts

  token="$(__forgejo_api__validate_token "$options")" || return 1
  http_opts="$(__forgejo_api__http_opts "$token")"

  endpoint="$(__forgejo_api__pull_request_endpoint "$options")"
  if [[ -z "$endpoint" ]]; then
    error "Cannot create pull request endpoint for options: $(jq_dump "$options" 'del(.token)')"
    return 1
  fi

  http_get "$endpoint" "$http_opts"
}

:<<'DOC'
Creates a pull request via the Forgejo API.

Usage: forgejo_create_pull_request "$pull_request_options"

  pull_request_options must include .title, .head, .base, and .token.
DOC
function forgejo_create_pull_request() {
  local options="$1"
  local token endpoint body http_opts

  token="$(__forgejo_api__validate_token "$options")" || return 1
  endpoint="$(__forgejo_api__pull_requests_endpoint "$options")" || return 1
  body="$(__forgejo_api__pull_request_body "$options")"
  http_opts="$(__forgejo_api__http_opts "$token")"

  http_post "$endpoint" "$body" "$http_opts"
}

:<<'DOC'
Creates a release via the Forgejo API.

Usage: create_release "\$release_options"
DOC
function create_release() {
  local options="$1"
  local token endpoint body http_opts

  token="$(__forgejo_api__validate_token "$options")" || return 1
  http_opts="$(__forgejo_api__http_opts "$token")"

  endpoint="$(__forgejo_api__release_endpoint "$options")"
  if [[ -z "$endpoint" ]]; then
    error "Cannot create release endpoint for options: $(jq_dump "$options" 'del(.token)')"
    return 1
  fi

  body="$(__forgejo_api__release_body "$options")"
  if [[ -z "$body" ]]; then
    error "Cannot create release body for options: $(jq_dump "$options" 'del(.token)')"
    return 1
  fi

  http_post "$endpoint" "$body" "$http_opts"
}

:<<'DOC'
Uploads a file as a release asset on Forgejo.

Usage: upload_artifact "\$upload_options"
DOC
function upload_artifact() {
  local options="$1"
  local filepath filename token endpoint http_opts

  filepath="$(jq_get "$options" '.filepath')"
  filename="$(jq_get "$options" '.filename')"
  if [[ -z "$filepath" || -z "$filename" ]]; then
    error "Incomplete file upload information. name: '$filename', path: '$filepath'"
    return 1
  fi

  token="$(__forgejo_api__validate_token "$options")" || return 1

  endpoint="$(__forgejo_api__upload_endpoint "$options")"
  if [[ -z "$endpoint" ]]; then
    error "Cannot create upload endpoint for options: $(jq_dump "$options" 'del(.token)')"
    return 1
  fi

  http_opts="$(__forgejo_api__http_opts "$token")"
  http_opts="$(jq_set "$http_opts" '.form_field' 'attachment')"

  http_upload "$endpoint" "$filepath" "$http_opts"
}

# ── Comment operations ───────────────────────────────────────────────

function __forgejo_api__comments_endpoint() {
  local options="$1"
  local owner repo issue_number endpoint

  owner="$(jq_get "$options" '.owner' "$(manifest_get org)")"
  repo="$(jq_get "$options" '.repo' "$(manifest_get repo)")"
  issue_number="$(jq_get "$options" '.issue_number')"

  if [[ -z "$issue_number" ]]; then
    error "Cannot create comments endpoint. Issue number is undefined"
    return 1
  fi

  endpoint="${__forgejo_api__BASE}${__forgejo_api__ISSUE_COMMENTS_ENDPOINT}"
  endpoint="${endpoint//:owner:/$owner}"
  endpoint="${endpoint//:repo:/$repo}"
  endpoint="${endpoint//:issue_number:/$issue_number}"
  echo "$endpoint"
}

:<<'DOC'
Lists all comments on a Forgejo issue/PR, handling pagination.

PRs are issues in Forgejo's API, so pr_number and issue_number are
interchangeable.

Usage: forgejo_list_comments "$comment_options"

  comment_options must include .issue_number and .token
DOC
function forgejo_list_comments() {
  local options="$1"
  local token endpoint http_opts
  local page=1 limit="$__forgejo_api__COMMENT_PAGE_LIMIT" all_comments="[]"

  token="$(__forgejo_api__validate_token "$options")" || return 1
  endpoint="$(__forgejo_api__comments_endpoint "$options")" || return 1
  http_opts="$(__forgejo_api__http_opts "$token")"

  while true; do
    local result count
    result="$(http_get "${endpoint}?page=${page}&limit=${limit}" "$http_opts")" || return 1
    count="$(jq_len "$result")"
    all_comments="$(jq_concat "$all_comments" "$result")"

    if (( count < limit )); then
      break
    fi
    (( page++ ))
  done

  echo "$all_comments"
}

:<<'DOC'
Creates a comment on a Forgejo issue/PR.

Usage: forgejo_create_comment "$comment_options" "$body"

  comment_options must include .issue_number and .token
DOC
function forgejo_create_comment() {
  local options="$1" body="$2"
  local token endpoint http_opts payload

  token="$(__forgejo_api__validate_token "$options")" || return 1
  endpoint="$(__forgejo_api__comments_endpoint "$options")" || return 1
  http_opts="$(__forgejo_api__http_opts "$token")"
  payload="$(jq_set '{}' '.body' "$body")"

  http_post "$endpoint" "$payload" "$http_opts"
}

# ── Label operations ─────────────────────────────────────────────────

:<<'DOC'
Creates default label options populated with owner/repo from manifest.json.

Usage: new_label_options
DOC
function new_label_options() {
  jq_set "{}" \
    '.owner' "$(manifest_get org)" \
    '.repo'  "$(manifest_get repo)" \
    '.token' ""
}

function __forgejo_api__labels_endpoint() {
  local options="$1"
  local owner repo endpoint

  owner="$(jq_get "$options" '.owner')"
  repo="$(jq_get "$options" '.repo')"

  if [[ -z "$owner" || -z "$repo" ]]; then
    error "Cannot format labels endpoint. Missing owner or repo: $(jq_dump "$options" 'del(.token)')"
    return 1
  fi

  endpoint="${__forgejo_api__BASE}${__forgejo_api__LABELS_ENDPOINT}"
  endpoint="${endpoint//:owner:/$owner}"
  endpoint="${endpoint//:repo:/$repo}"
  echo "$endpoint"
}

function __forgejo_api__label_endpoint() {
  local options="$1"
  local owner repo label_id endpoint

  owner="$(jq_get "$options" '.owner')"
  repo="$(jq_get "$options" '.repo')"
  label_id="$(jq_get "$options" '.label_id')"

  if [[ -z "$owner" || -z "$repo" || -z "$label_id" ]]; then
    error "Cannot format label endpoint. Missing owner, repo, or label_id: $(jq_dump "$options" 'del(.token)')"
    return 1
  fi

  endpoint="${__forgejo_api__BASE}${__forgejo_api__LABEL_ENDPOINT}"
  endpoint="${endpoint//:owner:/$owner}"
  endpoint="${endpoint//:repo:/$repo}"
  endpoint="${endpoint//:label_id:/$label_id}"
  echo "$endpoint"
}

function __forgejo_api__validate_token() {
  local options="$1"
  local token

  token="$(jq_get "$options" '.token')"
  if [[ -z "$token" ]]; then
    error "No authorization token set"
    return 1
  fi

  echo "$token"
}

function __forgejo_api__http_opts() {
  jq_set '{}' '.auth' "token $1"
}

:<<'DOC'
Lists all repository labels, handling pagination automatically.

Usage: forgejo_list_labels "$label_options"
DOC
function forgejo_list_labels() {
  local options="$1"
  local token endpoint http_opts

  token="$(__forgejo_api__validate_token "$options")" || return 1
  endpoint="$(__forgejo_api__labels_endpoint "$options")" || return 1
  http_opts="$(__forgejo_api__http_opts "$token")"

  local page=1 limit=50 all_labels="[]"

  while true; do
    local result query
    query="page=${page}&limit=${limit}"
    result="$(http_get "${endpoint}?${query}" "$http_opts")" || return 1

    local count
    count="$(jq_len "$result")"
    all_labels="$(jq_concat "$all_labels" "$result")"

    if (( count < limit )); then
      break
    fi
    (( page++ ))
  done

  echo "$all_labels"
}

:<<'DOC'
Creates a label on the repository.

Usage: forgejo_create_label "$label_options" "$label_data"

  label_data is a JSON object with: name, color, description
DOC
function forgejo_create_label() {
  local options="$1" label_data="$2"
  local token endpoint body http_opts

  token="$(__forgejo_api__validate_token "$options")" || return 1
  endpoint="$(__forgejo_api__labels_endpoint "$options")" || return 1
  http_opts="$(__forgejo_api__http_opts "$token")"
  body="$(jq_dump "$label_data" '{name, color, description}')"

  http_post "$endpoint" "$body" "$http_opts"
}

:<<'DOC'
Updates a label on the repository.

Usage: forgejo_edit_label "$label_options" "$edit_data"

  label_options must include .label_id
  edit_data is a JSON object with any of: name, color, description, is_archived
DOC
function forgejo_edit_label() {
  local options="$1" edit_data="$2"
  local token endpoint http_opts

  token="$(__forgejo_api__validate_token "$options")" || return 1
  endpoint="$(__forgejo_api__label_endpoint "$options")" || return 1
  http_opts="$(__forgejo_api__http_opts "$token")"

  http_patch "$endpoint" "$edit_data" "$http_opts"
}

# ── Repo discovery ───────────────────────────────────────────────────

:<<'DOC'
Creates default org options for repo discovery.

Usage: new_org_options
DOC
function new_org_options() {
  jq_set '{}' \
    '.org'   "$(manifest_get org)" \
    '.token' ""
}

:<<'DOC'
Lists all non-archived repositories in the organization, handling pagination.

Usage: forgejo_list_org_repos "$org_options"
DOC
function forgejo_list_org_repos() {
  local options="$1"
  local token endpoint http_opts
  local page=1 limit=50 all_repos="[]"

  token="$(__forgejo_api__validate_token "$options")" || return 1
  endpoint="$(__forgejo_api__org_repos_endpoint "$options")" || return 1
  http_opts="$(__forgejo_api__http_opts "$token")"

  while true; do
    local result filtered count
    result="$(
      http_get "${endpoint}?page=${page}&limit=${limit}" "$http_opts"
    )" || return 1

    filtered="$(jq_filter "$result" '.archived == false')"
    all_repos="$(jq_concat "$all_repos" "$filtered")"
    count="$(jq_len "$result")"

    if (( count < limit )); then
      break
    fi
    (( page++ ))
  done

  echo "$all_repos"
}

:<<'DOC'
Fetches a raw file from a repository. Returns the file content or fails
if the file does not exist.

Usage: forgejo_get_raw_file "$options" "$filepath"

  options must include .owner and .repo
DOC
function forgejo_get_raw_file() {
  local options="$1" filepath="$2"
  local token endpoint http_opts

  token="$(__forgejo_api__validate_token "$options")" || return 1
  endpoint="$(__forgejo_api__raw_file_endpoint "$options" "$filepath")" || return 1
  http_opts="$(__forgejo_api__http_opts "$token")"

  http_get "$endpoint" "$http_opts"
}

function __forgejo_api__org_repos_endpoint() {
  local options="$1"
  local org endpoint

  org="$(jq_get "$options" '.org')"
  if [[ -z "$org" ]]; then
    error "Cannot format org repos endpoint. Missing org."
    return 1
  fi

  endpoint="${__forgejo_api__BASE}${__forgejo_api__ORG_REPOS_ENDPOINT}"
  endpoint="${endpoint//:org:/$org}"
  echo "$endpoint"
}

function __forgejo_api__raw_file_endpoint() {
  local options="$1" filepath="$2"
  local owner repo endpoint

  owner="$(jq_get "$options" '.owner')"
  repo="$(jq_get "$options" '.repo')"

  if [[ -z "$owner" || -z "$repo" ]]; then
    error "Cannot format raw file endpoint. Missing owner or repo."
    return 1
  fi

  endpoint="${__forgejo_api__BASE}${__forgejo_api__RAW_FILE_ENDPOINT}"
  endpoint="${endpoint//:owner:/$owner}"
  endpoint="${endpoint//:repo:/$repo}"
  endpoint="${endpoint//:filepath:/$filepath}"
  echo "$endpoint"
}
