#!/bin/zsh
#
# Publish public DNS records for the Caddy site blocks that are explicitly
# marked public, and refresh the A record for the current WAN IP.
#
# A site block is published ONLY if the line immediately above it is exactly
#
#     # ddns: public
#
# Anything unmarked stays off public DNS. That default matters: this script
# used to scrape *every* site block, which is how internal-only services ended
# up with public records (and, via Certificate Transparency, public hostnames).
# Forgetting a marker now fails closed.

CADDYFILE=/home/connor/Caddyfile
NTFY_ENV=/home/connor/.config/ntfy/publish.env
NTFY_URL=https://ntfy.rcjohnstone.com/infra

notify() {
    # Best-effort: never let a notification failure fail the run.
    [[ -r $NTFY_ENV ]] || return 0
    local user pass
    user=$(sed -n 's/^NTFY_USER=//p' $NTFY_ENV)
    pass=$(sed -n 's/^NTFY_PASS=//p' $NTFY_ENV)
    [[ -n $user && -n $pass ]] || return 0
    curl -sS -m 15 -u "$user:$pass" \
        -H "Title: $1" -H "Priority: ${3:-default}" -H "Tags: ${4:-warning}" \
        -d "$2" $NTFY_URL >/dev/null 2>&1
    return 0
}

fail() {
    print -u2 "ddns_update: $1"
    notify "DDNS update failed" "$1" high rotating_light
    exit 1
}

ip=$(curl -sq4 -m 20 ifconfig.me)
if [[ -z "$ip" ]]; then
    fail "could not determine public IPv4, aborting"
fi

if ! out=$(ddns -p $ip 4 2>&1); then
    fail "ddns -p failed:\n$out"
fi

# Emit the hostname from each site block preceded by the marker. Blank lines
# between the marker and the block are tolerated; anything else resets it.
subdomains=$(awk '
    /^[[:space:]]*#[[:space:]]*ddns:[[:space:]]*public[[:space:]]*$/ { pub=1; next }
    /^[[:space:]]*$/                                                { next }
    /^[^[:space:]#].*\{[[:space:]]*$/ {
        if (pub) { name=$0; sub(/[{,].*/, "", name); gsub(/[[:space:]]/, "", name); print name }
        pub=0; next
    }
    { pub=0 }
' $CADDYFILE \
    | grep -E '(^|\.)rcjohnstone\.com$' \
    | sed 's/^rcjohnstone\.com$/@.rcjohnstone.com/' \
    | sort -u)

if [[ -z "$subdomains" ]]; then
    fail "no '# ddns: public' markers found in $CADDYFILE -- refusing to continue"
fi

added=()
for subdomain in ${(f)subdomains}
do
    # already-tracked names just print "already exists"; that is not an error
    if out=$(ddns -s $subdomain 2>&1); then
        print -r -- "$out" | grep -v 'already exists'
        print -r -- "$out" | grep -q 'already exists' || added+=$subdomain
    else
        print -u2 "ddns_update: failed to add $subdomain:\n$out"
    fi
done

if (( ${#added} )); then
    notify "DDNS: new public records" \
        "Now publicly resolvable:\n${(F)added}" default globe_with_meridians
fi

# Reconcile. The bare `ddns` below refreshes every subdomain in ddns.db, not
# just the ones we asked for -- that is how `spanish` acquired a public record
# despite having no marker. Anything tracked locally but no longer marked is
# drift, and drift here means a service is on the internet that we did not
# intend to publish. Report it loudly rather than silently republishing.
tracked=$(python3 - <<'EOF' 2>/dev/null
import sqlite3
try:
    c = sqlite3.connect('/home/connor/.config/ddns/ddns.db')
    print("\n".join(sorted(r[0] for r in c.execute("select name from subdomains"))))
except Exception:
    pass
EOF
)
if [[ -n "$tracked" ]]; then
    marked=$(print -r -- "$subdomains" | sed 's/\.rcjohnstone\.com$//' | sed 's/^@$/@/' | sort -u)
    drift=$(comm -23 <(print -r -- "$tracked") <(print -r -- "$marked"))
    if [[ -n "$drift" ]]; then
        print -u2 "ddns_update: tracked but NOT marked public in the Caddyfile:\n$drift"
        notify "DDNS drift: unmarked names still published" \
            "These have DNS records but no '# ddns: public' marker:\n$drift\n\nRemove them from DigitalOcean and from ddns.db, or add the marker." \
            high warning
    fi
fi

ddns
