#!/usr/bin/bash

GROB_VERSION=0.2.2

# source bash base library
# shellcheck disable=SC1091
source /usr/libexec/bash-base.bash || {
    echo "$0: fatal error: failed to source /usr/libexec/bash-base.bash" >&2
    exit 1
}

bb_require_libs bash-yaml

# usage [<retcode> [<msg> [..]]]
function usage() {
    local ret_code="${1-1}"
    (( $# == 0 )) || shift

    cat >&2 <<EOF
${0##*/}: usage: ${0##*/} [<global options>] <grob-config.yml> <command>
  where command can be:
    check [<archive> [..]]
    create <type> [<host> [<drive>]]
    delete <archive> [..]
    export-tar <archive> (<destination-file>|-)
    extract <archive> <destination-dir>
    info
    init
    list
  and <type> is one of: hourly, daily, weekly, monthly, yearly, custom*
  and global options can be one of
    -h              .. just print this help and exit
    -n              .. just print out what would be done but do not do
                       any write operation on repo and do not send mails
                       (implies -v)
    -v              .. verbose: emit info messages to stderr
    -vv             .. very verbose: debug and info messages to stderr
    -V              .. display version and exit
    -w              .. wait for grob repo lock
    -W <seconds>    .. same as -w but only wait specified seconds, then abort
EOF

    (( $# == 0 )) || bb_msg info "usage hint: $*"

    bb_quit "$ret_code"
}

function cleanup() {
    [ -z "${TMPDIR-}" ] || rm -rf "$TMPDIR"
}

# kill_pid <pid>
# kills pid, first with TERM and, if it survives for 0.1s, with KILL
function kill_pid() {
    kill "$1" 2> /dev/null || return 0
    sleep 0.1
    kill -KILL "$1" 2> /dev/null || return 0
}

function kill_children() {
    [ -z "${SOCAT_PID-}" ] || {
        kill_pid "$SOCAT_PID"
        SOCAT_PID=
    }
    [ -z "${SSH_PID-}" ] || {
        kill_pid "$SSH_PID"
        SSH_PID=
    }
}

# wait_for_sock <socket-filename> <timeout>
# errors: 2, 4, 91 (file exists but is no socket), 92 (timeout)
function wait_for_socket() {
    (( $# == 2 )) || {
        bb_ierr "wait_for_socket: called with $# instead of 2 args"
        return 2
    }

    local _sockname="$1"
    local _timeout="$2"

    [[ "$_timeout" =~ ^[[:digit:]]$ ]] || {
        bb_ierr "wait_for_socket: timeout must be number but is: $_timeout"
        return 4
    }

    (( _timeout > 0 )) || {
        bb_ierr "wait_for_socket: timeout must be > 0 but is: $_timeout"
        return 4
    }

    local deadline=$((SECONDS + _timeout))

    while true
    do
        if [ -e "$_sockname" ]
        then
            if [ -S "$_sockname" ]
            then
                return # socket found
            else
                return 91 # found file, but it is no socket
            fi
        else
            sleep 0.05 # wait before trying again
        fi

        (( SECONDS <= deadline )) || return 92 # timeout hit
    done
}

# borg_pull_tcp <gmd> <user> <host> <remote-path> <name>
#               <pre-hook> <post-hook> [<borg-arg> [..]]
# start borg on remote side and borg serve on local side while tunneling
# their communication through SSH
# use tcp socket on the remote side (listening to loopback)
# since any user could abuse that port with a timed attack we forward
# a one time pad (random text) via the SSH connection and expect it back
# via the tunnel to make sure it is our client using the port
# note: tunneling with unix socket on the remote side would be better, but
# this does not work with older SSH versions or distros (e.g. CentOS 7.6)
# errors: 2, fixme: arg to return borg retval
#         91 (tmpdir not found), 92 (grob meta dir not found),
#         93 (repo not present), 94 (timeout waiting for remote port number),
#         95 (ssh error), 96 (coproc io error)
function borg_pull_tcp() {
    (( $# >= 5 )) || {
        bb_ierr "borg_pull called with $# instead of 4 or more args"
        return 2
    }

    local repo_path="$1"
    local user="$2"
    local host="$3"
    local remote_path="$4"
    local run_name="$5"
    local grob_pre_hook="$6"
    local grob_post_hook="$7"
    shift 7

    [[ -d "$TMPDIR" ]] || return 91

    [[ -d  "$repo_path" ]] || {
        bb_msg err "borg_pull_tcp: repo not present at: $repo_path"
        return 93
    }

    # create one time pad to make sure it is us on backchannel
    local otp
    otp=$(uuidgen) || otp="$RANDOM$RANDOM$RANDOM$RANDOM$RANDOM"

    # export variables used in local helper
    export GROB_REPO_PATH="$repo_path"
    export GROB_OTP="$otp"

    local sockname="$TMPDIR/borg-pull-ssh-forward.sock"

    if [ -e "$sockname" ]
    then
        bb_msg warning "borg_pull: socket already exists, removing it"
        rm -f "$sockname"
    fi

    # connect unix socket used by ssh with local helper
    socat "UNIX-LISTEN:$sockname" EXEC:/usr/libexec/grob-callback-handler &
    SOCAT_PID="$!"

    local ssh_err_fifo="$TMPDIR/ssh-stderr.fifo"
    if [ -e "$ssh_err_fifo" ]
    then
        bb_msg warning "borg_pull: ssh err fifo already exists, removing it"
        rm -f "$ssh_err_fifo"
    fi
    mkfifo "$ssh_err_fifo"

    local ssh_err_log="$TMPDIR/ssh-stderr.log"
    if [ -e "$ssh_err_log" ]
    then
        bb_msg warning "borg_pull: ssh err logfile already exists, removing it"
        rm -f "$ssh_err_log"
    fi

    local -a borg_args=( \
        create \
            "$@" \
            "ssh://nop/$repo_path::$run_name" \
            "$remote_path"
        )

    # launch client side via ssh as coprocess
    # shellcheck disable=SC2029
    coproc sshproc {
        ssh -oBatchMode=yes \
            -oConnectTimeout=50 \
            -E "$ssh_err_fifo" \
            -R 0:"$sockname" \
            "$user@$host" \
            /usr/libexec/grob/grob-client
    }
    SSH_PID="$!"
    local ssh_coproc_pid="$SSH_PID"
    local ssh_coproc_stdin="${sshproc[1]-}"
    local ssh_coproc_stdout="${sshproc[0]-}"
    [[ -n "$ssh_coproc_stdin" && -n "$ssh_coproc_stdout" ]] || {
        bb_msg error "borg_pull: coproc io internal error"
        return 96
    }

    # wait until ssh tells us the remote port
    local remote_port=
    # fixme: stop trying after some time
    local deadline="$((SECONDS + 60))"
    while true
    do
        local -a line
        local t_left="$((deadline - SECONDS))"
        (( t_left >= 0 )) || t_left=0
        if read -r -t "$t_left" -a line
        then
            bb_msg debug "got a line from fifo: ${line[*]}"
            if [[ "${line[*]}" =~ ^Allocated\ port\ .+\ for\ remote\ forward ]]
            then
                remote_port="${line[2]}"
                break # we have the port, stop waiting
            else
                printf "%s" "${line[*]}" > "$ssh_err_log"
                continue # not what we have been looking for
            fi
        else
            local ssh_rv="$?"
            if (( ssh_rv > 128 ))
            then
                bb_msg err "borg_pull_tcp: timeout reading ssh fifo:" \
                    "$(cat "$ssh_err_log")"
                kill_children
                SOCAT_PID=
                SSH_PID=
                return 94
            else
                bb_msg err "EOF while trying to get remote port number:" \
                    "$(cat "$ssh_err_log")"
                kill_children
                SOCAT_PID=
                SSH_PID=
                return 95
            fi
        fi
    done < "$TMPDIR/ssh-stderr.fifo"

    bb_msg debug "borg_pull_tcp: remote tcp port is $remote_port"

    ## deliver variables

    # clienttype
    # shellcheck disable=SC2034
    local client_type=tcp
    declare -p client_type >&"${ssh_coproc_stdin}"

    # borg args
    declare -p borg_args >&"${ssh_coproc_stdin}"

    # files cache TTL
    [ -z "${grob_files_cache_ttl-}" ] || {
        local BORG_FILES_CACHE_TTL
        # shellcheck disable=SC2034
        BORG_FILES_CACHE_TTL="${grob_files_cache_ttl}"
        declare -p BORG_FILES_CACHE_TTL >&"${ssh_coproc_stdin}"
        echo "export BORG_FILES_CACHE_TTL" >&"${ssh_coproc_stdin}"
    }

    # disable chunks archive on users request
    [ -z "${grob_disable_chunks_archive-}" ] || {
        local BORG_USE_CHUNKS_ARCHIVE
        BORG_USE_CHUNKS_ARCHIVE=no
        declare -p BORG_USE_CHUNKS_ARCHIVE >&"${ssh_coproc_stdin}"
        echo "export BORG_USE_CHUNKS_ARCHIVE" >&"${ssh_coproc_stdin}"
    }

    # passphrase if it is used
    [ -z "${PASSPHRASE_PATH-}" ] || {
        local borg_passphrase
        # shellcheck disable=SC2034
        borg_passphrase="$(<"$PASSPHRASE_PATH")"
        declare -p borg_passphrase >&"${ssh_coproc_stdin}"
    }

    # tunnel OTP
    local GROB_OTP
    GROB_OTP="$otp"
    declare -p GROB_OTP >&"${ssh_coproc_stdin}"
    echo "export GROB_OTP" >&"${ssh_coproc_stdin}"

    # tunnel remote port
    local GROB_TUNNEL_PORT
    # shellcheck disable=SC2034
    GROB_TUNNEL_PORT="$remote_port"
    declare -p GROB_TUNNEL_PORT >&"${ssh_coproc_stdin}"
    echo "export GROB_TUNNEL_PORT" >&"${ssh_coproc_stdin}"

    # client hooks
    [ -z "$grob_pre_hook" ] || declare -p grob_pre_hook >&"${ssh_coproc_stdin}"
    [ -z "$grob_post_hook" ] || declare -p grob_post_hook >&"${ssh_coproc_stdin}"

    echo "start" >&"${ssh_coproc_stdin}"

    # read ssh stdout
    while read -r -u "${ssh_coproc_stdout}" line
    do
        echo "$line"
    done

    wait "$SSH_PID" 2>/dev/null || true

    kill_children
    SOCAT_PID=
    SSH_PID=

    # return the exit code of the remote side
    local ssh_coproc_retval=0
    wait "$ssh_coproc_pid" || ssh_coproc_retval="$?"

    case "$ssh_coproc_retval" in
        255)
            if [ -r "$ssh_err_log" ]
            then
                bb_msg err "borg_pull_tcp: ssh failed: $(cat "$ssh_err_log")"
            else
                bb_msg err "borg_pull_tcp: ssh failed, but got no borg log"
            fi
            ;;
    esac
    return "$ssh_coproc_retval"
}

# backup_drive launches a backup
# backup_drive <host> <drive> <archive-name> [<borg-args>]
# errors: 2, 91 (config error)
function create_archive() {
    (( $# >= 3 )) || {
        bb_ierr "backup_drive called with $# args instead of 3"
        return 2
    }

    local host="$1"
    local drive="$2"
    local backup_name="$3"
    shift 3
    local -a borg_args=("--stats" "$@")

    local -a parents

    # shellcheck disable=SC2034
    parents=(hosts "$host" drives "$drive")
    local drive_path
    byml_get_value drive_path grobcfg parents "path" || {
        bb_msg err "backup: no path found for: $host $drive"
        return 91
    }

    local remote_user
    byml_get_value_with_parents remote_user grobcfg parents "remote_user" || {
        bb_msg err "backup: config file misses mandatory entry: remote_user"
        return 91
    }

    local ssh_tunnel_mode
    byml_get_value_with_parents_and_default ssh_tunnel_mode grobcfg \
        parents "ssh_tunnel_mode" "unix" || {
        bb_msg err "backup: config file has problem (ssh_tunnel_mode)"
        return 91
    }

    # check for hooks
    local grob_pre_hook
    byml_get_value_with_parents_and_default grob_pre_hook grobcfg \
        parents "client_pre_hook" "" || {
        bb_msg err "backup: config file has problem (client_pre_hook)"
        return 91
    }
    local grob_post_hook
    byml_get_value_with_parents_and_default grob_post_hook grobcfg \
        parents "client_post_hook" "" || {
        bb_msg err "backup: config file has problem (client_post_hook)"
        return 91
    }

    # check for files cache TTL
    local grob_files_cache_ttl grob_files_cache_ttl_default
    if [ -z "${nr_all_drives-}" ]
    then
        grob_files_cache_ttl_default="20"
    else
        grob_files_cache_ttl_default="$((nr_all_drives * 2))"
        (( grob_files_cache_ttl_default >= 20 )) || \
            grob_files_cache_ttl_default=20
    fi
    byml_get_value_with_parents_and_default grob_files_cache_ttl grobcfg \
        parents "files_cache_ttl" "$grob_files_cache_ttl_default" || {
        bb_msg err "backup: config file has problem (grob_files_cache_ttl)"
        return 91
    }

    # check for stay_one_fs (default=true)
    local grob_stay_one_fs
    byml_get_value_with_parents_and_default grob_stay_one_fs grobcfg \
        parents "stay_one_fs" "false" || {
        bb_msg err "backup: config file has problem (stay_one_fs)"
        return 91
    }
    case "$grob_stay_one_fs" in
        True|true|On|on) borg_args+=("--one-file-system");;
        False|false|Off|off) ;;
        *)
            bb_msg err "backup: unknown value of stay_one_fs:" \
                "$grob_stay_one_fs"
            return 91
    esac

    # check for exclude_cachedirs (default=false)
    local grob_exclude_cachedirs
    byml_get_value_with_parents_and_default grob_exclude_cachedirs grobcfg \
        parents "exclude_cachedirs" "false" || {
        bb_msg err "backup: config file has problem (exclude_cachedirs)"
        return 91
    }
    case "$grob_exclude_cachedirs" in
        True|true|On|on|Yes|yes) ;;
        False|false|Off|off|No|no) borg_args+=("--exclude-caches");;
        *)
            bb_msg err "backup: unknown value of exclude_cachedirs: " \
                "$grob_exclude_cachedirs"
            return 91
    esac

    # check for disable_chunks_archiv (default=no)
    local grob_disable_chunks_archive
    byml_get_value_with_parents_and_default grob_disable_chunks_archive grobcfg \
        parents "disable_chunks_archive" "no" || {
        bb_msg err "backup: config file has problem ($?)" \
               "near disable_chunks_archive"
        return 91
    }
    case "$grob_disable_chunks_archive" in
        True|true|On|on|Yes|yes) grob_disable_chunks_archive=yes;;
        False|false|Off|off|No|no) ;;
        *)
            bb_msg err "backup: unknown value of disable_chunks_archive: " \
                "$grob_disable_chunks_archive"
            return 91
    esac

    local borg_repo_path="$grob_repo_path/borg_repo"

    bb_msg info "backing up $host:$drive" \
        "drive_path=$drive_path borg_repo_path=$borg_repo_path" \
        "remote_user=$remote_user ssh_tunnel_mode=$ssh_tunnel_mode"

    # do not run backup if we do a dry run
    [ -z "$option_dryrun" ] || return 0

    borg_pull_tcp \
        "$borg_repo_path" \
        "$remote_user" \
        "$host" \
        "$drive_path" \
        "$backup_name" \
        "$grob_pre_hook" \
        "$grob_post_hook" \
        "${borg_args[@]}" || {
        local retval="$?"

        bb_msg err "backup_drive failed($retval): $host $drive $backup_name"
        return "$retval"
    }
}

