#!/usr/bin/env bash
# nc-bisync.sh – Fileserver-Freigaben <-> Nextcloud mit rclone bisync (rclone >= 1.66)
#
# Jede Freigabe ist ein eigenes Verzeichnispaar (siehe PAIRS). Ein Unterordner
# "archiv" wird im normalen Lauf nie abgeglichen, sondern nur mit --archiv.
#
#   nc-bisync.sh [NAME ...]            Paare abgleichen (ohne Archiv); ohne NAME alle
#   nc-bisync.sh --archiv [NAME ...]   zusätzlich die Archiv-Unterordner (jeweils zuerst)
#   nc-bisync.sh --init [NAME ...]     Erstabgleich (--resync)
#   nc-bisync.sh --list                konfigurierte Paare anzeigen
#
# Läuft als der Benutzer, dem die Dateien auf dem Fileserver gehören (nicht root).
#
# Weitere Optionen werden an rclone durchgereicht (mit Wert immer als --opt=wert):
#   --dry-run   nur anzeigen, was passieren würde
#   --force     --max-delete-Grenze einmalig übergehen (nach manueller Prüfung)

set -uo pipefail
umask 002   # neue Dateien bleiben für die Gruppe beschreibbar

# ---------- Konfiguration ----------
# Name | lokaler Pfad | Pfad in der Nextcloud
PAIRS=(
    "administration|/mnt/shares/administration|nc:Administration"
    "bibliothek|/mnt/shares/bibliothek|nc:Bibliothek"
    "dokumente|/mnt/shares/dokumente|nc:Dokumente"
    "fotos|/mnt/shares/fotos|nc:Fotos"
    "projekte|/mnt/shares/projekte|nc:Projekte"
    "unternehmen|/mnt/shares/unternehmen|nc:Unternehmen"
    # Feiner aufteilen: Ein Unterordner als eigenes Paar wird im übergeordneten
    # Paar automatisch ausgeschlossen. Danach --init für beide Paare ausführen.
    # "tango|/mnt/shares/projekte/Tango|nc:Projekte/Tango"
)
ARCHIVE_SUBDIR="archiv"                  # Unterordner, der nur mit --archiv läuft
BACKUP_ROOT="/mnt/USB-Crucial/.bisync-backup"  # außerhalb aller Paare!
KEEP_DAYS=30                             # Backups und Logs so lange aufheben
MAX_DELETE=10                            # Abbruch, wenn > x % gelöscht würden
CHECKERS=8                               # parallele Verzeichnis-Listings
NOTIFY_MAIL=""                           # z.B. admin@example.com (benötigt 'mail')
NTFY_URL=""                              # z.B. https://ntfy.sh/mein-geheimes-topic

STATE_DIR="/var/lib/nc-bisync"
LOG_DIR="/var/log/nc-bisync"
export RCLONE_CONFIG="${RCLONE_CONFIG:-$HOME/.config/rclone/rclone.conf}"
# -----------------------------------

# --- Argumente ---
MODE=normal
DRYRUN=0
EXTRA=()
SELECTED=()
for a in "$@"; do
    case "$a" in
        --archiv)  MODE=archiv ;;
        --init)    MODE=init ;;
        --list)    MODE=list ;;
        --dry-run) DRYRUN=1; EXTRA+=("$a") ;;
        -*)        EXTRA+=("$a") ;;
        *)         SELECTED+=("$a") ;;
    esac
done

CHECK_ACCESS=(--check-access)
# Probelauf beim Erstabgleich: es werden keine RCLONE_TEST-Dateien angelegt
[[ "$MODE" == init ]] && (( DRYRUN )) && CHECK_ACCESS=()

# --- Paare einlesen ---
NAMES=(); LOCALS=(); REMOTES=()
for p in "${PAIRS[@]}"; do
    IFS='|' read -r n l r <<< "$p"
    NAMES+=("$n"); LOCALS+=("${l%/}"); REMOTES+=("${r%/}")
done

die() { echo "Fehler: $*" >&2; exit 2; }

