#!/usr/bin/env bash
#
# watchtower-notify-migrate.sh
#
# Converts the legacy Watchtower email notification variables in a
# docker-compose file to the WATCHTOWER_NOTIFICATION_URL (shoutrrr smtp://)
# format used by the maintained fork (watchtower.nickfedor.com, v1.x).
#
# Usage:
#   ./watchtower-notify-migrate.sh docker-compose.yml            # dry run, prints diff
#   ./watchtower-notify-migrate.sh -o new.yml docker-compose.yml # write to new.yml
#   ./watchtower-notify-migrate.sh -i docker-compose.yml         # in-place, keeps .bak
#
# Exit codes: 0 = success or nothing to do, 1 = error.

set -euo pipefail
LC_ALL=C

# ---------- options ----------
IN_PLACE=0
OUTPUT=""
usage() { grep '^#' "$0" | sed 's/^# \{0,1\}//' | sed -n '3,14p'; exit "${1:-0}"; }

while getopts ":io:h" opt; do
  case "$opt" in
    i) IN_PLACE=1 ;;
    o) OUTPUT="$OPTARG" ;;
    h) usage 0 ;;
    *) usage 1 ;;
  esac
done
shift $((OPTIND - 1))
[ $# -eq 1 ] || usage 1
FILE="$1"
[ -f "$FILE" ] || { echo "ERROR: file not found: $FILE" >&2; exit 1; }
[ "$IN_PLACE" -eq 1 ] && [ -n "$OUTPUT" ] && { echo "ERROR: -i and -o are mutually exclusive" >&2; exit 1; }

# ---------- helpers ----------
# warn() also runs inside $(...) subshells, so warnings go through a file.
WARNFILE="$(mktemp)"
trap 'rm -f "$WARNFILE"' EXIT
warn() { printf '%s\n' "$1" >> "$WARNFILE"; }

# Percent-encode everything except RFC 3986 unreserved characters.
urlencode() {
  local s="$1" out="" c i
  for ((i = 0; i < ${#s}; i++)); do
    c="${s:i:1}"
    case "$c" in
      [a-zA-Z0-9.~_-]) out+="$c" ;;
      *) printf -v c '%%%02X' "'$c"; out+="$c" ;;
    esac
  done
  printf '%s' "$out"
}

# Compose ${VAR} references must survive verbatim or substitution breaks.
enc() {
  local v="$1"
  if [[ "$v" =~ ^\$\{[A-Za-z_][A-Za-z0-9_:?-]*\}$ || "$v" =~ ^\$[A-Za-z_][A-Za-z0-9_]*$ ]]; then
    warn "value '$v' is a compose variable reference — passed through unencoded; ensure the resolved value needs no URL escaping"
    printf '%s' "$v"
  else
    urlencode "$v"
  fi
}

# ---------- pass 1: extract legacy vars ----------
LEGACY_RE='WATCHTOWER_NOTIFICATIONS|WATCHTOWER_NOTIFICATION_EMAIL_(FROM|TO|SERVER|SERVER_PORT|SERVER_USER|SERVER_PASSWORD|SERVER_TLS_SKIP_VERIFY|DELAY|SUBJECTTAG)'

# Emits: lineno<TAB>style<TAB>indent-width<TAB>key<TAB>value
EXTRACTED="$(awk -v re="^(${LEGACY_RE})$" '
  function unquote(v) {
    if (length(v) >= 2) {
      f = substr(v, 1, 1); l = substr(v, length(v), 1)
      if ((f == "\"" && l == "\"") || (f == "\x27" && l == "\x27"))
        return substr(v, 2, length(v) - 2)
    }
    return v
  }
  function trim(v) { gsub(/^[ \t]+|[ \t\r]+$/, "", v); return v }
  {
    line = $0; sub(/\r$/, "", line)
    if (match(line, /^[ \t]*-[ \t]*/)) {                     # list style: - KEY=value
      rest = substr(line, RLENGTH + 1)
      match(line, /^[ \t]*/); indent = RLENGTH
      rest = unquote(trim(rest))
      eq = index(rest, "=")
      if (eq > 1) {
        key = trim(substr(rest, 1, eq - 1))
        val = substr(rest, eq + 1)
        if (key ~ re) { printf "%d\tlist\t%d\t%s\t%s\n", NR, indent, key, unquote(val) }
      }
    } else if (match(line, /^[ \t]*"?[A-Z_]+"?[ \t]*:/)) {   # map style: KEY: value
      co = index(line, ":")
      key = trim(substr(line, 1, co - 1)); key = unquote(key)
      val = trim(substr(line, co + 1))
      match(line, /^[ \t]*/); indent = RLENGTH
      if (key ~ re) { printf "%d\tmap\t%d\t%s\t%s\n", NR, indent, key, unquote(val) }
    }
  }
' "$FILE")"

if [ -z "$EXTRACTED" ]; then
  if grep -qE 'WATCHTOWER_NOTIFICATION_URL' "$FILE"; then
    echo "Nothing to do: $FILE already uses WATCHTOWER_NOTIFICATION_URL and contains no legacy email variables."
  else
    echo "Nothing to do: no legacy Watchtower email notification variables found in $FILE."
  fi
  if grep -qE '^\s*-{2}notification-email' "$FILE"; then
    echo "NOTE: found --notification-email-* CLI flags in a command: block — this script only converts environment variables."
  fi
  exit 0
fi

# Bash 3.2 compatible (macOS): plain variables instead of associative arrays.
V_NOTIFICATIONS="" V_FROM="" V_TO="" V_SERVER="" V_PORT=""
V_USER="" V_PASS="" V_SKIP="" V_DELAY="" V_TAG=""
FIRST_LINE="" FIRST_STYLE="" FIRST_INDENT=""
DELETE_LINES=""

while IFS="$(printf '\t')" read -r lineno style indent key value; do
  [ -z "$FIRST_LINE" ] && { FIRST_LINE="$lineno"; FIRST_STYLE="$style"; FIRST_INDENT="$indent"; }
  DELETE_LINES="$DELETE_LINES $lineno"
  case "$key" in
    WATCHTOWER_NOTIFICATIONS)                             V_NOTIFICATIONS="$value" ;;
    WATCHTOWER_NOTIFICATION_EMAIL_FROM)                   V_FROM="$value" ;;
    WATCHTOWER_NOTIFICATION_EMAIL_TO)                     V_TO="$value" ;;
    WATCHTOWER_NOTIFICATION_EMAIL_SERVER)                 V_SERVER="$value" ;;
    WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PORT)            V_PORT="$value" ;;
    WATCHTOWER_NOTIFICATION_EMAIL_SERVER_USER)            V_USER="$value" ;;
    WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD)        V_PASS="$value" ;;
    WATCHTOWER_NOTIFICATION_EMAIL_SERVER_TLS_SKIP_VERIFY) V_SKIP="$value" ;;
    WATCHTOWER_NOTIFICATION_EMAIL_DELAY)                  V_DELAY="$value" ;;
    WATCHTOWER_NOTIFICATION_EMAIL_SUBJECTTAG)             V_TAG="$value" ;;
  esac