# setup everything so borg can read passphrase
# errors: 2, 91 (unable to read encryption mode from config),
#         92 (unable to read passphrase location from config),
#         93 (unable to read passphrase from location)
function require_encryption() {
    local caller="$1"

    local retval errmsg
    local enc_mode

    if byml_get_value enc_mode grobcfg no_parents "borg_encryption_mode"
    then
        if [[ "$enc_mode" == "none" ]]
        then
            return
        fi
    else
        retval="$?"

        byml_error_string errmsg "$retval"
        bb_msg err "$caller: error reading borg_encryption_mode from config:" \
            "$errmsg"
        return 91
    fi

    declare -g PASSPHRASE_PATH=
    if byml_get_value PASSPHRASE_PATH grobcfg no_parents "borg_passphrase_file"
    then
        PASSPHRASE_PATH="${PASSPHRASE_PATH/#\~/$HOME}"
        [ -f "$PASSPHRASE_PATH" ] ||
        mkpasswd -l 42 -s 0 > "$PASSPHRASE_PATH" || {
            bb_msg err "require_encryption: failed to write autogenerated" \
                "passphrase to: $PASSPHRASE_PATH"
            return 92
        }
    else
        retval="$?"

        (( retval == 54 )) || {
            byml_error_string errmsg "$retval"
            bb_msg err "init: error reading borg_passphrase_file" \
                " from config: $errmsg"
            return 93
        }
    fi

    export BORG_PASSCOMMAND="cat $PASSPHRASE_PATH"
}

