#!/bin/bash

# These can be overridden when running this script
HOSTNAME=${HOSTNAME:-$(hostname)}
BACKUP_HOST=${BACKUP_HOST:-backup.jim.sh}
BACKUP_USER=${BACKUP_USER:-jim-backups}
BACKUP_REPO=${BACKUP_REPO:-borg/${HOSTNAME}}

# Main dir is where this repo was checked out
BORG_DIR="$(realpath "$(dirname "$0")")"
cd "${BORG_DIR}"

# This is named with uppercase so that it doesn't tab-complete for
# "./b<tab>", which should give us "./borg.sh"
BORG_BIN="${BORG_DIR}/Borg.bin"

# Use stable host ID in case MAC address changes
HOSTID="${HOSTNAME}@$(python -c 'import uuid;print(uuid.getnode())')"

function error_handler() {
    echo "Error at $1 line $2:"
    echo -n '>>> ' ; tail -n +"$2" < "$1" | head -1
    echo "... exited with code $3"
    exit "$3"
}
trap 'error_handler ${BASH_SOURCE} ${LINENO} $?' ERR
set -o errexit
set -o errtrace

update_paths()
{
    sed -i \
        -e "s!\${HOSTNAME}!${HOSTNAME}!g" \
        -e "s!\${BORG_DIR}!${BORG_DIR}!g" \
        -e "s!\${BACKUP_USER}!${BACKUP_USER}!g" \
        -e "s!\${BACKUP_HOST}!${BACKUP_HOST}!g" \
        -e "s!\${BACKUP_REPO}!${BACKUP_REPO}!g" \
        README.md

    sed -i\
        -e "1c#!${BORG_DIR}/.venv/bin/python" \
        backup.py
}

if [ "$1" == "--update-paths" ] ; then
    if [ -e "vars.sh" ]; then
        echo "Updating paths"
        update_paths
        exit 0
    else
        echo "Can't update paths, not set up yet"
        exit 1
    fi
fi

if [ -e "vars.sh" ]; then
    echo "Error: BORG_DIR $BORG_DIR already looks set up; giving up."
    echo "Use \"git clean\" to return it to original state if desired"
    exit 1
fi

# Make a temp dir to work in
TMP=$(mktemp -d)

# Install some cleanup handlers
cleanup()
{
    set +o errexit
    set +o errtrace
    trap - ERR
    ssh -o ControlPath="$TMP"/ssh-control -O exit x >/dev/null 2>&1
    rm -rf -- "$TMP"
}
cleanup_int()
{
    echo
    cleanup
    exit 1
}
trap cleanup 0
trap cleanup_int 1 2 15

msg()
{
    color="$1"
    shift
    echo -ne "\033[1;${color}m===\033[0;${color}m" "$@"
    echo -e "\033[0m"
}
log(){ msg 33 "$@" ; }
notice() { msg 32 "$@" ; }
warn() { msg 31 "$@" ; }
error() { msg 31 "Error:" "$@" ; exit 1 ; }

# Create pip environment
setup_venv()
{
    if ! which pipenv >/dev/null 2>&1 ; then
        echo "pipenv not found, try: sudo apt install pipenv"
        exit 1
    fi
    mkdir .venv
    pipenv install
}

# Create shell script with environment variables
create_borg_vars()
{
    VARS=${BORG_DIR}/vars.sh

    # These variables are used elsewhere in this script
    BORG_REPO="ssh://${BACKUP_USER}@${BACKUP_HOST}/./${BACKUP_REPO}"
    BORG=${BORG_DIR}/borg.sh
    SSH=$BORG_DIR/ssh

    cat >"$VARS" <<EOF
export BACKUP_USER=${BACKUP_USER}
export BACKUP_HOST=${BACKUP_HOST}
export BACKUP_REPO=${BACKUP_REPO}
export HOSTNAME=${HOSTNAME}
export BORG_REPO=${BORG_REPO}
export BORG_HOST_ID=${HOSTID}
export BORG_PASSCOMMAND="cat ${BORG_DIR}/passphrase"
export BORG_DIR=${BORG_DIR}
export SSH=${SSH}
export BORG=${BORG}
export BORG_BIN=${BORG_BIN}
EOF
    if ! "$BORG" -h >/dev/null ; then
        error "Can't run the borg wrapper; does borg work?"
    fi

}

