#!/bin/bash
# This file is in the public domain.
# Transmit a Challenger TAN by physical mail, via pingen.com.
#
# Usage:
#   echo "MESSAGE" | $0 '{"CONTACT_NAME":"...","ADDRESS_LINES":"...","ADDRESS_COUNTRY":"CH"}'
#
# The TAN is part of the message read from stdin and never appears in argv.
#
# Exit codes (see challenger-send-post(1)).  Success is 0 or the 200-210
# band; every other value is a failure.  The success band is up at 200 and
# not at 1 because libgnunetutil reports a failed exec() of the helper as
# exit code 1, which must not be mistaken for a delivery.
# Posting a letter only establishes that pingen accepted it for printing and
# dispatch, so this helper never reports 0.
#
#  201  accepted by pingen for printing and dispatch
#   10  postal address unusable or missing
#   30  pingen is unavailable or reported an internal error
#   31  pingen rejected the letter
#   40  local configuration or invocation error
#   41  pingen refused the credentials or the organisation
#   42  insufficient balance on the pingen account
#   50  unclassified failure, including a message too long for one page
set -eu
umask 077

# You need to provide these (set in environment).
# PINGEN_CLIENT_ID=
# PINGEN_CLIENT_SECRET=
# PINGEN_ORG_ID=
if [ -z "${PINGEN_CLIENT_ID:-}" ]; then
  echo "PINGEN_CLIENT_ID not set in environment" 1>&2
  exit 40
fi
if [ -z "${PINGEN_CLIENT_SECRET:-}" ]; then
  echo "PINGEN_CLIENT_SECRET not set in environment" 1>&2
  exit 40
fi
if [ -z "${PINGEN_ORG_ID:-}" ]; then
  echo "PINGEN_ORG_ID not set in environment" 1>&2
  exit 40
fi
if [ $# -ne 1 ]; then
  echo "Usage: $0 '{\"CONTACT_NAME\":\"...\",\"ADDRESS_LINES\":\"...\",\"ADDRESS_COUNTRY\":\"CH\"}'" 1>&2
  exit 40
fi
if ! command -v typst >/dev/null; then
  echo "typst(1) is not installed" 1>&2
  exit 40
fi

ENDPOINT="https://api.pingen.com"
LOGS="$HOME/authorization-post.log"
ERRORS="$HOME/authorization-post.err"

MESSAGE=$(cat -)
ADDR="$1"
# The address fields are handed to typst as --input values below.  Typst
# treats those strictly as strings and never as markup, so unlike with the
# LaTeX template that this script used before, no character filtering is
# needed (and none must be added: it would corrupt legitimate addresses).
# Name of business or individual, possibly with "Attn. $INDIVIDUAL_NAME"
CONTACT_NAME=$(echo "$ADDR" | jq -r '.CONTACT_NAME // empty')
# Multi-line address
ADDRESS_LINES=$(echo "$ADDR" | jq -r '.ADDRESS_LINES // empty')
# This will just be the 2-letter country code
ADDRESS_COUNTRY=$(echo "$ADDR" | jq -r '.ADDRESS_COUNTRY // empty')

if [ -z "$CONTACT_NAME" ] || [ -z "$ADDRESS_LINES" ] || [ -z "$ADDRESS_COUNTRY" ]; then
  echo "Address object lacks CONTACT_NAME, ADDRESS_LINES or ADDRESS_COUNTRY" 1>&2
  exit 10
fi

case "$ADDRESS_COUNTRY" in
AF)
  COUNTRY='Afghanistan'
  ;;
AX)
  COUNTRY='Åland Islands'
  ;;
AL)
  COUNTRY='Albania'
  ;;
DZ)
  COUNTRY='Algeria'
  ;;
AS)
  COUNTRY='American Samoa'
  ;;
AD)
  COUNTRY='Andorra'
  ;;
AO)
  COUNTRY='Angola'
  ;;
AI)
  COUNTRY='Anguilla'
  ;;
AQ)
  COUNTRY='Antarctica'
  ;;
AG)
  COUNTRY='Antigua & Barbuda'
  ;;
AR)
  COUNTRY='Argentina'
  ;;
AM)
  COUNTRY='Armenia'
  ;;
AW)
  COUNTRY='Aruba'
  ;;
AU)
  COUNTRY='Australia'
  ;;
AT)
  COUNTRY='Austria'
  ;;
