#!/usr/bin/env ysh

# SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
#
# SPDX-License-Identifier: CC0-1.0

if (len(ARGV) !== 0) {
  echo "Usage: $0" >&2
  exit 2
}

const collection = 'sh.tangled.repo.artifact'
const maximum_blob_size = 50 * 1024 * 1024
const maximum_scan_limit = 10000
const targets = [
  {os: 'linux', arch: 'amd64', extension: '', pack: true, static: true},
  {os: 'linux', arch: 'arm64', extension: '', pack: true, static: true},
  {os: 'darwin', arch: 'amd64', extension: '', pack: false, static: false},
  {os: 'darwin', arch: 'arm64', extension: '', pack: false, static: false},
  {os: 'windows', arch: 'amd64', extension: '.exe', pack: false, static: false},
  {os: 'freebsd', arch: 'amd64', extension: '', pack: false, static: true},
]
const pds = get(ENV, 'LUNE_RELEASE_PDS', 'https://sequeste.red')
const identifier = get(ENV, 'LUNE_RELEASE_IDENTIFIER', 'secluded.site')
const password_file = get(ENV, 'LUNE_RELEASE_APP_PASSWORD_FILE', '')
const scan_limit_text = get(ENV, 'LUNE_RELEASE_RECORD_SCAN_LIMIT', '100')

if (password_file === '') {
  echo 'LUNE_RELEASE_APP_PASSWORD_FILE is required' >&2
  exit 1
}
if ! test -s "$password_file" {
  echo "App password file is missing or empty: $password_file" >&2
  exit 1
}
if (scan_limit_text !~ '^[0-9]+$') {
  echo 'LUNE_RELEASE_RECORD_SCAN_LIMIT must be a positive integer' >&2
  exit 1
}
const scan_limit = int(scan_limit_text)
if (scan_limit < 1 or scan_limit > maximum_scan_limit) {
  echo "LUNE_RELEASE_RECORD_SCAN_LIMIT must be between 1 and $maximum_scan_limit" >&2
  exit 1
}
if (pds !~ '^https://') {
  echo 'LUNE_RELEASE_PDS must use HTTPS' >&2
  exit 1
}
if (pds ~ '/$') {
  echo 'LUNE_RELEASE_PDS must not end with a slash' >&2
  exit 1
}

const required_environment = [
  'TANGLED_PIPELINE_KIND',
  'TANGLED_REF',
  'TANGLED_REF_NAME',
  'TANGLED_REF_TYPE',
  'TANGLED_SHA',
  'TANGLED_REPO_DID',
  'TANGLED_REPO_REPO_DID',
]
for name in (required_environment) {
  if (get(ENV, name, '') === '') {
    echo "$name is required" >&2
    exit 1
  }
}

const tag = ENV.TANGLED_REF_NAME
if (ENV.TANGLED_PIPELINE_KIND !== 'push') {
  echo 'Releases require a push pipeline' >&2
  exit 1
}
if (ENV.TANGLED_REF_TYPE !== 'tag') {
  echo 'Releases require a tag ref' >&2
  exit 1
}
if (ENV.TANGLED_REF !== "refs/tags/$tag") {
  echo "TANGLED_REF does not match TANGLED_REF_NAME: $[ENV.TANGLED_REF]" >&2
  exit 1
}
if (tag !~ '^v.+$') {
  echo "Release tag must start with v: $tag" >&2
  exit 1
}

const work = $(mktemp -d)
trap --add EXIT {
  rm -rf -- "$work"
}
chmod 0700 "$work"

const tag_object = $(git rev-parse --verify "$tag^{tag}")
if (tag_object !== ENV.TANGLED_SHA) {
  echo 'The pushed tag object does not match TANGLED_SHA' >&2
  exit 1
}
const release_commit = $(git rev-parse --verify "$tag^{}")
const checkout_commit = $(git rev-parse --verify HEAD)
if (release_commit !== checkout_commit) {
  echo 'The annotated tag does not resolve to the checked-out commit' >&2
  exit 1
}
const tag_bytes = $(printf '%s' "$tag_object" | xxd -r -p | base64 -w 0 | tr -d '=')
if (len(tag_bytes) !== 27) {
  echo 'The annotated tag object ID is not a 20-byte SHA-1 value' >&2
  exit 1
}

