#!/bin/sh
# =============================================================================
# ftpush — upload files/folders to a remote site (ftpsuite). Part of the
# ftpsuite; shares its config and library with ftpull, ftps-cli, ftps-gui.
#
# Usage:
#   ftpush [-s N] [-p N] PATH [PATH...]   upload to site N (default if -s
#                                         omitted). A relative PATH is a path
#                                         INSIDE the mirror — pair 1's local root,
#                                         or -p N — and mirrors to the same
#                                         subtree on the server. Run from ANY
#                                         directory; the current dir is never
#                                         used. An ABSOLUTE path matches a pair by
#                                         longest prefix (for a file outside the
#                                         mirror).
#   ftpush --sites | --add-site | --edit-site [-s N] | --del-site [-s N]
#   ftpush --pairs [-s N] | --add-pair [-s N] | --del-pair [-s N] [-p N]
#          | --edit-pair [-s N] [-p N]
#   ftpush --export [FILE] | --import FILE
#   ftpush --version | -v | --help | -h
#
# Sites are selected by NUMBER everywhere (see --sites). Config is shared with
# the whole suite; managing it here is the same as managing it from ftpull.
# =============================================================================

# ── Locate + source the shared library ──────────────────────────────────────
_self_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
for _cand in "${FTPS_LIB:-}" "$_self_dir/ftps_common.sh" \
             "$HOME/.local/lib/ftpsuite/ftps_common.sh" \
             /usr/local/lib/ftpsuite/ftps_common.sh; do
    [ -n "$_cand" ] && [ -f "$_cand" ] && { . "$_cand"; _lib_ok=1; break; }
done
[ "${_lib_ok:-}" = 1 ] || { echo "ftpush: cannot find ftps_common.sh (set FTPS_LIB)" >&2; exit 2; }

set -eu

usage() { sed -n '3,20p' "$0" | grep -v '^# =====' | sed 's/^# \{0,1\}//'; }