AZ)
  COUNTRY='Azerbaijan'
  ;;
BS)
  COUNTRY='Bahamas'
  ;;
BH)
  COUNTRY='Bahrain'
  ;;
BD)
  COUNTRY='Bangladesh'
  ;;
BB)
  COUNTRY='Barbados'
  ;;
BY)
  COUNTRY='Belarus'
  ;;
BE)
  COUNTRY='Belgium'
  ;;
BZ)
  COUNTRY='Belize'
  ;;
BJ)
  COUNTRY='Benin'
  ;;
BM)
  COUNTRY='Bermuda'
  ;;
BT)
  COUNTRY='Bhutan'
  ;;
BO)
  COUNTRY='Bolivia'
  ;;
BA)
  COUNTRY='Bosnia & Herzegovina'
  ;;
BW)
  COUNTRY='Botswana'
  ;;
BV)
  COUNTRY='Bouvet Island'
  ;;
BR)
  COUNTRY='Brazil'
  ;;
IO)
  COUNTRY='British Indian Ocean Territory'
  ;;
VG)
  COUNTRY='British Virgin Islands'
  ;;
BN)
  COUNTRY='Brunei'
  ;;
BG)
  COUNTRY='Bulgaria'
  ;;
BF)
  COUNTRY='Burkina Faso'
  ;;
BI)
  COUNTRY='Burundi'
  ;;
KH)
  COUNTRY='Cambodia'
  ;;
CM)
  COUNTRY='Cameroon'
  ;;
CA)
  COUNTRY='Canada'
  ;;
CV)
  COUNTRY='Cape Verde'
  ;;
BQ)
  COUNTRY='Caribbean Netherlands'
  ;;
KY)
  COUNTRY='Cayman Islands'
  ;;
CF)
  COUNTRY='Central African Republic'
  ;;
TD)
  COUNTRY='Chad'
  ;;
CL)
  COUNTRY='Chile'
  ;;
CN)
  COUNTRY='China'
  ;;
CX)
  COUNTRY='Christmas Island'
  ;;
CC)
  COUNTRY='Cocos'
  ;;
CO)
  COUNTRY='Colombia'
  ;;
KM)
  COUNTRY='Comoros'
  ;;
CG)
  COUNTRY='Congo - Brazzaville'
  ;;
CD)
  COUNTRY='Congo - Kinshasa'
  ;;
CK)
  COUNTRY='Cook Islands'
  ;;
CR)
  COUNTRY='Costa Rica'
  ;;
CI)
  #shellcheck disable=SC1112
  COUNTRY='Côte d’Ivoire'
  ;;
HR)
  COUNTRY='Croatia'
  ;;
CU)
  COUNTRY='Cuba'
  ;;
CW)
  COUNTRY='Curaçao'
  ;;
CY)
  COUNTRY='Cyprus'
  ;;
CZ)
  COUNTRY='Czechia'
  ;;
DK)
  COUNTRY='Denmark'
  ;;
DJ)
  COUNTRY='Djibouti'
  ;;
DM)
  COUNTRY='Dominica'
  ;;
DO)
  COUNTRY='Dominican Republic'
  ;;
EC)
  COUNTRY='Ecuador'
  ;;
EG)
  COUNTRY='Egypt'
  ;;
SV)
  COUNTRY='El Salvador'
  ;;
GQ)
  COUNTRY='Equatorial Guinea'
  ;;
ER)
  COUNTRY='Eritrea'
  ;;
EE)
  COUNTRY='Estonia'
  ;;
SZ)
  COUNTRY='Eswatini'
  ;;
ET)
  COUNTRY='Ethiopia'
  ;;
FK)
  COUNTRY='Falkland Islands'
  ;;
FO)
  COUNTRY='Faroe Islands'
  ;;
FJ)
  COUNTRY='Fiji'
  ;;
FI)
  COUNTRY='Finland'
  ;;
FR)
  COUNTRY='France'
  ;;
GF)
  COUNTRY='French Guiana'
  ;;
PF)
  COUNTRY='French Polynesia'
  ;;
TF)
  COUNTRY='French Southern Territories'
  ;;
GA)
  COUNTRY='Gabon'
  ;;
GM)
  COUNTRY='Gambia'
  ;;
GE)
  COUNTRY='Georgia'
  ;;
DE)
  COUNTRY='Germany'
  ;;
GH)
  COUNTRY='Ghana'
  ;;
