#!/bin/sh

# Prepare and, only on explicit request, submit the LuaCoolProp CTAN release.
# API: https://ctan.org/help/submit
# Archive rules: https://ctan.org/file/help/ctan/CTAN-upload-addendum
# CTAN requires a browsable package tree. The CTAN maintainer for LuaCoolProp
# requested that its separately generated TDS archive not be uploaded.

set -eu

fail()
{
    printf 'error: %s\n' "$*" >&2
    exit 1
}

usage()
{
    cat <<'EOF'
Usage: scripts/submit-ctan.sh [--check | --validate | --submit --yes] [--new]

  --check      Check the existing release archives locally (default).
  --validate   Send the outer ZIP to CTAN's validation endpoint only.
  --submit     Validate, then send the outer ZIP to CTAN for publication.
  --yes        Required with --submit to confirm the public submission.
  --new        Declare a new package instead of an update (exceptional).

Build LuaCoolProp beforehand with scripts/package-release.sh and a separately
obtained pgfplots-autonode TDS archive. Only the browsing ZIP is sent to CTAN.
No network request is made by --check. --validate does not submit a package.
EOF
}

need_command()
{
    command -v "$1" >/dev/null 2>&1 \
        || fail "required command not found: $1"
}

MODE=check
CONFIRMED=false
UPDATE=true
MODE_SEEN=false
STATUS_SEEN=false
while [ "$#" -gt 0 ]; do
    case $1 in
        --check|--validate|--submit)
            [ "$MODE_SEEN" = false ] || fail "choose only one of --check, --validate, and --submit"
            MODE=${1#--}
            MODE_SEEN=true
            ;;
        --yes) CONFIRMED=true ;;
        --update|--new)
            [ "$STATUS_SEEN" = false ] || fail "choose only one of --update and --new"
            STATUS_SEEN=true
            [ "$1" = --update ] && UPDATE=true || UPDATE=false
            ;;
        --help|-h) usage; exit 0 ;;
        *) usage >&2; fail "unknown option: $1" ;;
    esac
    shift
done
[ "$MODE" = submit ] || [ "$CONFIRMED" = false ] \
    || fail "--yes is only valid with --submit"
[ "$MODE" != submit ] || [ "$CONFIRMED" = true ] \
    || fail "--submit requires --yes; no upload was attempted"

for command_name in awk cmp cp dirname grep mktemp rm sed unzip zip; do
    need_command "$command_name"
done
SCRIPT_DIR=$(CDPATH= cd -P "$(dirname "$0")" && pwd) \
    || fail "could not locate the script directory"
PROJECT_DIR=$(CDPATH= cd -P "$SCRIPT_DIR/.." && pwd) \
    || fail "could not locate the project directory"
PACKAGE=luacoolprop
VERSION_FILE=$PROJECT_DIR/VERSION
CTAN_PATH=/macros/luatex/generic/luacoolprop
SUMMARY='Thermodynamic calculations and diagrams in LuaTeX via the CoolProp shared library'
DESCRIPTION='LuaCoolProp is a format-generic LuaTeX package for thermodynamic calculations and pure-fluid PH, PV, TS, HS, and PT diagrams with PGFPlots. It uses an externally installed CoolProp shared library through LuaTeX FFI and requires the independent pgfplots-autonode package. LaTeX, plain TeX, and ConTeXt frontends are provided.'
NOTE='This package was vibe-coded with the assistance of ChatGPT. AI-generated code and thermodynamic results require independent review; the README and manual warn users accordingly. Full --shell-escape is required for the LuaTeX FFI mechanism and can execute arbitrary commands with the user permissions, so only trusted documents should be compiled. CoolProp is an external native dependency and pgfplots-autonode is an independent CTAN dependency; neither is included in this upload.'
REPOSITORY=https://github.com/cjorssen/luacoolprop
BUGTRACKER=https://github.com/cjorssen/luacoolprop/issues
OUTER=$PROJECT_DIR/dist/$PACKAGE.zip
TDS=$PROJECT_DIR/dist/$PACKAGE.tds.zip
[ -f "$OUTER" ] || fail "missing $OUTER; run scripts/package-release.sh first"
[ -f "$TDS" ] || fail "missing $TDS; run scripts/package-release.sh first"
[ -f "$VERSION_FILE" ] || fail "missing $VERSION_FILE"
VERSION=$(sed -n 's/^VERSION=//p' "$VERSION_FILE")
case $VERSION in
    ''|*[!A-Za-z0-9._-]*) fail "invalid version in VERSION: $VERSION" ;;
