#! /bin/bash
# vim: shiftwidth=4 tabstop=4 expandtab

function set_default_backup_author_name() {
    [[ "${GIT_AUTHOR_NAME:-null}" == "null" ]] && \
        declare -g GIT_AUTHOR_NAME="$1"
}

function add_backup_pending_change() {
    local backup_dir=$1 action="${2:-null}" path="${3:-null}"
    case "$action" in
        add)
            if ! run_external git -C "$backup_dir" add "$path"; then
                log ERROR "Failed to add $path to git"
                return 1
            fi
            ;;
        rm)
            if ! run_external git -C "$backup_dir" rm -q -f "$path"; then
                log ERROR "Failed to remove $path in git"
                return 1
            fi
            ;;
        *)
            log ERROR "add_backup_pending_change(): Unknown action '$action'"
            return 1
    esac
    return 0
}

function _backup_dir() {
    local output_var=$1 backup_dir

    backup_dir="${BACKUP_DIRECTORY_PATH}/config"

    # Ensure backup directory exists
    if [[ -d "$backup_dir" ]]; then
        log DEBUG "Backup directory already exists ($backup_dir)"
    elif mkdir "$backup_dir"; then
        log INFO "Backup directory created ($backup_dir)"
    else
        log FATAL "Failed to create backup directory ($backup_dir)"
    fi

    # Ensure git in initialized in backup directory
    if [[ -d "$backup_dir/.git" ]]; then
        log DEBUG "Git already initialized in backup directory"
        [[ -n "$( git -C "$backup_dir" status --short )" ]] && \
            log FATAL "Changes detected in backup directory:\n\n$( git -C "$backup_dir" status )"
    elif run_external git init -q -b backups "$backup_dir"; then
        log INFO "Git initialized in backup directory"
    else
        log FATAL "Failed to initialized Git in backup directory"
    fi

    # Ensure we don't track changes on telltale file ("last_backup" file)
    touch "$backup_dir/.gitignore"
    if [[ $(grep -Ec "last_backup" "$backup_dir/.gitignore") -lt 1 ]]; then
        echo "last_backup" > "$backup_dir"/.gitignore
        git -C "$backup_dir" add .gitignore
        # Remove telltale file if already present
        [[ "${GIT_AUTHOR_NAME:-null}" == "null" ]] && \
            declare -g GIT_AUTHOR_NAME && GIT_AUTHOR_NAME="$(_log_command_name)"
        [[ "${GIT_AUTHOR_EMAIL:-null}" == "null" ]] && \
            declare -g GIT_AUTHOR_EMAIL && GIT_AUTHOR_EMAIL="$(id -u -n)@$(hostname -s)"
        rm -f "$backup_dir/last_backup"
        git -C "$backup_dir" rm -q last_backup 2> /dev/null
        # shellcheck disable=SC2097,SC2098
        GIT_AUTHOR_NAME=$GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL=$GIT_AUTHOR_EMAIL \
        GIT_COMMITTER_NAME=$GIT_AUTHOR_NAME GIT_COMMITTER_EMAIL=$GIT_AUTHOR_EMAIL \
            git -C "$backup_dir" commit -q -m "Ignore telltale file (last_backup)" \
            --author="$GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL>"
    fi

    declare -g "$output_var=$backup_dir"
}

function _hyp_backup_dir() {
    local hyp=$1 output_var=$2 hyp_backup_dir

    if [[ "${3:-null}" == "null" ]]; then
        _backup_dir BACKUP_DIR
        # shellcheck disable=SC2153
        hyp_backup_dir="$BACKUP_DIR/$hyp"
    else
        hyp_backup_dir="$3/$hyp"
    fi

    # Ensure hypervisor backup directory exist
    if [[ -d "$hyp_backup_dir" ]]; then
        log DEBUG "Hypervisor $hyp backup directory already exist ($hyp_backup_dir)"
    else
        log INFO "Create $hyp backup directory ($hyp_backup_dir)"
        if ! run_external mkdir -p "$hyp_backup_dir"; then
            log ERROR "Failed to create $hyp backup directory ($hyp_backup_dir)"
            return 1
        fi
    fi

    declare -g "$output_var=$hyp_backup_dir"
}

function backup_telltale_path() {
    local output_var=$1 telltale_path

    if [[ "${2:-null}" == "null" ]]; then
        _backup_dir BACKUP_DIR
        # shellcheck disable=SC2153
        telltale_path="$BACKUP_DIR/last_backup"
    else
        telltale_path="$2/last_backup"
    fi

    declare -g "$output_var=$telltale_path"
}