GI)
  COUNTRY='Gibraltar'
  ;;
GR)
  COUNTRY='Greece'
  ;;
GL)
  COUNTRY='Greenland'
  ;;
GD)
  COUNTRY='Grenada'
  ;;
GP)
  COUNTRY='Guadeloupe'
  ;;
GU)
  COUNTRY='Guam'
  ;;
GT)
  COUNTRY='Guatemala'
  ;;
GG)
  COUNTRY='Guernsey'
  ;;
GN)
  COUNTRY='Guinea'
  ;;
GW)
  COUNTRY='Guinea-Bissau'
  ;;
GY)
  COUNTRY='Guyana'
  ;;
HT)
  COUNTRY='Haiti'
  ;;
HM)
  COUNTRY='Heard & McDonald Islands'
  ;;
HN)
  COUNTRY='Honduras'
  ;;
HK)
  COUNTRY='Hong Kong SAR China'
  ;;
HU)
  COUNTRY='Hungary'
  ;;
IS)
  COUNTRY='Iceland'
  ;;
IN)
  COUNTRY='India'
  ;;
ID)
  COUNTRY='Indonesia'
  ;;
IR)
  COUNTRY='Iran'
  ;;
IQ)
  COUNTRY='Iraq'
  ;;
IE)
  COUNTRY='Ireland'
  ;;
IM)
  COUNTRY='Isle of Man'
  ;;
IL)
  COUNTRY='Israel'
  ;;
IT)
  COUNTRY='Italy'
  ;;
JM)
  COUNTRY='Jamaica'
  ;;
JP)
  COUNTRY='Japan'
  ;;
JE)
  COUNTRY='Jersey'
  ;;
JO)
  COUNTRY='Jordan'
  ;;
KZ)
  COUNTRY='Kazakhstan'
  ;;
KE)
  COUNTRY='Kenya'
  ;;
KI)
  COUNTRY='Kiribati'
  ;;
KW)
  COUNTRY='Kuwait'
  ;;
KG)
  COUNTRY='Kyrgyzstan'
  ;;
LA)
  COUNTRY='Laos'
  ;;
LV)
  COUNTRY='Latvia'
  ;;
LB)
  COUNTRY='Lebanon'
  ;;
LS)
  COUNTRY='Lesotho'
  ;;
LR)
  COUNTRY='Liberia'
  ;;
LY)
  COUNTRY='Libya'
  ;;
LI)
  COUNTRY='Liechtenstein'
  ;;
LT)
  COUNTRY='Lithuania'
  ;;
LU)
  COUNTRY='Luxembourg'
  ;;
MO)
  COUNTRY='Macao SAR China'
  ;;
MG)
  COUNTRY='Madagascar'
  ;;
MW)
  COUNTRY='Malawi'
  ;;
MY)
  COUNTRY='Malaysia'
  ;;
MV)
  COUNTRY='Maldives'
  ;;
ML)
  COUNTRY='Mali'
  ;;
MT)
  COUNTRY='Malta'
  ;;
MH)
  COUNTRY='Marshall Islands'
  ;;
MQ)
  COUNTRY='Martinique'
  ;;
MR)
  COUNTRY='Mauritania'
  ;;
MU)
  COUNTRY='Mauritius'
  ;;
YT)
  COUNTRY='Mayotte'
  ;;
MX)
  COUNTRY='Mexico'
  ;;
FM)
  COUNTRY='Micronesia'
  ;;
MD)
  COUNTRY='Moldova'
  ;;
MC)
  COUNTRY='Monaco'
  ;;
MN)
  COUNTRY='Mongolia'
  ;;
ME)
  COUNTRY='Montenegro'
  ;;
MS)
  COUNTRY='Montserrat'
  ;;
MA)
  COUNTRY='Morocco'
  ;;
MZ)
  COUNTRY='Mozambique'
  ;;
MM)
  COUNTRY='Myanmar'
  ;;
NA)
  COUNTRY='Namibia'
  ;;
NR)
  COUNTRY='Nauru'
  ;;
NP)
  COUNTRY='Nepal'
  ;;
NL)
  COUNTRY='Netherlands'
  ;;
NC)
  COUNTRY='New Caledonia'
  ;;
NZ)
  COUNTRY='New Zealand'
  ;;
NI)
  COUNTRY='Nicaragua'
  ;;
NE)
  COUNTRY='Niger'
  ;;