esac

TEMP_DIR=$(mktemp -d "${TMPDIR:-/tmp}/$PACKAGE-ctan.XXXXXX") \
    || fail "could not create a temporary directory"
cleanup()
{
    rm -rf "$TEMP_DIR"
}
trap cleanup 0
trap 'exit 1' HUP INT TERM

# Verify the release artifacts and absence of common generated/private files
# before any remote request. Older releases may contain an embedded TDS ZIP;
# it is checked, then removed from the temporary CTAN upload copy below.
unzip -tqq "$OUTER" || fail "outer ZIP failed its integrity check"
unzip -tqq "$TDS" || fail "TDS ZIP failed its integrity check"
unzip -Z1 "$OUTER" >"$TEMP_DIR/outer-members" \
    || fail "could not list the outer ZIP"
unzip -Z1 "$TDS" >"$TEMP_DIR/tds-members" \
    || fail "could not list the TDS ZIP"
REQUIRED_OUTER='luacoolprop/README.md luacoolprop/luacoolprop-manual.pdf luacoolprop/VERSION luacoolprop/luacoolprop.tex'
REQUIRED_TDS='tex/generic/luacoolprop/luacoolprop.tex tex/luatex/luacoolprop/luacoolprop.lua doc/generic/luacoolprop/luacoolprop-manual.pdf source/generic/luacoolprop/luacoolprop-manual.tex'
for member in $REQUIRED_OUTER; do
    grep -Fqx "$member" "$TEMP_DIR/outer-members" \
        || fail "outer ZIP is missing $member"
done
for member in $REQUIRED_TDS; do
    grep -Fqx "$member" "$TEMP_DIR/tds-members" \
        || fail "TDS ZIP is missing $member"
done
if awk -v prefix="$PACKAGE/" -v tds="$PACKAGE.tds.zip" \
    '$0 != tds && $0 !~ ("^" prefix) { print; bad=1 } END { exit !bad }' \
    "$TEMP_DIR/outer-members" >"$TEMP_DIR/unexpected-members"; then
    fail "unexpected top-level ZIP member: $(sed -n '1p' "$TEMP_DIR/unexpected-members")"
fi
if grep -E '(^|/)(\.|AGENTS\.md($|/)|build-logs($|/)|__MACOSX($|/))|\.(aux|log|out|toc|synctex\.gz)$' \
    "$TEMP_DIR/outer-members" "$TEMP_DIR/tds-members" \
    >"$TEMP_DIR/forbidden-members"; then
    fail "unexpected generated/private ZIP member: $(sed -n '1p' "$TEMP_DIR/forbidden-members")"
fi
if grep -Fqx "$PACKAGE.tds.zip" "$TEMP_DIR/outer-members"; then
    unzip -p "$OUTER" "$PACKAGE.tds.zip" | cmp - "$TDS" \
        || fail "embedded TDS ZIP differs from the separate TDS artifact"
fi
unzip -p "$OUTER" "$PACKAGE/VERSION" | cmp - "$VERSION_FILE" \
    || fail "outer ZIP version metadata differs from VERSION"

if grep -E \
    '(^|/)(pgfplots-autonode|pgflibrarypgfplots\.autonode)' \
    "$TEMP_DIR/outer-members" "$TEMP_DIR/tds-members" >/dev/null; then
    fail "LuaCoolProp archives embed pgfplots-autonode files"
fi

# Submit a private temporary copy that contains only the browsable package
# directory. Manfred Lotz confirmed after the first upload that CTAN does not
# want LuaCoolProp's optional TDS ZIP alongside this package.
UPLOAD=$TEMP_DIR/$PACKAGE.zip
cp "$OUTER" "$UPLOAD" || fail "could not prepare the temporary CTAN archive"
if grep -Fqx "$PACKAGE.tds.zip" "$TEMP_DIR/outer-members"; then
    zip -dq "$UPLOAD" "$PACKAGE.tds.zip" \
        || fail "could not remove the TDS ZIP from the CTAN upload copy"
