#!/usr/bin/env bash
set -euo pipefail

branch=main
export GIT_SSH_COMMAND='ssh -o BatchMode=yes -o StrictHostKeyChecking=yes -o ConnectTimeout=3'
export GIT_TERMINAL_PROMPT=0
work=''
temp=''
fail() { printf '%s\n' "$*" >&2; exit 1; }
ask() {
    local answer=n
    read -r -p "$1 [y/N] " answer || answer=n
    [[ "$answer" == y || "$answer" == Y ]]
}
clean_main() {
    local current changes
    current="$(git symbolic-ref --quiet --short HEAD)" || return 1
    [[ "$current" == "$branch" ]] || return 1
    [[ ! -e "$(git rev-parse --git-path MERGE_HEAD)" ]] || return 1
    changes="$(git status --porcelain --untracked-files=all)" || return 1
    [[ -z "$changes" ]]
}
open_repo() {
    cd "$1" || fail "Cannot open repository: $1"
    root="$(git rev-parse --show-toplevel)" || fail 'Not a Git worktree.'
    cd "$root"
    local common
    common="$(git rev-parse --path-format=absolute --git-common-dir)"
    exec 9>"$common/fleet-sync.lock"
    flock -n 9 || fail 'Another Fleetsync operation is using this repository.'
    clean_main || fail 'Use main and commit or set aside local changes first.'
}
unchanged() {
    clean_main && [[ "$(git rev-parse HEAD)" == "$1" ]]
}
show_changes() {
    git --no-pager log --oneline --no-decorate "$1..$2"
    git --no-pager diff --no-ext-diff --no-textconv --name-status "$1" "$2"
    git --no-pager diff --no-ext-diff --no-textconv "$1" "$2" \
        -- . ':(glob,exclude)**/*.gpg'
}
cleanup() {
    if [[ -n "$work" ]]; then
        if ! git worktree remove --force "$work" >&2; then
            printf 'Temporary merge retained for inspection: %s\n' "$work" >&2
            return 1
        fi
        work=''
    fi
    if [[ -n "$temp" ]]; then rmdir "$temp"; temp=''; fi
}