done <<EOF_EXTRACT
$EXTRACTED
EOF_EXTRACT

# ---------- pass 2: build the shoutrrr smtp URL ----------
if [ -z "$V_SERVER" ]; then
  if [ -z "$V_FROM$V_TO$V_USER$V_PASS$V_PORT$V_SKIP$V_DELAY$V_TAG" ]; then
    echo "Nothing to do: only the WATCHTOWER_NOTIFICATIONS selector was found, no email variables to convert."
    exit 0
  fi
  echo "ERROR: no WATCHTOWER_NOTIFICATION_EMAIL_SERVER found — nothing to build the smtp:// URL from." >&2
  exit 1
fi

# Encryption mapping per port, matching the official docs' common configurations.
port="${V_PORT:-25}"
case "$port" in
  465) encp="ImplicitTLS"; starttls="no" ;;
  25)  encp="None";        starttls="no" ;;
  *)   encp="ExplicitTLS"; starttls="yes" ;;
esac

URL="smtp://"
if [ -n "$V_USER" ]; then
  URL+="$(enc "$V_USER"):$(enc "$V_PASS")@"
fi
URL+="${V_SERVER}:${port}/?fromaddress=$(enc "$V_FROM")&toaddresses=$(enc "$V_TO")"
URL+="&encryption=${encp}&usestarttls=${starttls}"
[ -z "$V_USER" ] && URL+="&auth=None"
case "$V_SKIP" in
  [Tt]rue|1|[Yy]es) URL+="&skiptlsverify=yes" ;;