function log_details() {
    if [ -z "$option_dryrun" ]
    then
        printf "%s: %s\n" "$(date "+%F %T")" "$*" >> "$details_file" || \
            bb_msg err "log_details failed ($*)"
    else
        bb_msg debug "details(dry run): $*"
    fi
}

function log_summary() {
    if [ -z "$option_dryrun" ]
    then
        printf "%s\n" "$*" >> "$summary_file" || \
            bb_msg err "log_summary failed ($*)"
    else
        bb_msg info "summary(dry run): $*"
    fi
}

function log_conclusion() {
    if [ -z "$option_dryrun" ]
    then
        printf "%s\n" "$*" >> "$conclusion_file" || \
            bb_msg err "log_conclusion failed ($*)"
    else
        bb_msg info "conclusion(dry run): $*"
    fi
}

# create_check_mailout <type> <fails> <summary-file> <stamp>
function create_check_mailout() {
    local backup_type="$1"
    local create_fails="$2"
    local summmary_file="$3"
    local stamp="$4"

    local -a no_parents=()
    local mail_addr mail_from backup_name subject
    local -a mailx_args=()

    local mail_needed=0 mailcfg_str

    # check if summary mail has to be sent out
    for mailcfg_str in "mail_summary" "${backup_type}_mail_summary"
    do
        if byml_get_value mail_addr grobcfg no_parents "$mailcfg_str"
        then
            mail_needed=1
        fi
    done

    # something failed, check if we have to mail just in that case
    (( create_fails == 0 )) || {
        for mailcfg_str in "mail_on_error" "${backup_type}_mail_on_error"
        do
            if byml_get_value mail_addr grobcfg no_parents "$mailcfg_str"
            then
                mail_needed=1
            fi
        done
    }

    (( mail_needed > 0 )) || {
        log_details "not sending summary email, cause config does not want to"
        return
    }

    [[ -z "$option_dryrun" ]] || {
        bb_msg info "skipping mail to $mail_addr because this is a dry run"
        return
    }

    byml_get_value backup_name grobcfg no_parents "name" || \
        backup_name="UNNAMED"

    if byml_get_value mail_from grobcfg no_parents "mail_from"
    then
        mailx_args+=("-r" "$mail_from")
    fi

    if (( create_fails == 0 ))
    then
        subject="$backup_name: Successfully ran $backup_type $stamp"
    else
        subject="$backup_name: ERRORS while running $backup_type $stamp"
    fi
    mailx_args+=("-s" "$subject")

    mailx_args+=("$mail_addr")

    echo "" > "$TMPDIR/mail-separator"
    echo "Summary follows:" >> "$TMPDIR/mail-separator"
    if cat "$conclusion_file" "$TMPDIR/mail-separator" "$summmary_file" | \
        mailx "${mailx_args[@]}"
    then
        log_details "sent repot mail: -s \"$subject\" \"$mail_addr\""
    else
        log_details "failed to send mail: -s \"$subject\" \"$mail_addr\""
        bb_msg err "failed to send report mail"
    fi
}