NG)
  COUNTRY='Nigeria'
  ;;
NU)
  COUNTRY='Niue'
  ;;
NF)
  COUNTRY='Norfolk Island'
  ;;
KP)
  COUNTRY='North Korea'
  ;;
MK)
  COUNTRY='North Macedonia'
  ;;
MP)
  COUNTRY='Northern Mariana Islands'
  ;;
NO)
  COUNTRY='Norway'
  ;;
OM)
  COUNTRY='Oman'
  ;;
PK)
  COUNTRY='Pakistan'
  ;;
PW)
  COUNTRY='Palau'
  ;;
PS)
  COUNTRY='Palestinian Territories'
  ;;
PA)
  COUNTRY='Panama'
  ;;
PG)
  COUNTRY='Papua New Guinea'
  ;;
PY)
  COUNTRY='Paraguay'
  ;;
PE)
  COUNTRY='Peru'
  ;;
PH)
  COUNTRY='Philippines'
  ;;
PN)
  COUNTRY='Pitcairn Islands'
  ;;
PL)
  COUNTRY='Poland'
  ;;
PT)
  COUNTRY='Portugal'
  ;;
PR)
  COUNTRY='Puerto Rico'
  ;;
QA)
  COUNTRY='Qatar'
  ;;
RE)
  COUNTRY='Réunion'
  ;;
RO)
  COUNTRY='Romania'
  ;;
RU)
  COUNTRY='Russia'
  ;;
RW)
  COUNTRY='Rwanda'
  ;;
WS)
  COUNTRY='Samoa'
  ;;
SM)
  COUNTRY='San Marino'
  ;;
ST)
  COUNTRY='São Tomé & Príncipe'
  ;;
SA)
  COUNTRY='Saudi Arabia'
  ;;
SN)
  COUNTRY='Senegal'
  ;;
RS)
  COUNTRY='Serbia'
  ;;
SC)
  COUNTRY='Seychelles'
  ;;
SL)
  COUNTRY='Sierra Leone'
  ;;
SG)
  COUNTRY='Singapore'
  ;;
SX)
  COUNTRY='Sint Maarten'
  ;;
SK)
  COUNTRY='Slovakia'
  ;;
SI)
  COUNTRY='Slovenia'
  ;;
SB)
  COUNTRY='Solomon Islands'
  ;;
SO)
  COUNTRY='Somalia'
  ;;
ZA)
  COUNTRY='South Africa'
  ;;
GS)
  COUNTRY='South Georgia & South Sandwich Islands'
  ;;
KR)
  COUNTRY='South Korea'
  ;;
SS)
  COUNTRY='South Sudan'
  ;;
ES)
  COUNTRY='Spain'
  ;;
LK)
  COUNTRY='Sri Lanka'
  ;;
BL)
  COUNTRY='St. Barthélemy'
  ;;
SH)
  COUNTRY='St. Helena'
  ;;
KN)
  COUNTRY='St. Kitts & Nevis'
  ;;
LC)
  COUNTRY='St. Lucia'
  ;;
MF)
  COUNTRY='St. Martin'
  ;;
PM)
  COUNTRY='St. Pierre & Miquelon'
  ;;
VC)
  COUNTRY='St. Vincent & Grenadines'
  ;;
SD)
  COUNTRY='Sudan'
  ;;
SR)
  COUNTRY='Suriname'
  ;;
SJ)
  COUNTRY='Svalbard & Jan Mayen'
  ;;
SE)
  COUNTRY='Sweden'
  ;;
CH)
  COUNTRY='Switzerland'
  ;;
SY)
  COUNTRY='Syria'
  ;;
TW)
  COUNTRY='Taiwan'
  ;;
TJ)
  COUNTRY='Tajikistan'
  ;;
TZ)
  COUNTRY='Tanzania'
  ;;
TH)
  COUNTRY='Thailand'
  ;;
TL)
  COUNTRY='Timor-Leste'
  ;;
TG)
  COUNTRY='Togo'
  ;;
TK)
  COUNTRY='Tokelau'
  ;;
TO)
  COUNTRY='Tonga'
  ;;
TT)
  COUNTRY='Trinidad & Tobago'
  ;;
TN)
  COUNTRY='Tunisia'
  ;;
TR)
  COUNTRY='Turkey'
  ;;
TM)
  COUNTRY='Turkmenistan'
  ;;
TC)
  COUNTRY='Turks & Caicos Islands'
  ;;