const auth_config = "$work/curl-auth.conf"
const password = $(cat -- "$password_file")
var session_request = {
  identifier: identifier,
  password: password,
}
json write (session_request) > "$work/session-request.json"
chmod 0600 "$work/session-request.json"

const curl_arguments = [
  '--silent',
  '--show-error',
  '--fail-with-body',
  '--connect-timeout', '10',
  '--max-time', '120',
]

echo 'Authenticating to the release PDS ...'
curl @curl_arguments \
  --request POST \
  --header 'Content-Type: application/json' \
  --data-binary "@$work/session-request.json" \
  "$pds/xrpc/com.atproto.server.createSession" \
  --output "$work/session.json"
var session = null
try {
  json read (&session) < "$work/session.json" 2>/dev/null
}
if (_status !== 0) {
  echo 'PDS returned an invalid session response' >&2
  exit 1
}
const publisher_did = session.did
const access_token = session.accessJwt
if (publisher_did !== ENV.TANGLED_REPO_DID) {
  echo 'Authenticated PDS account does not match TANGLED_REPO_DID' >&2
  exit 1
}
printf 'header = "Authorization: Bearer %s"\n' "$access_token" > "$auth_config"
chmod 0600 "$auth_config"

proc scan_release (output_path) {
  var matches = []
  var cursor = ''
  var scanned = 0
  while (scanned < scan_limit) {
    var page_size = scan_limit - scanned
    if (page_size > 100) {
      setvar page_size = 100
    }
    var cursor_arguments = []
    if (cursor !== '') {
      setvar cursor_arguments = ['--data-urlencode', "cursor=$cursor"]
    }
    curl @curl_arguments \
      --retry 2 \
      --retry-all-errors \
      --get \
      --data-urlencode "repo=$publisher_did" \
      --data-urlencode "collection=$collection" \
      --data-urlencode "limit=$page_size" \
      @cursor_arguments \
      "$pds/xrpc/com.atproto.repo.listRecords" \
      --output "$work/records-page.json"

    var page = null
    json read (&page) < "$work/records-page.json"
    for record in (page.records) {
      setvar scanned += 1
      var value = record.value
      var record_tag = get(value, 'tag', {})
      var record_tag_bytes = get(record_tag, '$bytes', '')
      if (
        get(value, 'repoDid', '') === ENV.TANGLED_REPO_REPO_DID and
        (record_tag_bytes === tag_bytes or record_tag_bytes === "$tag_bytes=")
      ) {
        call matches->append(record)
      }
    }
    if (len(page.records) === 0 or scanned >= scan_limit) {
      break
    }
    setvar cursor = get(page, 'cursor', '')
    if (cursor === '') {
      break
    }
  }
  json write (matches) > "$output_path"
}

echo "Checking the newest $scan_limit artifact records for $tag ..."
scan_release "$work/preflight-records.json"
var preflight_records = null
json read (&preflight_records) < "$work/preflight-records.json"
if (len(preflight_records) !== 0) {
  echo "This release already has published artifacts in the newest $scan_limit records" >&2
  echo 'Create and push a new tag instead of overwriting a release' >&2
  exit 1
}

const staging = "$work/dist"
install -d -m 0755 "$staging"
for target in (targets) {
  const suffix = "$[target.os]-$[target.arch]$[target.extension]"
  const artifact = "$staging/lune-$tag-$suffix"
  var ldflags = "-s -w -buildid= -X main.version=$tag"
  if (target.static) {
    setvar ldflags = "-d $ldflags -extldflags=-static"
  }
  echo "Building $[target.os]/$[target.arch] ..."
  env \
    CGO_ENABLED=0 \
    GOOS="$[target.os]" \
    GOARCH="$[target.arch]" \
    GOTOOLCHAIN=go1.25.5+auto \
    go build \
      -trimpath \
      -tags 'netgo,osusergo,static_build' \
      -ldflags "$ldflags" \
      -o "$artifact" \
      .
  if (target.pack) {
    echo "Packing $[target.os]/$[target.arch] ..."
    upx --best --lzma --quiet "$artifact"
    upx --test --quiet "$artifact"
  }
}

for target in (targets) {
  const artifact = "$staging/lune-$tag-$[target.os]-$[target.arch]$[target.extension]"
  if ! test -s "$artifact" {
    echo "Expected artifact is missing or empty: $artifact" >&2
    exit 1
  }
  const size = int($(wc -c < "$artifact"))
  if (size > maximum_blob_size) {
    echo "Artifact exceeds the 50 MiB lexicon limit: $artifact" >&2
    exit 1
  }
}

