#!/bin/zsh
# Open today's note, syncing it through WebDAV around the edit.
#
# The server rolls the note forward at 00:01, so normally this just fetches a
# file that already exists. Everything here is arranged so that losing the
# network degrades to editing the local copy, never to losing an edit.

emulate -L zsh
set -o errexit -o nounset -o pipefail

NOTES=${TODO_NOTES:-$HOME/docs/notes/todo}
REMOTE=${TODO_REMOTE:-https://files.rcjohnstone.com/docs/notes/todo}
DAV_USER=${TODO_USER:-connor}
RBW_ENTRY=${TODO_RBW_ENTRY:-SFTPGo}
PRUNE=${TODO_PRUNE:-$HOME/.local/lib/todo/prune.py}
# zshenv exports EDITOR on every invocation, including this script's own, so
# `EDITOR=x todo` cannot work. TODO_EDITOR is the override that does.
EDIT=${TODO_EDITOR:-${EDITOR:-nvim}}
AGENT=todo-cli/1.0

today=$(date --iso-8601)
current=$NOTES/$today.md

mkdir -p $NOTES
work=$(mktemp -d); trap 'rm -rf $work' EXIT

note()  { print -u2 -- "todo: $*" }
die()   { print -u2 -- "todo: $*"; exit 1 }

# Password stays in a variable and reaches curl over stdin. Passing it as -u
# would put it in argv, where any other user's `ps` can read it.
dav_pass=""
if command -v rbw >/dev/null 2>&1; then
    dav_pass=$(rbw get $RBW_ENTRY 2>/dev/null) || dav_pass=""
fi

dav() {
    [[ -n $dav_pass ]] || return 64
    printf 'user = "%s:%s"\n' $DAV_USER $dav_pass |
        curl -sS --config - -A $AGENT --max-time 15 "$@"
}

etag_of() { grep -i '^etag:' $1 2>/dev/null | tr -d '\r' | awk '{print $2}' }

# ---------------------------------------------------------------- pull

base_etag=""
have_remote=0

if [[ -z $dav_pass ]]; then
    note "no credential from rbw, working offline"
else
    code=$(dav -o $work/pull -D $work/pull.h -w '%{http_code}' \
              $REMOTE/$today.md) || code=000
    case $code in
        200)
            base_etag=$(etag_of $work/pull.h)
            have_remote=1
            if [[ -e $current ]] && ! cmp -s $current $work/pull; then
                note "local and remote both changed since last sync, keeping local"
                note "remote copy saved alongside it for comparison"
                cp $work/pull $NOTES/$today.remote-$(date +%H%M%S).md
            else
                cp $work/pull $current
            fi
            ;;
        404)
            # The 00:01 timer has not run, or the clock disagrees about the day.
            # One create, never a poll loop: repeated 404s trip http-probing.
            note "no $today.md on the server yet, creating it here"
            ;;
        401)
            # Never retry: repeated 401s from one IP trip http-generic-bf and
            # earn a four hour ban that no whitelist covers.
            die "credentials rejected. fix the $RBW_ENTRY entry, do not re-run blindly"
            ;;
        000)
            note "server unreachable, working offline"
            ;;
        *)
            note "server returned $code, working offline"
            ;;
    esac
fi

# ------------------------------------------------------- local fallback

if [[ ! -e $current ]]; then
    # Match the date glob exactly. A bare `ls | tail -1` can select a
    # .sync-conflict- sibling, which is how divergent notes spread before.
    previous=(${~NOTES}/[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9].md(N))
    if (( ${#previous} )); then
        src=${previous[-1]}
        if [[ -x $PRUNE ]]; then
            $PRUNE $src > $work/new
        else
            note "prune not found at $PRUNE, carrying the note forward unpruned"
            cp $src $work/new
        fi
        # Only line 1. A global replace would rewrite any date in the body,
        # so "meeting moved to 2026-09-08" would silently become today.
        sed -i "1s/.*/# $today/" $work/new
        cp $work/new $current
        note "created $today.md from ${src:t}"
    else
        printf '# %s\n' $today > $current
        note "started a fresh $today.md"
    fi
fi

before=$(md5sum < $current)

# ---------------------------------------------------------------- edit

$EDIT $current

# ---------------------------------------------------------------- push

[[ $(md5sum < $current) == $before ]] && { note "unchanged, nothing to push"; exit 0 }
[[ -n $dav_pass ]] || { note "no credential, leaving changes local"; exit 0 }

# SFTPGo returns ETags but ignores If-Match on PUT, so the precondition has to
# be checked here instead. The race window is milliseconds wide for one person.
if (( have_remote )); then
    dav -I -D $work/head.h -o /dev/null $REMOTE/$today.md >/dev/null 2>&1 || true
    now_etag=$(etag_of $work/head.h)
    if [[ -n $now_etag && -n $base_etag && $now_etag != $base_etag ]]; then
        sibling=$NOTES/$today.sync-conflict-$(date +%Y%m%d-%H%M%S)-${HOST:u}.md
        cp $current $sibling
        die "remote changed while you were editing. yours saved as ${sibling:t}, nothing overwritten"
    fi
fi

code=$(dav -X PUT --data-binary @$current -o /dev/null -w '%{http_code}' \
          $REMOTE/$today.md) || code=000
case $code in
    200|201|204) note "pushed $today.md" ;;
    401)         die "credentials rejected on push. changes are safe locally" ;;
    000)         note "push failed, server unreachable. changes are safe locally" ;;
    *)           note "push returned $code. changes are safe locally" ;;
esac