function check_is_backup_master() {
    is_master && return

    if [[ $FORCE -eq 1 ]]; then
        log INFO "Force mode: backup VMs configuration even if we are not on the master node"
        return
    fi

    if [[ ! -t 1 ]]; then
        log DEBUG "Non-interactive mode and not on master node, do nothing"
        exit 0
    fi

    # On interactive mode, run the backup command on the master node and synchronize it locally
    log INFO "No on the master node, run backup on it and sync it locally"
    local -a args=( --log-level "$LOG_LEVEL" )
    [[ $CONSOLE_LOG -eq 1 ]] && args+=( --console )

    ssh "$VIP" -- "$(_vm_command_path)" "${args[@]}" "$@" || \
        exit 1

    "$(_vm_command_path)" cluster sync  "${args[@]}" "${BACKUP_DIRECTORY_PATH}/" || \
        exit 1
    exit 0
}

function backup_hyp() {
    local idx=1 hyp backup_dir hyp_backup_dir status="all" active_vms_hyp vm
    while [[ $idx -le $# ]]; do
        opt=${!idx}
        case $opt in
            --backup-dir)
                ((idx++))
                backup_dir="${!idx}"
                ;;
            --hyp-backup-dir)
                ((idx++))
                hyp_backup_dir="${!idx}"
                ;;
            --status)
                ((idx++))
                status="${!idx}"
                ;;
            --active-vms-hyp)
                ((idx++))
                active_vms_hyp="${!idx}"
                ;;
            *)
                if [[ "${hyp:-null}" == "null" ]]; then
                    hyp=$opt
                else
                    log ERROR "backup_hyp(): invalid parameter '$opt'"
                    return 1
                fi
        esac
        ((idx++))
    done

    if [[ "${hyp:-null}" == "null" ]]; then
        log ERROR "backup_hyp(): Hypervisor not specified"
        return 1
    fi

    if [[ "${backup_dir:-null}" == "null" ]]; then
        _backup_dir BACKUP_DIR
        backup_dir=$BACKUP_DIR
    fi

    if [[ "${hyp_backup_dir:-null}" == "null" ]]; then
        _hyp_backup_dir "$hyp" HYP_BACKUP_DIR "$BACKUP_DIR" || return 1
        # shellcheck disable=SC2153
        hyp_backup_dir=$HYP_BACKUP_DIR
    fi

    local -a status_list all=0
    if [[ "$status" == "all" ]]; then
        status_list=( active inactive )
        all=1
    else
        status_list=( "$status" )
    fi

    set_default_backup_author_name "backup_hyp($hyp)"

    # Iterate on status list
    local -a hyp_vms=() vms
    for status in "${status_list[@]}"; do
        log INFO "Inspecting $status VM on $hyp"

        # List VMs
        if mapfile -t vms < <( list_vms "$hyp" "$status" ); then
            log DEBUG "${#vms[@]} $status VMs found on $hyp"
        else
            log ERROR "Failed to list $status VMs on $hyp"
            error=1
            continue
        fi

        # Iterate on VMs
        for vm in "${vms[@]}"; do
            hyp_vms+=( "$vm" )
            if [[ "$status" == "active" ]] && [[ "${active_vms_hyp:-null}" != "null" ]]; then
                declare -g "${active_vms_hyp}[$vm]=$hyp"
            fi
            backup_vm \
                --hyp "$hyp" \
                --backup-dir "$backup_dir" \
                --hyp-backup-dir "$hyp_backup_dir" \
                --status "$status" \
                "$vm" || error=1
        done
    done

    if [[ $all -eq 1 ]]; then
        log DEBUG "Cleanup old VM configurations on $hyp"
        local file
        local -a files
        if ! mapfile -t files < <( find "$hyp_backup_dir" -maxdepth 1 -type f -name '*.xml' ); then
            log "ERROR" "Failed to list VM configuration files of $hyp"
            return 1
        fi
        for file in "${files[@]}"; do
            vm="$( basename "$file" )"
            vm="${vm/%.xml/}"
            in_array "$vm" "${hyp_vms[@]}" && continue
            log "INFO" "Remove VM $vm old configuration in $hyp backups ($file)"
            if add_backup_pending_change \
                "$backup_dir" \
                rm "$file"; then
                log INFO "Removal of VM $vm on $hyp commit"
            else
                log ERROR "Failed to commit removal of VM $vm on $hyp ($file)"
                error=1
            fi
        done
    fi

    return "$error"
}