var records = []
const created_at = $(date --utc '+%Y-%m-%dT%H:%M:%SZ')
for target in (targets) {
  const name = "lune-$tag-$[target.os]-$[target.arch]$[target.extension]"
  const artifact = "$staging/$name"
  const size = int($(wc -c < "$artifact"))
  echo "Uploading $name ..."
  curl @curl_arguments \
    --config "$auth_config" \
    --request POST \
    --header 'Content-Type: application/octet-stream' \
    --data-binary "@$artifact" \
    "$pds/xrpc/com.atproto.repo.uploadBlob" \
    --output "$work/blob.json"
  var upload = null
  json read (&upload) < "$work/blob.json"
  if (upload.blob.size !== size) {
    echo "PDS returned invalid blob metadata for $name" >&2
    exit 1
  }
  call records->append({
    '$type': collection,
    name: name,
    repoDid: ENV.TANGLED_REPO_REPO_DID,
    tag: {'$bytes': tag_bytes},
    createdAt: created_at,
    artifact: upload.blob,
  })
}

curl @curl_arguments \
  --retry 2 \
  --retry-all-errors \
  --get \
  --data-urlencode "did=$publisher_did" \
  "$pds/xrpc/com.atproto.sync.getLatestCommit" \
  --output "$work/latest-commit.json"
var latest_commit = null
json read (&latest_commit) < "$work/latest-commit.json"

# Recheck after the expensive work. Reading the commit before the scan makes
# swapCommit reject any concurrent write from this point through applyWrites.
echo 'Rechecking publication state before creating records ...'
scan_release "$work/prewrite-records.json"
var prewrite_records = null
json read (&prewrite_records) < "$work/prewrite-records.json"
if (len(prewrite_records) !== 0) {
  echo 'Another pipeline published this release while artifacts were being prepared' >&2
  echo 'Create and push a new tag instead of overwriting a release' >&2
  exit 1
}

var writes = []
for record in (records) {
  call writes->append({
    '$type': 'com.atproto.repo.applyWrites#create',
    collection: collection,
    value: record,
  })
}
var apply_request = {
  repo: publisher_did,
  swapCommit: latest_commit.cid,
  writes: writes,
}
json write (apply_request) > "$work/apply-request.json"

echo 'Publishing artifact records ...'
var http_status = ''
try {
  setvar http_status = $(curl @curl_arguments \
    --config "$auth_config" \
    --request POST \
    --header 'Content-Type: application/json' \
    --data-binary "@$work/apply-request.json" \
    --output "$work/apply-response.json" \
    --write-out '%{http_code}' \
    "$pds/xrpc/com.atproto.repo.applyWrites")
}
const apply_curl_status = _status

# A response can be lost after the PDS commits. Reconcile before deciding
# whether the publication failed; never blindly repeat applyWrites.
echo 'Confirming published artifact records ...'
scan_release "$work/published-records.json"
var published_records = null
json read (&published_records) < "$work/published-records.json"
var exact_publication = len(published_records) === len(records)
for expected in (records) {
  var match_count = 0
  for published in (published_records) {
    var value = published.value
    var value_tag = get(value, 'tag', {})
    var value_tag_bytes = get(value_tag, '$bytes', '')
    if (value_tag_bytes === "$tag_bytes=") {
      setvar value.tag = {'$bytes': tag_bytes}
    }
    if (value === expected) {
      setvar match_count += 1
    }
  }
  if (match_count !== 1) {
    setvar exact_publication = false
  }
}

if (exact_publication) {
  echo "Published $tag with $[len(records)] artifacts"
  exit 0
}
if (len(published_records) !== 0) {
  echo 'Publication left an ambiguous or partial release state' >&2
  echo 'Inspect the PDS records and create a new tag before retrying' >&2
  exit 1
}
if (apply_curl_status !== 0 or http_status !~ '^2[0-9][0-9]$') {
  echo "PDS did not publish the release (curl status $apply_curl_status, HTTP $http_status)" >&2
  echo 'No matching artifact records were found; this tag may be retried after diagnosing the failure' >&2
  exit 1
}

echo 'PDS accepted applyWrites but the expected records were not found' >&2
exit 1
