#!/usr/bin/env bash

set -Eeuo pipefail

SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"

COMPRESS_THRS=$(($(nproc) / 4))
[[ $COMPRESS_THRS -eq 0 ]] && COMPRESS_THRS=1
[[ $COMPRESS_THRS -gt 8 ]] && COMPRESS_THRS=8

fail() {
    echo "$1" >&2
    exit 1
}

if [[ ${1:-} ]]; then
    . $1
    [[ $WORK_DIR ]] && mkdir -p $WORK_DIR && cd $WORK_DIR
else
    SHORT_HOST=$(hostname -s)
    TARGET=backup-$SHORT_HOST@antea
    SOURCES_FILE=$SCRIPT_DIR/sources_$SHORT_HOST
    . $SOURCES_FILE
fi
[[ ${TARGET:-} ]] || fail "TARGET expected to be set in config"
[[ ${SOURCES:-} ]] || fail "SOURCES expected to be set in $SOURCES_FILE or config"
TIMESTAMP=$(date +"%Y-%m-%d")

mkdir lock || fail "ERROR: backup already in progress (lock dir exists)"
trap_global() { rmdir lock; }
trap trap_global EXIT

get_snars() { for i in $1/*.snar; do basename $i .snar; done | sort;  }
get_backups() { for i in $(ssh $TARGET "ls $1/*.tar.xz"); do basename $i .tar.xz; done | sort; }
get_last_date() {
    date --date=$(echo "$1" | sed 's/ /\n/g' | grep "$2" | tail -n1 | sed 's/[.].*$//') +%s
}
show_timestamp() {
    date +"%Y-%m-%d" --date="@$1"
}

get_type_unvalidated() {
    mkdir -p "$1"

    if ! ls $1/*.snar >&/dev/null; then
        echo full
    elif ! ssh $TARGET "ls $1/*.tar.xz" >&/dev/null; then
        echo full
    else
        LAST_FULL=$(get_last_date "$(get_snars $1)" full)
        LAST_FULL_REAL=$(get_last_date "$(get_backups $1)" full)
        if [[ $LAST_FULL_REAL != $LAST_FULL ]]; then
            echo "WARNING: Inconsistent full backups: local is $(show_timestamp $LAST_FULL) but remote is $(show_timestamp $LAST_FULL_REAL), fallback to full backup" >&2
            echo full
            return
        fi

        LAST_BIG=$(get_last_date "$(get_snars $1)" '\(full\|weekly\)')
        LAST_BIG_REAL=$(get_last_date "$(get_backups $1)" '\(full\|weekly\)')
        if [[ $LAST_BIG_REAL != $LAST_BIG ]]; then
            echo "WARNING: Inconsistent weekly backups: local is $(show_timestamp $LAST_BIG) but remote is $(show_timestamp $LAST_BIG_REAL), fallback to full backup" >&2
            echo full
            return
        fi

        NOW=$(date +%s)
        # do a full backup every 8 weeks
        if [[ $(($NOW - $LAST_FULL)) -ge $((3600 * 24 * 7 * 8)) ]]; then
            echo full
        elif [[ $(($NOW - $LAST_BIG)) -ge $((3600 * 24 * 7)) ]]; then
            echo weekly
        else
            echo daily
        fi
    fi
}

get_trace() {
    TYPE=$1
    DATES=$2
    if [[ $TYPE = full ]]; then
        echo "empty"
    else
        LAST_F=$(show_timestamp $(get_last_date "$DATES" 'full'))
        LAST_W=$(show_timestamp $(get_last_date "$DATES" '\(weekly\|full\)'))
        # the full itself
        echo $LAST_F
        # all weekly after the full
        echo $DATES | sed 's/ /\n/g' | grep -A99999 -F $LAST_F | grep weekly
        # all daily since the last weekly if applicable
        if [[ $TYPE = "daily" ]]; then
            echo $DATES | sed 's/ /\n/g' | grep -A99999 -F $LAST_W | grep daily
        fi
    fi
}

get_type() {
    TYPE=$(get_type_unvalidated $1)

    LOCAL_TRACE=$(get_trace $TYPE "$(get_snars $1)")
    REMOTE_TRACE=$(get_trace $TYPE "$(get_backups $1)")

    echo "LOCAL_TRACE: $LOCAL_TRACE" >&2
    echo "REMOTE_TRACE: $REMOTE_TRACE" >&2

    if [[ $LOCAL_TRACE != $REMOTE_TRACE ]]; then
        printf "WARNING: Inconsistent backups and source metadata:\nlocal metadata: $LOCAL_TRACE\nbackups: $REMOTE_TRACE\nfalling back to full backup to recover consistency…" >&2
        echo full
    else
        echo $TYPE
    fi

}

for SRC in $SOURCES; do
    SRC_ID=$(echo $SRC | sed 's|^/||')
    mkdir -p $SRC_ID

    TYPE=$(get_type $SRC_ID)
    echo "TYPE: $TYPE" >&2
    [[ "$TYPE" = "full" ]] || [[ "$TYPE" = "weekly" ]] || [[ "$TYPE" = "daily" ]] || \
        fail "invalid backup type for $SRC: $TYPE"

    BACKUP_FILE=${SRC_ID}/${TIMESTAMP}.${TYPE}.tar.xz
    INCFILE_TGT=stamps/${SRC_ID}/${TIMESTAMP}.${TYPE}.snar.xz
    INCFILE=${SRC_ID}/${TIMESTAMP}.${TYPE}.snar
    INCFILE_TMP=${INCFILE}.tmp

    [[ -f $INCFILE_TMP ]] && fail "ERROR: backup $SRC already in progress (temporary incremental backup file exists)"
    if [[ -f $INCFILE ]]; then
        echo "WARNING: backup $SRC already done (incremental backup file exists)" >&2
        continue
    fi

    # drop extraneous stamp files
    if [[ "$TYPE" = "full" ]]; then
        echo "deleting" ${SRC_ID}/*.snar
        rm -f ${SRC_ID}/*.snar
    elif [[ "$TYPE" = "weekly" ]]; then
        echo "deleting" ${SRC_ID}/*.daily.snar
        rm -f ${SRC_ID}/*.daily.snar
    fi

    INC_FILTER="."
    if [[ "$TYPE" = "weekly" ]]; then
        INC_FILTER='\(full\|weekly\)'
    fi

    if [[ "$TYPE" != "full" ]]; then
        LAST=${SRC_ID}/$(get_snars $SRC_ID | grep $INC_FILTER | tail -n1).snar
        if [[ -f "$LAST" ]]; then
            cp --reflink=auto $LAST $INCFILE_TMP
        fi
    fi

    ssh $TARGET "mkdir -p stamps/${SRC_ID} ${SRC_ID}"

    trap_local() {
        ssh $TARGET "rm -f $BACKUP_FILE.tmp $INCFILE.tmp"
        rm -f $INCFILE_TMP
        trap_global
    }
    trap trap_local EXIT;

    echo "Backup $SRC ($TYPE)…"
    {
        {
            tar --create --xattrs --acls --listed-incremental=$INCFILE_TMP $SRC | \
                    xz -T$COMPRESS_THRS -3 | \
                    ssh $TARGET "umask 0027; cat > $BACKUP_FILE.tmp"
        } 3>&1 1>&2 2>&3 | \
                { grep -v "Removing leading \`/'" || true; } | \
                { grep -v "socket ignored" || true; }
    } 3>&1 1>&2 2>&3
    xz -T$COMPRESS_THRS --keep --stdout $INCFILE_TMP | ssh $TARGET "umask 0027; cat > $INCFILE_TGT.tmp"
    if echo $SRC | grep -q '^/home'; then
        USR=$(echo $SRC | sed 's|^/home/\([^/]*\).*$|\1|')
        ssh $TARGET "setfacl -m 'u:$USR:r' $BACKUP_FILE.tmp"
        DIR=$(dirname $BACKUP_FILE)
        while true; do
            ssh $TARGET "setfacl -m 'u:$USR:rx' $DIR"
            [[ "$DIR" = "." ]] && break
            DIR=$(dirname $DIR)
        done
    fi
    ssh $TARGET "mv $BACKUP_FILE{.tmp,}; mv $INCFILE_TGT{.tmp,}"
    trap trap_global EXIT
    mv $INCFILE_TMP $INCFILE
done