# config_check_interval <interval> <config-handle> <host> <drive>
# return 0 if interval is found in config file
# errors: 2, 91 (interval not found)
function config_check_interval() {
    (( $# == 4 )) || {
        bb_ierr "config_match_drive_type: called with $# instead of 4 args"
        return 2
    }

    local _iv="$1"
    local _cfg="$2"
    local _host="$3"
    local _drive="$4"

    # shellcheck disable=SC2034
    local -a intervals
    local -a parents
    parents=("hosts" "$_host" "drives" "$_drive")
    byml_get_list_with_parents intervals "$_cfg" "intervals" parents || \
        return 91 # no intervals at all

    bb_array_contains_value intervals "$_iv" || return 91 # iv not found
}

# cmd_create <type> [<host> [<drive>]]
# shellcheck disable=SC2034
function cmd_create() {
    (( $# > 0 && $# < 4 )) || \
        usage 1 "create accepts 1 to 3 args but got $#"

    require_encryption "create" || \
        bb_quit 1 "create: failed to setup encryption"

    local create_type="$1"
    local custom_host="${2-}"
    local custom_drive="${3-}"

    local retval

    # type can either be custom or something time related
    local archive_stamp now now_s
    if [[ "$create_type" =~ ^custom:[[:alnum:].]+$ ]]
    then
        archive_stamp="${create_type#custom:}"
        create_type="custom" # used later for creating paths
    else
        local time_format

        # choose useful timestamp format
        case "$create_type" in
            daily)    time_format="+%F";;
            hourly)   time_format="+%F.%H";;
            monthly)  time_format="+%Y.%m";;
            weekly)   time_format="+%Y.%W";;
            yearly)   time_format="+%Y";;
            *) bb_quit 1 "create: unsupported create type: $create_type"
        esac

        archive_stamp="$(date "$time_format")" || \
            bb_quit 1 "create: failed to to run: date \"$time_format\""
    fi
    now="$(date "+%F.%T")"
    now_s="$SECONDS"

    local -a host_list
    if [ -z "$custom_host" ]
    then
        byml_get_dict_keys host_list grobcfg "hosts" || \
            bb_quit 1 "create: failed to find hosts in config file ($?)"
    else
        host_list=("$custom_host")
    fi

    local state_path="$grob_repo_path/state/$create_type/$archive_stamp"
    local log_path="$grob_repo_path/log/$create_type/$archive_stamp"
    local summary_file="$log_path/summary-$now.log"
    local details_file="$log_path/details-$now.log"
    local conclusion_file="$log_path/conclusion-$now.log"

    local create_fails=0 create_skips=0 create_oks=0

    mkdir -p "$state_path" || bb_quit 1 "failed to create state path"
    mkdir -p "$log_path" ||  bb_quit 1 "failed to create log path"

    # find out how many hosts and drives we need to backup
    local nr_hosts=0 nr_drives=0 nr_all_drives=0
    local cur_host cur_drive
    local -a drive_list

    log_summary "This create run started at $now."
    log_details "starting the following create run at $now:"

    local str
    for str in create_type custom_host custom_drive archive_stamp
    do
        if [[ -v "${str}" ]]
        then
            log_details "  $str=${!str}"
        else
            log_details "  variable $str is not defined (should not happen)"
        fi
    done

    local -a todo_hosts=()
    local -a todo_drives=()

    # shellcheck disable=SC2034
    local -a intervals
    local -a parents
    local cur_host_drive cur_archive
    log_details "checking how many hosts/drives to backup"
    for cur_host in "${host_list[@]}"
    do
        nr_hosts=$((nr_hosts + 1))

        # shellcheck disable=SC2034
        parents=("hosts" "$cur_host")
        byml_get_dict_keys drive_list grobcfg "drives" parents || {
            bb_msg err "failed to get drive list for host" \
                   "('drives:' not found) $cur_host, skipping it"
            log_details "failed to get drive list for host" \
                "('drives:' not found) $cur_host, skipping it"
            log_summary "Failed to get drive list from config for" \
                "('drives:' not found) host $cur_host, skipping it"
            create_fails=$((create_fails + 1))
            continue
        }

        for cur_drive in "${drive_list[@]}"
        do
            nr_all_drives=$((nr_all_drives + 1))

            [ -z "$custom_drive" ] || {
                # if we are limited to one drive, skip if its any other
                [[ "$custom_drive" == "$cur_drive" ]] || {
                    bb_msg debug "create: skipping host $cur_host drive" \
                        "$cur_drive because it does not match drive filter" \
                        "$custom_drive"
                    continue
                }
            }

            parents=("hosts" "$cur_host")
            byml_get_list_with_parents intervals grobcfg \
                "intervals" parents || {
                retval="$?"
                if (( retval == 54 ))
                then
                    bb_msg warning "create: no intervals found in config" \
                        "for host $cur_host drive $cur_drive, skipping it"
                else
                    bb_msg err "create: error fetching intervals from config" \
                        "for host $cur_host drive $cur_drive ($retval)," \
                        "skipping it"
                fi
                continue
            }

            bb_array_contains_value intervals "$create_type" || {
                retval="$?"
                if (( retval == 91 ))
                then
                    bb_msg debug "create: skipping host $cur_host drive" \
                        "$cur_drive cause interval does not match $create_type"
                else
                    bb_msg err "create: error comparing intervals for host" \
                    "$cur_host drive $cur_drive ($retval), skipping it"
                fi
                continue
            }

            nr_drives=$((nr_drives + 1))
            todo_hosts+=("$cur_host")
            todo_drives+=("$cur_drive")
        done
    done

    log_summary "It planned to process $nr_hosts hosts and $nr_drives drives."
    log_details "going to backup $nr_hosts hosts and $nr_drives drives"

    if [ -z "$option_dryrun" ]
    then
        echo "$now" >> "$log_path/create-runs"
    fi

    local host_counter=0 drive_counter=0

    if (( ${#todo_drives[*]} > 0 ))
    then
        local hdi
        for hdi in "${!todo_drives[@]}"
        do
            log_details "now processing host $cur_host drive $cur_drive" \
                "($hdi/$nr_drives).."

            drive_counter=$((drive_counter + 1))
            cur_host="${todo_hosts[$hdi]}"
            cur_drive="${todo_drives[$hdi]}"
            cur_host_drive="${cur_host}_${cur_drive}"
            cur_archive="${create_type}_${archive_stamp}_${cur_host_drive}"

            if [ -f "${state_path}/${cur_host_drive}.ok" ]
            then
                create_skips=$((create_skips + 1))
                log_details "create for host=$cur_host drive=$cur_drive" \
                    "already completed successfully, skipping it"
                log_summary "Skipped already completed $cur_host $cur_drive"
            else
                if [ -f  "${state_path}/${cur_host_drive}.failed" ]
                then
                    log_details "continuing incomplete create run for" \
                        "host $cur_host ($host_counter/$nr_hosts)" \
                        "drive $cur_drive ($drive_counter/$nr_drives)"
                    log_summary "Detected incomplete create run of" \
                        "$cur_host $cur_drive"
                    rm -f "${state_path}/${cur_host_drive}.failed"
                else
                    log_details "starting create run for" \
                        "host $cur_host ($host_counter/$nr_hosts)" \
                        "drive $cur_drive ($drive_counter/$nr_drives)"
                    log_details "logging into" \
                        "\"$log_path/${now}_$cur_host_drive.(grob|borg)\""
                fi

                # do not do it, if its a dry run
                [[ -z "$option_dryrun" ]] || {
                    # call it but do not log into presistant logs
                    create_archive "$cur_host" "$cur_drive" "${cur_archive}"
                    continue
                }

                # create state file for running create
                touch "${state_path}/${cur_host_drive}.running"

                # run borgbackup for selected host/drive
                if create_archive "$cur_host" "$cur_drive" "${cur_archive}" \
                    >> "$log_path/${now}_$cur_host_drive.borg" \
                    2>> "$log_path/${now}_$cur_host_drive.grob"
                then
                    create_oks=$((create_oks + 1))
                    touch "${state_path}/${cur_host_drive}.ok"
                    log_details "successfully completed create run for" \
                        "host $cur_host ($host_counter/$nr_hosts)" \
                        "drive $cur_drive ($drive_counter/$nr_drives)"
                    log_summary "Successfully completed create run of" \
                        "$cur_host $cur_drive"
                else
                    create_fails=$((create_fails + 1))
                    touch "${state_path}/${cur_host_drive}.failed"
                    log_details "FAILED to complete create run for" \
                        "host $cur_host ($host_counter/$nr_hosts)" \
                        "drive $cur_drive ($drive_counter/$nr_drives)"
                    log_summary "FAILED to complete create run of" \
                        "$cur_host $cur_drive"
                fi
                rm -f "${state_path}/${cur_host_drive}.running"
            fi
        done
    else
        bb_msg warning "no drives found for selected interval: $create_type"
        log_summary "No drives found for selected interval: $create_type"
    fi

    local now2
    now2="$(date "+%F.%T")"

    if (( create_fails == 0 ))
    then
        log_conclusion "Grob create run $create_type $archive_stamp" \
            "finished successfully."
        log_summary "Create finished successfully at $now2."
        log_details "create run completed successfully"
    else
        log_conclusion "Grob create run $create_type $archive_stamp" \
            "finished WITH ERRORS."
        log_conclusion "$create_fails drives FAILED to backup, see logs."
        log_summary "Create finished WITH ERRORS at $now2."
        log_details "create run complete WITH ERRORS"
    fi

    [ -z "$custom_host" ] || {
        if [ -z "$custom_drive" ]
        then
            log_conclusion "This run was limited to host $custom_host."
        else
            log_conclusion "This run was limited to drive $custom_drive" \
                "on host $custom_host."
        fi
    }

    log_conclusion ""
    log_conclusion "$create_oks/$nr_drives drives" \
        "on $nr_hosts hosts have been backed up successfully."
    (( create_skips == 0 )) || {
        log_conclusion "$create_skips drives have been skipped because a" \
            "completed run was found."
    }

    local duration_str duration_s
    duration_s="$((SECONDS - now_s))"
    printf -v duration_str "%d hours %d minutes and %d seconds" \
        $(( duration_s / (60*60) )) \
        $(( duration_s % (60*60) / 60)) \
        $(( duration_s % 60 ))
    log_conclusion "This create run took $duration_str."

    log_details "$create_oks drives have been backed up successfully"
    log_details "$create_fails drives had ERRORS"
    log_details "$create_oks drives have been backed up successfully"
    log_details "$create_skips drives have been skipped because a" \
        "completed run was found."
    log_details "this run took $duration_str."

    log_summary ""
    log_summary "Powered by grob version $GROB_VERSION"
    log_summary "https://git.labs.zkk.tuwien.ac.at/sw/grob"

    create_check_mailout "$create_type" "$create_fails" "$summary_file" \
        "$archive_stamp"
}

function cmd_init() {
    (( $# == 0 )) || usage 1 "init accepts no args but got $#"

    local retval errmsg

    if [ -d "$grob_repo_path/borg_repo" ]
    then
        bb_quit 1 "init failed, grob repo already exists: $grob_repo_path"
    fi

    local enc_mode
    if byml_get_value enc_mode grobcfg no_parents "borg_encryption_mode"
    then
        [[ "$enc_mode" =~ ^(none)|(authenticated|repokey)(-blake2)?$ ]] || \
            bb_quit 1 "init: illegal borg_encryption_mode: $enc_mode"
    else
        retval="$?"

        byml_error_string errmsg "$retval"
        bb_quit 1 "init: error reading borg_encryption_mode from config:" \
            "$errmsg"
    fi

    local passphrase_path=
    if byml_get_value passphrase_path grobcfg no_parents "borg_passphrase_file"
    then
        passphrase_path="${passphrase_path/#\~/$HOME}"
        [ -f "$passphrase_path" ] ||
        mkpasswd -l 42 -s 0 > "$passphrase_path" || {
            bb_quit "init: failed to write autogenerated passphrase to:" \
                "$passphrase_path"
        }
    else
        retval="$?"

        (( retval == 54 )) || {
            byml_error_string errmsg "$retval"
            bb_quit 1 "init: error reading borg_passphrase_file from config:" \
                "$errmsg"
        }
    fi

    require_encryption "init" || \
        bb_quit 1 "init: failed to setup encryption"

    mkdir -p "$grob_repo_path/borg_repo" || \
        bb_quit "init: failed to mkdir: $grob_repo_path/borg_repo"

    borg init --encryption="$enc_mode" "$grob_repo_path/borg_repo" || \
        bb_quit "init: failed to run: borg init"
}

function cmd_list() {
    (( $# == 0 )) || usage 1 "list accepts no args but got $#"

    require_encryption "list" || \
        bb_quit 1 "list: failed to setup encryption"

    borg list "$grob_repo_path/borg_repo"
}

function cmd_delete() {
    (( $# > 0 )) || bb_quit 'deleting nothing, your wish is my command!'

    require_encryption "delete" || \
        bb_quit 1 "delete: failed to setup encryption"

    while (( $# > 0 ))
    do
        borg delete "$grob_repo_path/borg_repo" "$1" || \
            bb_msg err "delete: failed to delete: $1"
        shift
    done
}

function cmd_info() {
    require_encryption "info" || \
        bb_quit 1 "info: failed to setup encryption"

    # no args means summary over whole repository
    if (( $# == 0 ))
    then
        borg info "$grob_repo_path/borg_repo" || \
            bb_msg err "info: failed to get info for whole repo"
        return
    fi

    # if archives are specified, display info about them
    while (( $# > 0 ))
    do
        borg info "$grob_repo_path/borg_repo::$1" || \
            bb_msg err "info: failed to get info for: $1"
        shift
    done
}

function cmd_export_tar() {
    (( $# == 2 )) || usage 1 "export-tar accepts 2 arg but got $#"

    require_encryption "export-tar" || \
        bb_quit 1 "export-tar: failed to setup encryption"

    local _archive="$1"
    local _outfile="$2"

    borg export-tar --tar-filter=auto \
        "$grob_repo_path/borg_repo::$_archive" "$_outfile"
}

function cmd_extract() {
    (( $# == 2 )) || usage 1 "extract accepts 2 args but got $#"

    require_encryption "extract" || \
        bb_quit 1 "extract: failed to setup encryption"

    local _archive="$1"
    local _outdir="$2"
    shift 2

    if [ -d "$_outdir" ]
    then
        bb_quit 1 "extract: output directory already exists"
    fi

    mkdir -p "$_outdir" || \
        bb_quit 1 "extract: failed to mkdir outdir: $_outdir"

    borg extract "$grob_repo_path/borg_repo::$_archive" "$@"
}

function cmd_check() {
    borg check --repository-only "$grob_repo_path/borg_repo" || \
        bb_emerg "check: ERROR checking repository"

    while (( $# > 0 ))
    do
        borg check --archives-only --verify-data \
            "$grob_repo_path/borg_repo::$1" || \
            bb_msg emerg "check: ERROR while checking archive: $1"
        shift
    done
}

function cmd_validate_config() {
    (( $# == 0 )) || usage 1 "validate-config accepts no args but got $#"

    yamllint "$config_file" || \
        bb_quit 1 "validate-config: failed($?) to validate: $config_file"
}

export BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK=yes
export BORG_RELOCATED_REPO_ACCESS_IS_OK=yes

function main() {
    # config vars
    local option_lockwait="-1"
    local option_dryrun=
    local option_verbose=0

    # read global args
    local op
    while getopts ":hnvVwW:" op
    do
        case "$op" in
            h) usage 0;;
            n) option_dryrun=1
                option_verbose=$((option_verbose + 1));;
            v) option_verbose=$((option_verbose + 1));;
            V) echo "grob version $GROB_VERSION"; bb_quit;;
            w) option_lockwait="0";;
            W) option_lockwait="$OPTARG"
                [[ "$option_lockwait" =~ ^[[:digit:]]+$ ]] || \
                    usage 1 "-W only accepts integer"
                ;;
            *) usage 1 "unknown global option: $op"
        esac
    done
    shift $((OPTIND - 1))

    bb_msg_del_target default_info_err
    case "$option_verbose" in
        0) bb_msg_set_target notrace_stderr stderr err prog notrace;;
        1) bb_msg_set_target notrace_stderr stderr info:err prog notrace;;
        *) bb_msg_set_target notrace_stderr stderr debug:err prog notrace
    esac
    export GROB_CALLBACK_LOGLEVEL="$option_verbose"

    (( $# > 1 )) || usage 1 "expected config-file and command"

    declare -g TMPDIR
    TMPDIR=$(mktemp -d /tmp/grob.XXXXXXXXXXX) || \
        bb_fatal "failed to: mktemp -d"
    bb_register_exit_hook cleanup
    chmod 700 "$TMPDIR"
    bb_msg debug "TMPDIR=$TMPDIR"

    # used inside borg_pull
    declare -g SSH_PID=
    declare -g SOCAT_PID=
    bb_register_exit_hook kill_children # won't hurt if no children are running

    local config_file="$1"
    local grob_cmd="$2"
    shift 2

    byml_handle_open grobcfg "$config_file" || {
        local retval="$?"
        local errstr
        byml_error_string errstr "$retval"
        bb_quit 1 "could not read config file '$config_file': $errstr"
    }

    # shellcheck disable=SC2034
    local -a no_parents=()

    local grob_repo_path
    byml_get_value grob_repo_path grobcfg no_parents "grob_repo_path" || \
        bb_quit 1 "mandatory entry in config file not found: grob_repo_path"

    # backup config to repo to ease recovery in case of disaster
    if [ -d "$grob_repo_path" ]
    then
        cmp --quiet "$config_file" \
            "$grob_repo_path/grob-config-backup.yml" || \
            cat "$config_file" > "$grob_repo_path/grob-config-backup.yml"

        if [ "$grob_cmd" == "version" ]
        then
            bb_quit 0 "version $GROB_VERSION"
        fi

        bb_lock_aquire "grob_repo" "$grob_repo_path/.GROB_LOCK" write \
            "$option_lockwait" || \
            bb_quit 1 "failed to aquire repo lock, aborting"

        case "$grob_cmd" in
            check) cmd_check "$@";;
            create) cmd_create "$@";;
            delete) cmd_delete "$@";;
            export-tar) cmd_export_tar "$@";;
            info) cmd_info "$@";;
            init) cmd_init "$@";;
            list) cmd_list "$@";;
            validate-config) cmd_validate_config "$@";;
            *)
                bb_lock_release "grob_repo"
                usage 1 "no such command: $grob_cmd"
        esac

        bb_lock_release "grob_repo"
    else
        # if no grob_repo_path exists, we can do only a few things
        case "$grob_cmd" in
            init) cmd_init "$@";;
            validate-config) cmd_validate_config "$@";;
            version) bb_quit 0 "version $GROB_VERSION";;
            *) usage 1 "no such command or impossible unless initialized:" \
                "$grob_cmd"
        esac
    fi
}

main "$@"
bb_quit