TV)
  COUNTRY='Tuvalu'
  ;;
UM)
  COUNTRY='U.S. Outlying Islands'
  ;;
VI)
  COUNTRY='U.S. Virgin Islands'
  ;;
UG)
  COUNTRY='Uganda'
  ;;
UA)
  COUNTRY='Ukraine'
  ;;
AE)
  COUNTRY='United Arab Emirates'
  ;;
GB)
  COUNTRY='United Kingdom'
  ;;
US)
  COUNTRY='United States'
  ;;
UY)
  COUNTRY='Uruguay'
  ;;
UZ)
  COUNTRY='Uzbekistan'
  ;;
VU)
  COUNTRY='Vanuatu'
  ;;
VA)
  COUNTRY='Vatican City'
  ;;
VE)
  COUNTRY='Venezuela'
  ;;
VN)
  COUNTRY='Vietnam'
  ;;
WF)
  COUNTRY='Wallis & Futuna'
  ;;
EH)
  COUNTRY='Western Sahara'
  ;;
YE)
  COUNTRY='Yemen'
  ;;
ZM)
  COUNTRY='Zambia'
  ;;
ZW)
  COUNTRY='Zimbabwe'
  ;;
*)
  echo "ERROR: Country code '${ADDRESS_COUNTRY}' not recognized" 1>&2
  COUNTRY="${ADDRESS_COUNTRY}"
  ;;
esac

MYDIR=$(mktemp -d "${TMPDIR:-/tmp}/${USER:-$(id -un)}-challenger-postal-generator-XXXXXX")
# Keep failed letters and API replies for diagnosis; remove authentication files.
trap '
  EXIT_STATUS=$?
  rm -rf -- "$MYDIR/credentials"
  if [ "$EXIT_STATUS" -eq 201 ]; then
    rm -rf -- "$MYDIR"
  else
    echo "challenger-send-post failed, working files kept in ${MYDIR}" >>"$ERRORS"
  fi
' EXIT
trap 'exit 129' HUP
trap 'exit 130' INT
trap 'exit 143' TERM
mkdir "$MYDIR/credentials"
printf '%s' "$PINGEN_CLIENT_ID" >"$MYDIR/credentials/client-id"
printf '%s' "$PINGEN_CLIENT_SECRET" >"$MYDIR/credentials/client-secret"
printf '%s' "$MESSAGE" >"$MYDIR/message.txt"
cd "$MYDIR"

# The letter template.
# pingen prints the addressee through the window of the envelope and is
# therefore picky about where the address block sits.  Do not move the
# addressee, the date or the subject without re-checking that.
#
# --ignore-system-fonts keeps rendering reproducible and fast: "New Computer
# Modern" (the OpenType successor of the Computer Modern that pdflatex used
# here) ships inside the typst binary.  Drop that flag or add --font-path if
# addresses in non-Latin scripts ever need to be printed.
cat >letter.typ <<'ENDOFLETTERTEMPLATE'
// Geometry measured from the scrlttr2 letter this replaced.
// test_helpers.py --render-postal renders it without uploading.
#let FS = 10.9091pt          // effective size of cmr10 in the 11pt letter
#let LEAD = 0.242035em       // baseline step = 1.242035em = 13.5495pt
#let TW = 416.705pt          // text width  (31.50mm .. 178.50mm)
#let TX = 89.291pt           // text left   (31.50mm)
#let AX = 85.039pt           // address left (30mm, = toaddrhpos)
#let AW = 240.945pt          // address width (85mm, = toaddrwidth)
#let ACENTER = 203.647pt     // address block is centred on this baseline
#let ACLAMP = 152.16pt       // ... unless it is too tall, then it starts here
#let DATE_BL = 292.762pt
#let SUBJ_BL = 319.860pt
#let OPEN_BL = 360.508pt
#let BODY_BL = 387.606pt
#let CLOSE_GAP = 26.151pt    // last body baseline -> closing baseline
#let SIG_GAP = 40.648pt      // closing baseline -> signature baseline
#let BOTTOM_BL = 672.19pt    // last baseline that still fits on page 1

#let SIGNATURE = "Taler Operations AG, c/o Berner Fachhochschule, Höheweg 80, 2502 Biel/Bienne"
#let SUBJECT = "Address validation"
#let OPENING = "To whom it may concern,"
#let CLOSING = "Best regards"