print_random_key()
{
    dd if=/dev/urandom | tr -dc 'a-zA-Z0-9' | head -c 16
}

generate_keys()
{
    PASS_REPOKEY=$(print_random_key)
    echo "$PASS_REPOKEY" > passphrase
    chmod 600 passphrase
}

# Run a command on the remote host over an existing SSH tunnel
run_ssh_command()
{
    ssh -o ControlPath="$TMP"/ssh-control use-existing-control-tunnel "$@"
}

# Configure SSH key-based login
configure_ssh()
{
    mkdir "$SSH"

    # Create keys
    log "Creating SSH keys"
    ssh-keygen -N "" -t ecdsa \
               -C "backup-appendonly@$HOSTID" -f "$SSH/id_ecdsa_appendonly"
    ssh-keygen -N "" -t ecdsa \
               -C "backup-notify@$HOSTID" -f "$SSH/id_ecdsa_notify"

    # Create config snippets
    log "Creating SSH config and wrapper script"
    cat >> "$SSH/config" <<EOF
User $BACKUP_USER
ControlPath none
ServerAliveInterval 120
Compression no
UserKnownHostsFile $SSH/known_hosts
ForwardX11 no
ForwardAgent no
BatchMode yes
IdentitiesOnly yes
EOF

    # Connect to backup host, using persistent control socket
    log "Connecting to server"
    log "Please enter password; look in Bitwarden for: ssh ${BACKUP_HOST} / ${BACKUP_USER}"
    ssh -F "$SSH/config" -o BatchMode=no -o PubkeyAuthentication=no \
        -o ControlMaster=yes -o ControlPath="$TMP/ssh-control" \
        -o StrictHostKeyChecking=accept-new \
        -f "${BACKUP_USER}@${BACKUP_HOST}" sleep 600
    if ! run_ssh_command true >/dev/null 2>&1 </dev/null ; then
        error "SSH failed"
    fi
    log "Connected to ${BACKUP_USER}@${BACKUP_HOST}"

    # Since we now have an SSH connection, check that the repo doesn't exist
    if run_ssh_command "test -e $BACKUP_REPO" ; then
        error "$BACKUP_REPO already exists on the server, bailing out"
    fi

    # Copy SSH keys to the server's authorized_keys file, removing any
    # existing keys with this HOSTID.
    log "Setting up SSH keys on remote host"
    REMOTE_BORG="borg/borg"
    cmd="$REMOTE_BORG serve --restrict-to-repository ~/$BACKUP_REPO"

    keys=".ssh/authorized_keys"
    backup="${keys}.old-$(date +%Y%m%d-%H%M%S)"
    run_ssh_command "mkdir -p .ssh; chmod 700 .ssh; touch $keys"
    run_ssh_command "mv $keys $backup; sed '/@$HOSTID\$/d' < $backup > $keys"
    run_ssh_command "if cmp -s $backup $keys; then rm $backup ; fi"
    run_ssh_command "cat >> .ssh/authorized_keys" <<EOF
command="$cmd --append-only",restrict $(cat "$SSH/id_ecdsa_appendonly.pub")
command="borg/notify.sh",restrict $(cat "$SSH/id_ecdsa_notify.pub")
EOF

    # Test that everything worked
    log "Testing SSH login with new key"
    if ! ssh -F "$SSH/config" -i "$SSH/id_ecdsa_appendonly" -T \
         "${BACKUP_USER}@${BACKUP_HOST}" "$REMOTE_BORG" --version </dev/null ; then
        error "Logging in with a key failed -- is server set up correctly?"
    fi
    log "Remote connection OK!"
}