# Git invokes this helper over SSH only after the sender confirms a push.
# Keep stdout exclusively for Git's receive-pack protocol.
if [[ "${1:-}" == --receive-pack ]]; then
    [[ $# == 4 && "$2" =~ ^[0-9a-f]{40,64}$ && "$3" =~ ^[0-9a-f]{40,64}$ ]] \
        || fail 'Invalid receive request.'
    expected="$2"; incoming="$3"
    open_repo "$4"
    unchanged "$expected" || fail 'Peer HEAD changed since review; fetch again.'
    transfer="refs/fleetsync/incoming/$incoming"
    if git show-ref --verify --quiet "$transfer"; then
        fail 'An earlier transfer needs inspection before retrying.'
    fi
    trap 'git update-ref -d "$transfer" "$incoming" >&2' EXIT
    # Transfer objects to a temporary ref; receive-pack must not update main.
    git -c receive.denyCurrentBranch=true -c receive.denyNonFastForwards=true \
        -c receive.denyDeletes=true receive-pack "$root" || fail 'Transfer failed.'
    [[ "$(git rev-parse --verify "$transfer")" == "$incoming" ]] || fail 'Unexpected transfer tip.'
    unchanged "$expected" || fail 'Peer changed during transfer; nothing applied.'
    git merge-base --is-ancestor "$expected" "$incoming" || fail 'Peer update is not a fast-forward.'
    git merge --quiet --ff-only --no-overwrite-ignore "$incoming" >&2 || fail 'Peer checkout update refused.'
    exit 0
fi

[[ $# -le 1 ]] || fail 'Usage: fleet-sync [repository]'
[[ -t 0 && -t 1 ]] || fail 'Run this script in an interactive terminal.'
command -v flock >/dev/null || fail 'Install flock (util-linux on Linux).'
open_repo "${1:-$HOME/.local/src/dotfiles}"
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
[[ -r fleet.conf ]] || fail 'Missing readable fleet.conf.'
mapfile -t lines < fleet.conf
labels=()
urls=()
for line in "${lines[@]}"; do
    line="${line%%[[:space:]]#*}"
    [[ "$line" =~ ^[[:space:]]*(#|$) ]] && continue
    read -r label url extra <<< "$line"
    [[ -n "$url" && -z "$extra" && "$label" != -* ]] \
        || fail 'Expected: hostname SSH-URL (without spaces in the URL).'
    [[ "$url" == ssh://* || "$url" == *@*:* ]] \
        || fail "Use an SSH URL for $label."
    labels+=("$label"); urls+=("$url")
done
me="$(uname -n)"; me="${me%%.*}"
result=0

resolve_merge() {
    local base="$1" candidate="$2" label="$3" path choice stage merged
    local -a conflicts
    temp="$(mktemp -d "${TMPDIR:-/tmp}/fleet-sync.XXXXXXXX")" || fail 'Cannot create temporary directory.'
    work="$temp/merge"
    git worktree add --quiet --detach "$work" "$base" || fail 'Cannot prepare temporary worktree.'
    if ! git -C "$work" merge --no-ff --no-commit "$candidate"; then
        [[ -e "$(git -C "$work" rev-parse --git-path MERGE_HEAD)" ]] \
            || fail 'Merge could not start; live checkout is unchanged.'
    fi
    mapfile -d '' -t conflicts < <(git -C "$work" diff --name-only --diff-filter=U -z)
    for path in "${conflicts[@]}"; do
        printf '\nConflict: %q\nLocal = %s; peer = %s.\n' "$path" "$me" "$label"
        if [[ "$path" != *.gpg ]]; then
            git -C "$work" --no-pager diff --no-ext-diff --no-textconv --cc -- "$path"
        else
            printf 'Encrypted entry: contents are not displayed or text-merged.\n'
        fi
        while :; do
            printf 'l = whole LOCAL file; p = whole PEER file; e = merge editor; a = abort.\n'
            choice=a
            read -r -p 'Choose [l/p/e/a]: ' choice || choice=a
            case "$choice" in
                l|p)
                    stage=2; [[ "$choice" == p ]] && stage=3
                    if git -C "$work" cat-file -e ":$stage:$path" 2>/dev/null; then
                        if [[ "$stage" == 2 ]]; then
                            git -C "$work" checkout --ours -- "$path" || fail 'Cannot select local version.'
                        else
                            git -C "$work" checkout --theirs -- "$path" || fail 'Cannot select peer version.'
                        fi
                        git -C "$work" add -- "$path" || fail 'Cannot stage resolution.'
                    else
                        git -C "$work" rm -- "$path" || fail 'Cannot stage deletion.'
                    fi
                    break ;;
                e)
                    if [[ "$path" == *.gpg ]]; then
                        printf 'Select a complete encrypted version, or abort and resolve separately.\n'
                        continue
                    fi
                    printf 'Editor: LOCAL = this device, REMOTE = peer, MERGED = result.\n'
                    if git -C "$work" -c mergetool.keepBackup=false \
                        -c mergetool.keepTemporaries=false mergetool \
                        --tool="${FLEET_MERGETOOL:-vimdiff}" --no-prompt -- "$path"; then
                        [[ -z "$(git -C "$work" ls-files --unmerged -- "$path")" ]] && break
                    fi
                    printf 'File remains unresolved; choose again or abort.\n' ;;
                a|'') cleanup; return 1 ;;
                *) printf 'Choose l, p, e, or a.\n' ;;
            esac
        done
    done
    [[ -z "$(git -C "$work" diff --name-only --diff-filter=U)" ]] \
        || fail 'Unresolved files remain; live checkout is unchanged.'
    git -C "$work" diff --quiet || fail 'Unstaged merge edits remain; live checkout is unchanged.'
    printf '\nCombined result for %s (live checkout still unchanged):\n' "$me"
    git -C "$work" --no-pager diff --cached --no-ext-diff --no-textconv --name-status "$base"
    git -C "$work" --no-pager diff --cached --no-ext-diff --no-textconv "$base" \
        -- . ':(glob,exclude)**/*.gpg'
    if ! ask 'Accept and commit this combined result locally?'; then cleanup; return 1; fi
    unchanged "$base" || fail 'Local checkout changed during review; run again.'
    git -C "$work" commit -m "Merge reviewed changes from $label" \
        || fail 'Cannot commit merge; live checkout is unchanged.'
    merged="$(git -C "$work" rev-parse HEAD)"
    git merge --ff-only --no-overwrite-ignore "$merged" || fail 'Cannot apply reviewed merge.'
    cleanup || fail 'Cannot remove temporary merge worktree.'
}

send_back() {
    local url="$1" peer="$2" label="$3" outgoing
    outgoing="$(git rev-parse HEAD)"
    [[ "$outgoing" != "$peer" ]] || return 0
    git merge-base --is-ancestor "$peer" "$outgoing" || return 0
    printf '\nProposed update: %s -> %s\n' "$me" "$label"
    show_changes "$peer" "$outgoing"
    printf 'This also updates existing live Stow targets on %s.\n' "$label"
    if ! ask "Send and apply this reviewed result on $label now?"; then
        printf 'Peer left unchanged; it can fetch later.\n'
        return 0
    fi
    unchanged "$outgoing" || fail 'Local checkout changed during review; run again.'
    if git push --quiet --no-follow-tags \
        --receive-pack="~/.local/bin/fleet-sync --receive-pack $peer $outgoing" \
        --force-with-lease="refs/fleetsync/incoming/$outgoing:" \
        "$url" "$outgoing:refs/fleetsync/incoming/$outgoing"; then
        printf 'Updated %s.\n' "$label"
    else
        printf 'Peer update failed; local result is retained. Fetch and review again.\n' >&2
        result=1
    fi
}

for i in "${!labels[@]}"; do
    label="${labels[$i]}"; url="${urls[$i]}"
    [[ "$label" == "$me" ]] && continue
    printf '\n== %s ==\n' "$label"
    clean_main || fail 'Checkout changed; stop and inspect it.'
    base="$(git rev-parse HEAD)"
    if ! git fetch --quiet --no-tags "$url" "refs/heads/$branch"; then
        printf 'Fetch failed; skipping this peer. See the error above.\n' >&2
        continue
    fi
    candidate="$(git rev-parse --verify 'FETCH_HEAD^{commit}')"
    if [[ "$candidate" == "$base" ]]; then
        printf 'Nothing new on either device.\n'
    elif git merge-base --is-ancestor "$candidate" "$base"; then
        printf 'Local history is ahead.\n'
        send_back "$url" "$candidate" "$label"
    elif git merge-base --is-ancestor "$base" "$candidate"; then
        printf 'Proposed update: %s -> %s\n' "$label" "$me"
        show_changes "$base" "$candidate"
        if ask 'Accept these changes locally?'; then
            unchanged "$base" || fail 'Local checkout changed during review; run again.'
            git merge --ff-only --no-overwrite-ignore "$candidate" || fail 'Merge failed.'
            printf 'Accepted.\n'
        else
            printf 'Left unchanged.\n'
        fi
    else
        printf 'Histories diverged. Local and peer commits:\n'
        git --no-pager log --graph --oneline --left-right "$base...$candidate"
        if ask 'Prepare an interactive merge without changing live files?'; then
            if resolve_merge "$base" "$candidate" "$label"; then
                send_back "$url" "$candidate" "$label"
            else
                printf 'Merge cancelled; both live checkouts are unchanged.\n'
                result=1
            fi
        else
            printf 'Divergence left unresolved.\n'
            result=1
        fi
    fi
done
exit "$result"