# Verschachtelte Paare müssen lokal und in der Nextcloud gleich verschachtelt sein
for i in "${!NAMES[@]}"; do
    for j in "${!NAMES[@]}"; do
        [[ $i == "$j" ]] && continue
        if [[ "${LOCALS[$j]}" == "${LOCALS[$i]}"/* ]]; then
            rel="${LOCALS[$j]#"${LOCALS[$i]}"/}"
            [[ "${REMOTES[$j]}" == "${REMOTES[$i]}/$rel" ]] \
                || die "Paar '${NAMES[$j]}' liegt lokal in '${NAMES[$i]}', in der Nextcloud aber nicht unter ${REMOTES[$i]}/$rel"
        fi
    done
done

index_of() {  # index_of <Name> -> Index oder Fehler
    local k
    for k in "${!NAMES[@]}"; do [[ "${NAMES[$k]}" == "$1" ]] && { echo "$k"; return 0; }; done
    return 1
}

TODO=()
if (( ${#SELECTED[@]} == 0 )); then
    TODO=("${!NAMES[@]}")
else
    for s in "${SELECTED[@]}"; do
        idx="$(index_of "$s")" || die "Unbekanntes Paar: $s (siehe --list)"
        TODO+=("$idx")
    done
fi

if [[ "$MODE" == list ]]; then
    for i in "${!NAMES[@]}"; do
        arch="-"; [[ -d "${LOCALS[$i]}/$ARCHIVE_SUBDIR" ]] && arch="mit $ARCHIVE_SUBDIR"
        printf '%-16s %-40s %-28s %s\n' "${NAMES[$i]}" "${LOCALS[$i]}" "${REMOTES[$i]}" "$arch"
    done
    exit 0
fi

mkdir -p "$STATE_DIR" "$LOG_DIR" "$BACKUP_ROOT"

notify() {  # notify <Betreff> <Text>
    echo "$(date '+%F %T') $1: $2" >> "$LOG_DIR/summary.log"
    if [[ -n "$NOTIFY_MAIL" ]] && command -v mail >/dev/null; then
        printf '%s\n' "$2" | mail -s "[nc-bisync] $1" "$NOTIFY_MAIL" || true
    fi
    if [[ -n "$NTFY_URL" ]]; then
        curl -fsS -H "Title: nc-bisync: $1" -d "$2" "$NTFY_URL" >/dev/null || true
    fi
}

# Filterdatei je Paar; wird nur bei geändertem Inhalt neu geschrieben,
# weil bisync bei geänderten Filtern einen --resync verlangt.
build_filter() {  # build_filter <Basispfad> <Archiv ausschließen 0|1> <Datei>
    local base=$1 tmp k
    tmp="$(mktemp)"
    {
        echo "# Temporärdateien von Unison, Office und Betriebssystemen"
        echo '- **/.unison.*'
        echo '- **/.~lock.*#'
        echo '- **/~$*'
        echo '- **/.DS_Store'
        echo '- **/Thumbs.db'
        echo '- **/desktop.ini'
        if [[ "$2" == 1 ]]; then
            echo "# Archiv läuft als eigenes Paar (--archiv)"
            printf -- '- /%s/**\n' "$ARCHIVE_SUBDIR"
        fi
        for k in "${!LOCALS[@]}"; do
            if [[ "${LOCALS[$k]}" == "$base"/* ]]; then
                echo "# eigenes Paar: ${NAMES[$k]}"
                printf -- '- /%s/**\n' "${LOCALS[$k]#"$base"/}"
            fi
        done
    } > "$tmp"
    if ! cmp -s "$tmp" "$3"; then mv "$tmp" "$3"; else rm -f "$tmp"; fi
}

run_job() {  # run_job <Jobname> <Pfad1> <Pfad2> <Filterdatei> [zusätzliche Optionen...]
    local name=$1 p1=$2 p2=$3 filters=$4; shift 4
    local log="$LOG_DIR/${name}-$(date +%F).log"
    local start rc dur
    start=$(date +%s)

    rclone bisync "$p1" "$p2" \
        --filters-file "$filters" \
        --workdir "$STATE_DIR/workdir" \
        ${CHECK_ACCESS[@]+"${CHECK_ACCESS[@]}"} \
        --max-delete "$MAX_DELETE" \
        --conflict-resolve newer \
        --resilient --recover --max-lock 2h \
        --create-empty-src-dirs \
        --backup-dir1 "$BACKUP_ROOT/$(date +%F)/$name" \
        --checkers "$CHECKERS" \
        --log-file "$log" -v \
        "$@" ${EXTRA[@]+"${EXTRA[@]}"}
    rc=$?
    dur=$(( $(date +%s) - start ))

    echo "$(date '+%F %T') $name rc=$rc dauer=$((dur / 60))min$((dur % 60))s" >> "$LOG_DIR/summary.log"

    if (( rc != 0 )); then
        if tail -n 40 "$log" | grep -qi -- "resync"; then
            notify "Resync nötig ($name)" "bisync verlangt einen --resync. Ursache im Log prüfen: $log"
        elif tail -n 40 "$log" | grep -qi "max-delete"; then
            notify "Zu viele Löschungen ($name)" "Löschgrenze von ${MAX_DELETE}% überschritten. Prüfen, dann ggf. mit --force starten. Log: $log"
        elif tail -n 40 "$log" | grep -qi "check-access\|RCLONE_TEST"; then
            notify "Zugriffsprüfung fehlgeschlagen ($name)" "RCLONE_TEST fehlt oder weicht ab – Laufwerk/Pfad/Remote prüfen. Log: $log"
        else
            notify "Fehler ($name)" "bisync beendet mit rc=$rc. Log: $log"
        fi
    fi
    return $rc
}

process_pair() {  # process_pair <Index>
    local i=$1
    local name="${NAMES[$i]}" l="${LOCALS[$i]}" r="${REMOTES[$i]}"
    local al="$l/$ARCHIVE_SUBDIR" ar="$r/$ARCHIVE_SUBDIR"
    local ff="$STATE_DIR/filters-$name.txt" fa="$STATE_DIR/filters-$name-archiv.txt"
    local has_arch=0 rc=0
    local resync=()

    [[ -d "$l" ]] || { notify "Übersprungen ($name)" "Lokales Verzeichnis $l fehlt."; return 3; }
    [[ -d "$al" ]] && has_arch=1

    build_filter "$l" 1 "$ff"
    (( has_arch )) && build_filter "$al" 0 "$fa"

    if [[ "$MODE" == init ]]; then
        resync=(--resync --resync-mode newer)
        if (( ! DRYRUN )); then
            rclone mkdir "$r" && rclone touch "$l/RCLONE_TEST" && rclone touch "$r/RCLONE_TEST" \
                || { notify "Init fehlgeschlagen ($name)" "RCLONE_TEST konnte nicht angelegt werden."; return 3; }
            if (( has_arch )); then
                rclone mkdir "$ar" && rclone touch "$al/RCLONE_TEST" && rclone touch "$ar/RCLONE_TEST" \
                    || { notify "Init fehlgeschlagen ($name-archiv)" "RCLONE_TEST konnte nicht angelegt werden."; return 3; }
            fi
        fi
    fi

    # Archiv zuerst: verschobene Dateien liegen dann schon im Archiv der
    # Nextcloud, bevor der Hauptlauf sie dort entfernt.
    if [[ "$MODE" == init || "$MODE" == archiv ]] && (( has_arch )); then
        if ! run_job "$name-archiv" "$al" "$ar" "$fa" ${resync[@]+"${resync[@]}"}; then
            notify "Übersprungen ($name)" "Archiv-Abgleich fehlgeschlagen, daher kein Abgleich von $name."
            return 3
        fi
    fi

    run_job "$name" "$l" "$r" "$ff" ${resync[@]+"${resync[@]}"} || rc=$?
    return $rc
}

WORST=0
for i in "${TODO[@]}"; do
    # Sperre je Paar: Cronjobs mit verschiedenen Paaren blockieren sich nicht
    (
        flock -n 9 || {
            echo "$(date '+%F %T') ${NAMES[$i]}: vorheriger Lauf noch aktiv, übersprungen" >> "$LOG_DIR/summary.log"
            exit 0
        }
        process_pair "$i"
    ) 9>"$STATE_DIR/lock-${NAMES[$i]}"
    rc=$?
    (( rc > WORST )) && WORST=$rc
done

# Aufräumen: alte Backup-Tagesordner und Logs
find "$BACKUP_ROOT" -mindepth 1 -maxdepth 1 -type d -mtime +"$KEEP_DAYS" -exec rm -rf {} + 2>/dev/null
find "$LOG_DIR" -name '*.log' ! -name summary.log -mtime +"$KEEP_DAYS" -delete 2>/dev/null

exit $WORST