#set page(paper: "a4", margin: 0pt)
#set text(
  font: "New Computer Modern",
  size: FS,
  top-edge: 0.75em,
  bottom-edge: -0.25em,
  lang: "en",
  hyphenate: false,
)
#set par(leading: LEAD, spacing: LEAD, justify: true)

// place a block so that its *first baseline* lands on `bl`
#let at-baseline(x, bl, body) = place(top + left, dx: x, dy: bl - 0.75em, body)

#let name = sys.inputs.at("name", default: "")
#let addr = sys.inputs.at("addr", default: "")
#let country = sys.inputs.at("country", default: "")
#let message = read("message.txt")

// ---- addressee -----------------------------------------------------------
// TeX collapses runs of whitespace; a multi-line name becomes a single line.
#let flat(s) = s.replace(regex("\\s+"), " ").trim()
#let addr-lines = (
  ((flat(name),) + addr.split("\n").map(flat) + (flat(country),)).filter(l => l != "")
)
#let addr-block = block(width: AW)[
  #set par(justify: false)
  #addr-lines.join(linebreak())
]

#context {
  let h = measure(addr-block).height
  let bl = calc.max(ACLAMP, ACENTER - (h - FS) / 2)
  at-baseline(AX, bl, addr-block)
}

// ---- reference line, subject, opening ------------------------------------
#at-baseline(TX, DATE_BL, block(width: TW, align(right, datetime.today().display(
  "[month repr:long] [day padding:none], [year]",
))))
#at-baseline(TX, SUBJ_BL, text(weight: "bold", SUBJECT))
#at-baseline(TX, OPEN_BL, OPENING)

// ---- body ----------------------------------------------------------------
// TeX collapses runs of whitespace and starts a new paragraph on a blank line.
#let paragraphs = message.split(regex("\n[ \t]*\r?\n")).map(p => p.replace(
  regex("\\s+"),
  " ",
).trim()).filter(p => p != "")

#let body-at(scale) = block(width: TW)[
  #set text(size: FS * scale, hyphenate: true)
  #set par(first-line-indent: (amount: 1em, all: false))
  #for p in paragraphs [#par(p)]
]

#context {
  // Shrink the body until closing and signature still fit on this page.
  let scale = 1.0
  let fits(s) = {
    let last = BODY_BL + (measure(body-at(s)).height - FS * s)
    last + CLOSE_GAP + SIG_GAP <= BOTTOM_BL
  }
  while scale > 0.34 and not fits(scale) {
    scale -= 0.02
  }
  if not fits(scale) {
    panic("challenge message is far too long to fit on one A4 page")
  }

  at-baseline(TX, BODY_BL, body-at(scale))

  // ---- closing and signature ---------------------------------------------
  // scrlttr2 puts both in a box as wide as its widest line, left-aligned with
  // the text body, and centres each line inside that box.
  let last = BODY_BL + (measure(body-at(scale)).height - FS * scale)
  let sigw = measure(SIGNATURE).width
  let sigbox(s) = block(width: sigw, {
    set par(justify: false)
    align(center, s)
  })
  at-baseline(TX, last + CLOSE_GAP, sigbox(CLOSING))
  at-baseline(TX, last + CLOSE_GAP + SIG_GAP, sigbox(SIGNATURE))
}
ENDOFLETTERTEMPLATE

if ! typst compile \
  --ignore-system-fonts \
  --no-pdf-tags \
  --input "name=$CONTACT_NAME" \
  --input "addr=$ADDRESS_LINES" \
  --input "country=$COUNTRY" \
  letter.typ letter.pdf; then
  # Either the address block does not fit the window-envelope geometry, or
  # the message is too long even at the smallest body scale.  Rather than
  # post a letter with a truncated TAN, the template panics and we fail.
  echo "Failed to render letter for country ${ADDRESS_COUNTRY}" >>"$ERRORS"
  exit 50
fi

REPLY=$(curl \
  -s \
  -X POST \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode "grant_type=client_credentials" \
  --data-urlencode "client_id@credentials/client-id" \
  --data-urlencode "client_secret@credentials/client-secret" \
  --data-urlencode "scope=letter" \
  https://identity.pingen.com/auth/access-tokens) || {
  echo "Could not reach the pingen identity service" >>"$ERRORS"
  exit 30
}

ACCESS_TOKEN=$(printf '%s' "$REPLY" | jq -r '.access_token | select(type == "string")' 2>/dev/null || true)