function backup_vm() {
    local idx=1 vm hyp backup_dir hyp_backup_dir status vm_backup_path error=0
    while [[ $idx -le $# ]]; do
        opt=${!idx}
        case $opt in
            --hyp)
                ((idx++))
                hyp="${!idx}"
                ;;
            --backup-dir)
                ((idx++))
                backup_dir="${!idx}"
                ;;
            --hyp-backup-dir)
                ((idx++))
                hyp_backup_dir="${!idx}"
                ;;
            --status)
                ((idx++))
                status="${!idx}"
                ;;
            *)
                if [[ "${vm:-null}" == "null" ]]; then
                    vm=$opt
                else
                    log ERROR "backup_vm(): invalid parameter '$opt'"
                    return 1
                fi
        esac
        ((idx++))
    done

    if [[ "${vm:-null}" == "null" ]]; then
        log ERROR "backup_vm(): VM not specified"
        return 1
    fi

    if [[ "${hyp:-null}" == "null" ]]; then
        hyp=$( locate "$vm" )
        if ! check_hyp "$hyp"; then
            log ERROR "Fail to locate VM $vm"
            return 1
        fi
    fi

    if [[ "$status" == "null" ]]; then
        if is_running "$vm" "$hyp"; then
            status=active
        else
            status=inactive
        fi
    fi

    if [[ "${backup_dir:-null}" == "null" ]]; then
        _backup_dir BACKUP_DIR
        backup_dir=$BACKUP_DIR
    fi

    if [[ "${hyp_backup_dir:-null}" == "null" ]]; then
        _hyp_backup_dir "$hyp" HYP_BACKUP_DIR "$BACKUP_DIR" || return 1
        hyp_backup_dir=$HYP_BACKUP_DIR
    fi

    set_default_backup_author_name "backup_vm($vm)"

    log DEBUG "Backup VM $vm configuration on $hyp"
    vm_backup_path="$hyp_backup_dir/$vm.xml"

    if ! vm_config=$( run_external ssh "$hyp" -- cat "/etc/libvirt/qemu/${vm}.xml" ); then
        log ERROR "Failed to dump VM $vm configuration"
        return 1
    fi

    local update=1 vm_current_config_hash vm_last_config_hash
    if [[ -e "$vm_backup_path" ]]; then
        vm_current_config_hash="$( md5sum -t <<< "$vm_config" | awk '{print $1}' )"
        vm_last_config_hash="$( md5sum "$vm_backup_path" | awk '{print $1}' )"
        log DEBUG "VM $vm config hash: current=$vm_current_config_hash / last backuped=$vm_last_config_hash"
        if [[ "$vm_current_config_hash" == "$vm_last_config_hash" ]]; then
            log DEBUG "No change detected in VM $vm configuration on $hyp"
            update=0
        else
            log INFO "Change detected in VM $vm configuration on $hyp, save it"
        fi
    else
        log INFO "New VM $vm configuration detected on $hyp, save it"
    fi

    if [[ $update -eq 1 ]]; then
        if cat <<< "$vm_config" > "$vm_backup_path" && chmod 0400 "$vm_backup_path"; then
            log INFO "VM $vm configuration backuped ($vm_backup_path), let's record this change"
            if ! add_backup_pending_change \
                "$backup_dir" \
                add "$vm_backup_path"; then
                log ERROR "Failed to record change on VM $vm configuration on $hyp ($vm_backup_path)"
                return 1
            fi
            log DEBUG "Change on VM $vm configuration on $hyp recorded"
        else
            log ERROR "Failed to store VM $vm configuration backup in $vm_backup_path"
            return 1
        fi
    fi

    if [[ "$status" == "active" ]]; then
        vm_running_symlink_path="$backup_dir/${vm}.xml"
        log DEBUG "VM $vm is running on $hyp, update active symlink ($vm_running_symlink_path)"
        if [[ -L "$vm_running_symlink_path" ]] \
            && [[ "$( readlink -f "$vm_running_symlink_path" )" == "$vm_backup_path" ]]; then
            log DEBUG "VM $vm active symlink already up to date"
        elif ln -sf "$hyp/${vm}.xml" "$vm_running_symlink_path"; then
            log INFO "VM $vm running symlink updated, let's record this change"
            if ! add_backup_pending_change \
                "$backup_dir" \
                add "$vm_running_symlink_path"; then
                log ERROR "Failed to record change on VM $vm active symlink on $hyp ($vm_running_symlink_path)"
                return 1
            fi
            log INFO "Change on VM $vm configuration on $hyp recorded"
        else
            log ERROR "Failed to update VM $vm running symlink"
            return 1
        fi
    fi

    return 0
}