fi
unzip -tqq "$UPLOAD" || fail "temporary CTAN ZIP failed its integrity check"
unzip -Z1 "$UPLOAD" >"$TEMP_DIR/upload-members" \
    || fail "could not list the temporary CTAN ZIP"
if grep -Fqx "$PACKAGE.tds.zip" "$TEMP_DIR/upload-members"; then
    fail "temporary CTAN ZIP still contains the TDS archive"
fi
if awk -v prefix="$PACKAGE/" \
    '$0 !~ ("^" prefix) { print; bad=1 } END { exit !bad }' \
    "$TEMP_DIR/upload-members" >"$TEMP_DIR/unexpected-upload-members"; then
    fail "unexpected CTAN ZIP member: $(sed -n '1p' "$TEMP_DIR/unexpected-upload-members")"
fi

printf 'Local CTAN archive checks passed.\n'
printf 'Package: %s %s (%s)\n' "$PACKAGE" "$VERSION" "$( [ "$UPDATE" = true ] && printf update || printf new )"
printf 'CTAN path: %s\n' "$CTAN_PATH"
printf 'Uploader: Christophe Jorssen <christophe.jorssen@gmail.com>\n'
printf 'License: lppl1.3c; no mailing-list announcement requested\n'
printf 'Release archive: %s\n' "$OUTER"
printf 'CTAN upload: temporary browsing archive without a TDS ZIP\n'
printf 'Separate TDS archive (not uploaded): %s\n' "$TDS"
[ "$MODE" != check ] || exit 0

need_command curl
need_command python3

# --form-string avoids curl interpreting metadata beginning with @ or < as
# files. curl itself supplies the multipart Content-Type and boundary.
request()
{
    action=$1
    response=$TEMP_DIR/$action.json
    status=$(curl --silent --show-error --connect-timeout 20 --max-time 600 \
        --output "$response" --write-out '%{http_code}' \
        --form-string 'author=Christophe Jorssen' \
        --form-string "ctanPath=$CTAN_PATH" \
        --form-string "description=$DESCRIPTION" \
        --form-string 'email=christophe.jorssen@gmail.com' \
        --form-string "pkg=$PACKAGE" \
        --form-string "summary=$SUMMARY" \
        --form-string "update=$UPDATE" \
        --form-string 'uploader=Christophe Jorssen' \
        --form-string "version=$VERSION" \
        --form-string 'license=lppl1.3c' \
        --form-string "repository=$REPOSITORY" \
        --form-string "bugtracker=$BUGTRACKER" \
        --form-string "note=$NOTE" \
        --form "file=@$UPLOAD;filename=$PACKAGE.zip;type=application/zip" \
        "https://www.ctan.org/submit/$action") \
        || fail "CTAN $action request failed (network or TLS error); no further request was made"
    printf 'CTAN %s HTTP %s:\n' "$action" "$status"
    python3 - "$response" "$action" <<'PY'
import json
import sys

path, action = sys.argv[1:]
try:
    with open(path, encoding="utf-8") as stream:
        messages = json.load(stream)
except (OSError, UnicodeError, ValueError) as error:
    sys.exit(f"Invalid CTAN response: {error}")
if not isinstance(messages, list):
    sys.exit("Invalid CTAN response: expected a list of messages")
has_error = False
uploaded = False
for message in messages:
    if not isinstance(message, list) or len(message) < 2:
        sys.exit("Invalid CTAN response: malformed message")
    level, description = message[:2]
    print(f"  {level}: {description}" +
          (f" ({', '.join(map(str, message[2:]))})" if len(message) > 2 else ""))
    has_error |= level == "ERROR" or description == "Upload failed"
    uploaded |= level == "INFO" and description == "Upload succeeded"
if has_error or (action == "upload" and not uploaded):
    sys.exit(1)
PY
    [ "$status" = 200 ] || fail "CTAN $action returned HTTP $status"
}

printf 'Sending %s to CTAN for validation only...\n' "$PACKAGE"
request validate
[ "$MODE" != validate ] || {
    printf 'Validation completed; no publication request was sent.\n'
    exit 0
}

printf 'Validation passed. Sending the confirmed public submission...\n'
request upload
printf 'CTAN accepted the upload into its review workflow; publication is not immediate.\n'