# ── upload ───────────────────────────────────────────────────────────────────
cmd_upload() {
    site_num="$1"; force_pair="$2"; shift 2
    require_config; load_all_sites; pick_site "$site_num"
    eval "PAIRS=\$SITE_${site}_pairs; _H=\$SITE_${site}_host; _U=\$SITE_${site}_user"
    if [ -z "$_H" ] || [ -z "$_U" ]; then echo "Site '$site' has no host/user — run ftpush --edit-site." >&2; exit 1; fi

    # With -p N, paths are taken RELATIVE TO THAT PAIR'S LOCAL ROOT (and the
    # remote mirrors the same subtree), so you can run ftpush from ANY directory
    # without cd-ing into the site tree first. Without -p, relative paths still
    # resolve against the current directory as before. Absolute paths always
    # win as given.
    force_local_root=""; force_remote_root=""
    if [ -n "$force_pair" ]; then
        pc=$(pairs_count "$PAIRS")
        case "$force_pair" in ''|*[!0-9]*) echo "Not a valid pair number: $force_pair" >&2; exit 1 ;; esac
        { [ "$force_pair" -lt 1 ] || [ "$force_pair" -gt "$pc" ]; } && { echo "Pair -p $force_pair out of range (have $pc, see --pairs)" >&2; exit 1; }
        pair_line=$(echo "$PAIRS" | sed '/^$/d' | awk -v n="$force_pair" 'NR==n')
        force_local_root=$(printf '%s' "$pair_line"  | sed 's/|.*//; s:/*$::')
        force_remote_root=$(printf '%s' "$pair_line" | sed 's/.*|//; s:/*$::')
    fi
    # Global flat -p mode passes the pair roots in directly (site already picked).
    if [ -n "${GP_LROOT:-}" ]; then force_local_root="$GP_LROOT"; force_remote_root="$GP_RROOT"; fi

    # Resolve every path first: "local|remote|kind" per line (kind = file|dir).
    # A RELATIVE path in pair-root mode is taken relative to the pair's local
    # root and mirrored to the same subtree remotely (run from anywhere). An
    # ABSOLUTE path is always matched to a pair by longest local prefix, so a
    # full path to any pair's tree still goes to the right place.
    RESOLVED=""; fcount=0
    for raw in "$@"; do
        # Glob (* ? [..]) in a relative path is expanded against the mirror root,
        # so `ftpush 'a*'` or `ftpush 'tools/*.html'` works from any directory.
        # Quote the pattern in the shell so ftpush (not your shell) expands it.
        case "$raw" in
            /*) : ;;                                       # absolute → handled below
            *[*?[]*)
                _hit=0
                for _m in "$force_local_root"/$raw; do
                    [ -e "$_m" ] || continue               # no-match → skip (nullglob)
                    _hit=1
                    _rel="${_m#"$force_local_root"/}"
                    _rp="$force_remote_root/$_rel"
                    if [ -d "$_m" ]; then _k=dir; else _k=file; fcount=$((fcount+1)); fi
                    RESOLVED="${RESOLVED}
${_m}|${_rp}|${_k}"
                done
                [ "$_hit" = 0 ] && echo "Skipping — no match: $raw" >&2
                continue ;;
        esac
        case "$raw" in
            /*)                                            # absolute → match a pair by prefix
                lp="$raw"; rp=$(resolve_remote "$lp") || rp="" ;;
            *)                                             # relative → path inside the mirror (pair root)
                lp="$force_local_root/$raw"
                rp="$force_remote_root/$raw" ;;
        esac
        [ -e "$lp" ] || { echo "Skipping — not found: $lp" >&2; continue; }
        [ -z "$rp" ] && { echo "Skipping — no matching pair on site '$site': $lp" >&2; continue; }
        if [ -d "$lp" ]; then kind=dir; else kind=file; fcount=$((fcount+1)); fi
        RESOLVED="${RESOLVED}
${lp}|${rp}|${kind}"
    done
    [ -z "$(printf '%s' "$RESOLVED" | sed '/^$/d')" ] && { echo "Nothing to upload." >&2; exit 1; }

    echo; echo "Connecting to $site..."

    # Pass 1: which target files already exist remotely (Created vs Overwritten).
    # Heredoc-fed while loops run in the current shell (no subshell), so the
    # accumulating vars survive — unlike a pipe into while.
    EXISTED=""
    if [ "$fcount" -gt 0 ]; then
        CHK=""; idx=0
        while IFS='|' read -r lp rp kind; do
            [ -z "$lp" ] && continue; [ "$kind" = file ] || continue
            idx=$((idx+1))
            CHK="${CHK}echo FT_M${idx}S; cls -1 \"$rp\" 2>&1; echo FT_M${idx}E; "
        done <<EOF
$RESOLVED
EOF
        chk_out=$(ftps_run_lftp "$site" "$CHK" 2>&1) || true
        idx=0
        while IFS='|' read -r lp rp kind; do
            [ -z "$lp" ] && continue; [ "$kind" = file ] || continue
            idx=$((idx+1))
            blk=$(printf '%s\n' "$chk_out" | sed -n "/FT_M${idx}S/,/FT_M${idx}E/p")
            if printf '%s' "$blk" | grep -qi 'no such file\|not found\|550'; then ex=0; else ex=1; fi
            EXISTED="${EXISTED}
${rp}|${ex}"
        done <<EOF
$RESOLVED
EOF
    fi

    # Pass 2: upload, one connection.
    CMDS=""
    while IFS='|' read -r lp rp kind; do
        [ -z "$lp" ] && continue
        rd=$(dirname "$rp")
        if [ "$kind" = dir ]; then
            echo "Folder:  $lp -> $rp"
            CMDS="${CMDS}mkdir -p \"$rp\" 2>/dev/null; mirror -R --verbose \"$lp\" \"$rp\"; "
        else
            echo "File:    $lp -> $rp"
            CMDS="${CMDS}mkdir -p \"$rd\" 2>/dev/null; put -O \"$rd\" \"$lp\" -o \"$(basename "$rp")\"; "
        fi
    done <<EOF
$RESOLVED
EOF

    out="${TMPDIR:-/tmp}/.ftpush_out.$$"

    # ── Live progress ────────────────────────────────────────────────────────
    # lftp's own output only lands when a file finishes, so a big upload looked
    # like a hang. Run it in the background and report real progress by counting
    # the files it has actually completed, with a percentage when we know the
    # total. Falls back to a spinner + elapsed time for mirrored folders, where
    # the file count is not known in advance.
    _total=$(printf '%s\n' "$RESOLVED" | awk -F'|' '$3=="file"{n++} END{print n+0}')

    # WHERE the progress goes. Gating on `[ -t 1 ]` meant it never appeared in a
    # deploy terminal: those scripts run `exec > >(tee -a "$LOG") 2>&1`, so
    # stdout is a pipe and the whole feature silently switched itself off --
    # exactly where a long upload most looks like a hang.
    #
    # So write it to the controlling terminal instead when there is one. That
    # shows progress even with stdout piped, and keeps the redrawn line OUT of
    # the log file, where a few hundred \r-updates are noise. Falls back to
    # stdout when it is a terminal, and to nothing at all otherwise (cron, CI).
    if [ -w /dev/tty ] 2>/dev/null; then _prog=/dev/tty
    elif [ -t 1 ];      then _prog=/dev/stdout
    else                     _prog=""; fi

    if [ -n "$_prog" ] && [ -z "${FTPS_NO_PROGRESS:-}" ]; then
        ftps_run_lftp "$site" "$CMDS" > "$out" 2>&1 &
        _pid=$!
        _spin='|/-\'
        # Fractional sleep is GNU coreutils, not POSIX. Probe once and fall back
        # to a 1s tick rather than erroring on a shell whose sleep rejects it.
        if sleep 0.2 2>/dev/null; then _tick=0.2; else _tick=1; fi
        _i=0; _t0=$(date +%s)
        while kill -0 "$_pid" 2>/dev/null; do
            # grep -c prints 0 AND exits 1 when nothing matches. Both obvious
            # spellings are wrong and both have now been shipped:
            #   `|| echo 0`  -> output becomes "0\n0", arithmetic below blows up
            #   (bare)       -> the assignment inherits grep's exit 1 and, under
            #                   `set -e`, kills the shell on the FIRST iteration
            #                   -- before any file has transferred. That is why
            #                   this branch had never once worked: it took lftp
            #                   down with it and printed nothing at all.
            # `|| true` keeps the "0" and drops the status, which is what we want.
            _done=$(grep -c '^Transferring file' "$out" 2>/dev/null || true)
            [ -n "$_done" ] || _done=0
            _el=$(( $(date +%s) - _t0 ))
            _i=$(( (_i + 1) % 4 ))
            _c=$(printf '%s' "$_spin" | cut -c$((_i+1)))
            if [ "$_total" -gt 0 ] 2>/dev/null; then
                [ "$_done" -gt "$_total" ] && _done=$_total
                _pct=$(( _done * 100 / _total ))
                # spinner bold light yellow (1;93), percentage bold light blue
                # (1;94), and everything after the percentage in plain light
                # blue (94) -- same hue, less weight, so the percentage still
                # reads as the headline number.
                printf '\r  \033[1;93m%s\033[0m  \033[1;94m%3d%%\033[0m  \033[94m%d/%d files  %ds \033[0m' "$_c" "$_pct" "$_done" "$_total" "$_el" > "$_prog"
            else
                printf '\r  \033[1;93m%s\033[0m  \033[94m%d file(s)  %ds \033[0m' "$_c" "$_done" "$_el" > "$_prog"
            fi
            sleep "$_tick"
        done
        # Same trap as the non-progress branch below, and I left it here when I
        # fixed that one: under `set -e` a failing `wait` exits the shell BEFORE
        # st is assigned, so ftpush died without printing lftp's error at all --
        # a failed upload looked like a silent rc=1. Observed 2026-09-12 on the
        # first deploy that ever actually took this branch.
        if wait "$_pid"; then st=0; else st=$?; fi
        printf '\r\033[K' > "$_prog"   # wipe the progress line, leave the output clean
    else
        # `cmd; st=$?` is fatal under `set -e`: the shell exits the moment lftp
    # returns non-zero, BEFORE st is assigned, so every failure path below was
    # unreachable and a failed transfer died mutely. An `if` makes the status
    # examinable, which is the whole point of capturing it.
    if ftps_run_lftp "$site" "$CMDS" > "$out" 2>&1; then st=0; else st=$?; fi
    fi
    ftps_filter_mkdir_noise < "$out"
    if [ "$st" -ne 0 ]; then
        echo "ftpush: transfer FAILED (lftp exit $st) — nothing was uploaded." >&2
        [ -s "$out" ] || echo "ftpush: lftp produced no output — usually a login or connection failure. FTP servers commonly rate-limit rapid reconnects; wait a few seconds and retry." >&2
        rm -f "$out"; exit "$st"
    fi
    rm -f "$out"

    # Summary: name + size + Created/Overwritten (files) or Mirrored (dirs).
    printf '%s\n' "$RESOLVED" | while IFS='|' read -r lp rp kind; do
        [ -z "$lp" ] && continue
        if [ "$kind" = dir ]; then echo "$(basename "$lp") — Mirrored"; continue; fi
        sz=$(wc -c < "$lp" 2>/dev/null | tr -d ' ')
        ex=$(printf '%s\n' "$EXISTED" | awk -F'|' -v r="$rp" '$1==r{print $2}')
        [ "$ex" = 1 ] && lbl=Overwritten || lbl=Created
        echo "$(basename "$lp") — ${sz} bytes — ${lbl}"
    done
    echo "Done."
}

# ── Arg parsing ──────────────────────────────────────────────────────────────
case "${1:-}" in
    --help|-h|"") usage; exit 0 ;;
    --version|-v) echo "ftpush (ftpsuite) $FTPS_VERSION"; exit 0 ;;
esac
ftps_dispatch_config "$@" || true   # exits if it was a config command

SITE=""; FORCE=""
while true; do
    case "${1:-}" in
        -s)   SITE="${2:-}"; shift 2 2>/dev/null || { echo "Usage: ftpush [-s N] [-p N] PATH..." >&2; exit 1; } ;;
        -s?*) SITE="${1#-s}"; shift ;;                # joined form: -s2
        -p)   FORCE="${2:-}"; shift 2 2>/dev/null || { echo "Usage: ftpush [-s N] [-p N] PATH..." >&2; exit 1; } ;;
        -p?*) FORCE="${1#-p}"; shift ;;               # joined form: -p1
        *) break ;;
    esac
done
# The mirror is the anchor: a relative PATH is always a path inside a pair's
# local root, so ftpush runs from any directory. -p N is a FLAT number across
# ALL sites (ftpush --pairs lists them) when -s is omitted; with -s it's the
# pair index within that site. Default is -p1. Absolute paths match by prefix.
GP_LROOT=""; GP_RROOT=""
if [ -z "$SITE" ]; then
    require_config; load_all_sites
    [ -z "$FORCE" ] && FORCE=1
    _gp=$(ftps_global_pair "$FORCE")
    [ -z "$_gp" ] && { echo "No pair -p$FORCE — see: ftpush --pairs" >&2; exit 1; }
    _gsite=${_gp%%|*}; _grest=${_gp#*|}
    GP_LROOT=$(printf '%s' "${_grest%%|*}" | sed 's:/*$::')
    GP_RROOT=$(printf '%s' "${_grest#*|}"  | sed 's:/*$::')
    SITE=$(ftps_site_number "$_gsite")
    FORCE=""
else
    [ -z "$FORCE" ] && FORCE=1
fi
cmd_upload "$SITE" "$FORCE" "$@"