# Create the repository on the server
create_repo()
{
    log "Creating repo $BACKUP_REPO"
    # Create repo
    $BORG init --make-parent-dirs --encryption repokey
}

# Export keys as HTML page
export_keys()
{
    log "Exporting keys"
    $BORG key export --paper '' key.txt
    chmod 600 key.txt
    cat >>key.txt <<EOF

Repository: ${BORG_REPO}
Passphrase: ${PASS_REPOKEY}
EOF
}

configure_systemd()
{
    TIMER=borg-backup.timer
    SERVICE=borg-backup.service
    TIMER_UNIT=${BORG_DIR}/${TIMER}
    SERVICE_UNIT=${BORG_DIR}/${SERVICE}

    log "Creating systemd files"

    # Choose a time between 1am and 6am based on this hostname
    HASH=$(echo hash of "$HOSTNAME" | sha1sum)
    HOUR=$((0x${HASH:0:8} % 5 + 1))
    MINUTE=$((0x${HASH:8:8} % 6 * 10))
    TIME=$(printf %02d:%02d:00 $HOUR $MINUTE)

    log "Backup time is $TIME"

    cat > "$TIMER_UNIT" <<EOF
[Unit]
Description=Borg backup to ${BACKUP_HOST}

[Timer]
OnCalendar=*-*-* $TIME
Persistent=true

[Install]
WantedBy=timers.target
EOF

    cat >> "$SERVICE_UNIT" <<EOF
[Unit]
Description=Borg backup to ${BACKUP_HOST}

[Service]
Type=simple
ExecStart=${BORG_DIR}/backup.py
Nice=10
IOSchedulingClass=best-effort
IOSchedulingPriority=6
EOF

    log "Setting up systemd"
    if (
        ln -sfv "${TIMER_UNIT}" /etc/systemd/system &&
        ln -sfv "${SERVICE_UNIT}" /etc/systemd/system &&
        systemctl --no-ask-password daemon-reload &&
        systemctl --no-ask-password enable ${TIMER} &&
        systemctl --no-ask-password start ${TIMER}
    ); then
        log "Backup timer installed:"
        systemctl list-timers --no-pager ${TIMER}
    else
        warn ""
        warn "Systemd setup failed"
        warn "Do something like this to configure automatic backups:"
        echo "     sudo ln -sfv \"${TIMER_UNIT}\" /etc/systemd/system &&"
        echo "     sudo ln -sfv \"${SERVICE_UNIT}\" /etc/systemd/system &&"
        echo "     sudo systemctl daemon-reload &&"
        echo "     sudo systemctl enable ${TIMER} &&"
        echo "     sudo systemctl start ${TIMER}"
        warn ""
    fi
}

git_setup()
{
    if ! git checkout -b "setup-${HOSTNAME}" ; then
        warn "Git setup failed; ignoring"
        return
    fi

    log "Committing local changes to git"
    git add README.md borg-backup.service borg-backup.timer vars.sh
    git commit -a -m "autocommit after initial setup on ${HOSTNAME}"
}

log "Configuration:"
log "  Backup server host: ${BACKUP_HOST}"
log "  Backup server user: ${BACKUP_USER}"
log "     Repository path: ${BACKUP_REPO}"

setup_venv
create_borg_vars
generate_keys
configure_ssh
create_repo
export_keys
configure_systemd
update_paths
git_setup

echo
notice "Add this password to Bitwarden:"
notice ""
notice "      Name: borg ${HOSTNAME}"
notice "  Username: repo key"
notice "  Password: $PASS_REPOKEY"
notice ""
notice "Test the backup file list with"
notice "    sudo ${BORG_DIR}/backup.py --dry-run"
notice "and make any necessary adjustments to:"
notice "    ${BORG_DIR}/config.yaml"

echo

echo "All done"