esac

[ -z "$V_FROM" ] && warn "EMAIL_FROM is empty — fromaddress is required by the smtp service"
[ -z "$V_TO" ]   && warn "EMAIL_TO is empty — toaddresses is required by the smtp service"

# Non-email legacy types would silently lose their trigger when the
# WATCHTOWER_NOTIFICATIONS selector is removed, so flag them.
if [ -n "$V_NOTIFICATIONS" ]; then
  for t in $(printf '%s' "$V_NOTIFICATIONS" | tr ',' ' '); do
    case "$t" in
      email|shoutrrr|"") : ;;
      *) warn "WATCHTOWER_NOTIFICATIONS also listed '$t' — this script migrates email only, convert that service manually" ;;
    esac
  done
fi

# ---------- pass 3: rewrite the file ----------
NEW_LINES=()
pad="$(printf '%*s' "$FIRST_INDENT" '')"
emit() { # key value
  if [ "$FIRST_STYLE" = "list" ]; then
    NEW_LINES+=("${pad}- \"$1=$2\"")
  else
    NEW_LINES+=("${pad}$1: \"$2\"")
  fi
}
emit "WATCHTOWER_NOTIFICATION_URL" "$URL"
[ -n "$V_TAG" ] && emit "WATCHTOWER_NOTIFICATION_TITLE_TAG" "$V_TAG"
if [ -n "$V_DELAY" ]; then
  if grep -qE 'WATCHTOWER_NOTIFICATIONS_DELAY' "$FILE"; then
    warn "EMAIL_DELAY dropped — WATCHTOWER_NOTIFICATIONS_DELAY is already set"
  else
    emit "WATCHTOWER_NOTIFICATIONS_DELAY" "$V_DELAY"
  fi
fi

# BSD awk rejects newlines in -v values, so the block is passed via ENVIRON.
REPL="$(printf '%s\n' "${NEW_LINES[@]}")"
export REPL
CONVERTED="$(awk -v del=" $DELETE_LINES " -v first="$FIRST_LINE" '
  {
    if (NR == first) { print ENVIRON["REPL"]; next }
    if (index(del, " " NR " ") > 0) next
    print
  }
' "$FILE")"

# ---------- output ----------
echo "Converted notification URL:"
echo "  $URL"
echo

if [ -s "$WARNFILE" ]; then
  sed 's/^/WARNING: /' "$WARNFILE"
  echo
fi

if grep -qE 'WATCHTOWER_NOTIFICATION_(SLACK|MSTEAMS|GOTIFY)_' "$FILE"; then
  echo "NOTE: non-email legacy notification variables found — left untouched (this script migrates email only)."
  echo
fi

if [ "$IN_PLACE" -eq 1 ]; then
  cp "$FILE" "$FILE.bak"
  printf '%s\n' "$CONVERTED" > "$FILE"
  echo "Written in place: $FILE (backup: $FILE.bak)"
elif [ -n "$OUTPUT" ]; then
  printf '%s\n' "$CONVERTED" > "$OUTPUT"
  echo "Written: $OUTPUT"
else
  echo "Dry run — diff against $FILE:"
  diff -u "$FILE" <(printf '%s\n' "$CONVERTED") || true
  echo
  echo "Re-run with -i (in place, keeps .bak) or -o FILE to write."
fi

echo
echo "Verify afterwards: docker compose config -q && docker compose up -d, then check the startup notification."