if [ -z "$ACCESS_TOKEN" ]; then
  echo "pingen refused the configured credentials" >>"$ERRORS"
  exit 41
fi

if [[ $ACCESS_TOKEN == *$'\r'* || $ACCESS_TOKEN == *$'\n'* ]]; then
  echo "pingen returned an invalid access token" >>"$ERRORS"
  exit 30
fi
printf 'Authorization: Bearer %s\n' "$ACCESS_TOKEN" >credentials/auth

REPLY=$(curl -s \
  -X GET "$ENDPOINT/file-upload" \
  -H @credentials/auth) || {
  echo "Could not reach pingen to request an upload URL" >>"$ERRORS"
  exit 30
}
ATTRS=$(printf '%s' "$REPLY" | jq '.data.attributes // empty' 2>/dev/null || true)
UPLOAD_URL=$(printf '%s' "$ATTRS" | jq -r '.url | select(type == "string")' 2>/dev/null || true)
URL_SIG=$(printf '%s' "$ATTRS" | jq -r '.url_signature | select(type == "string")' 2>/dev/null || true)

if [ -z "$UPLOAD_URL" ] || [ -z "$URL_SIG" ]; then
  echo "pingen did not return an upload URL" >>"$ERRORS"
  exit 30
fi

printf '%s' "$UPLOAD_URL" >credentials/upload-url
printf '%s' "$URL_SIG" >credentials/url-signature
# The signed URL grants upload access, so keep it out of curl's argv too.
URL_CONFIG=${UPLOAD_URL//\\/\\\\}
URL_CONFIG=${URL_CONFIG//\"/\\\"}
URL_CONFIG=${URL_CONFIG//$'\r'/\\r}
URL_CONFIG=${URL_CONFIG//$'\n'/\\n}
URL_CONFIG=${URL_CONFIG//$'\t'/\\t}
URL_CONFIG=${URL_CONFIG//$'\v'/\\v}
printf 'url = "%s"\n' "$URL_CONFIG" >credentials/upload-config

STATUS=$(curl -s \
  -X PUT \
  -T letter.pdf \
  --config credentials/upload-config \
  -o "$MYDIR/upload-status.txt" \
  -w "%{http_code}") || {
  echo "Could not upload letter to pingen" >>"$ERRORS"
  exit 30
}

case "$STATUS" in
200) ;;
201) ;;
204) ;;
401 | 403)
  echo "Upload of letter rejected: $STATUS" >>"$ERRORS"
  exit 41
  ;;
*)
  echo "Failed to upload letter: $STATUS" >>"$ERRORS"
  exit 30
  ;;
esac

jq -n '
    {   data: {
        type: "letters",
	attributes: {
	  file_original_name: "letter.pdf",
	  file_url: $UPLOAD_URL,
	  file_url_signature: $URL_SIG,
	  address_position: "left",
	  delivery_product: "cheap",
	  print_mode: "duplex",
	  auto_send: true,
 	  print_spectrum: "grayscale"
        } }
    }' \
  --rawfile UPLOAD_URL credentials/upload-url \
  --rawfile URL_SIG credentials/url-signature >credentials/request

STATUS=$(curl -s \
  --request POST \
  --url "$ENDPOINT/organisations/${PINGEN_ORG_ID}/letters" \
  --header "Content-Type: application/vnd.api+json" \
  --header @credentials/auth \
  --data-binary @credentials/request \
  -o "$MYDIR/final-reply.txt" \
  -w "%{http_code}") || {
  echo "Could not submit letter to pingen" >>"$ERRORS"
  exit 30
}
case "$STATUS" in
201)
  echo "pingen accepted letter (HTTP 201)" >>"$LOGS"
  ;;
401 | 403)
  echo "pingen refused the credentials or organisation: $STATUS" >>"$ERRORS"
  exit 41
  ;;
402)
  echo "pingen account has insufficient balance: $STATUS" >>"$ERRORS"
  exit 42
  ;;
400 | 422)
  echo "pingen rejected the letter: $STATUS" >>"$ERRORS"
  exit 31
  ;;
5??)
  echo "pingen is unavailable: $STATUS" >>"$ERRORS"
  exit 30
  ;;
*)
  echo "Failed to add letter: $STATUS" >>"$ERRORS"
  exit 50
  ;;
esac

# Physical delivery takes days. The EXIT trap removes the successful job.
exit 201
