#!/bin/bash
#############################################################
#### Docker Volume Backup Script                         ####
#### Backup and restore Docker named volumes using       ####
#### tar archives with optional compression              ####
####                                                     ####
#### Author: Phil Connor                                 ####
#### Contact: contact@mylinux.work                       ####
#### License: MIT                                        ####
#### Version: 1.0                                        ####
####                                                     ####
#### Usage: ./docker-volume-backup.sh [OPTIONS]          ####
#############################################################

set -euo pipefail

SCRIPT_NAME=$(basename "$0")
readonly SCRIPT_NAME
readonly DEFAULT_BACKUP_DIR="/opt/docker-backups"
readonly DEFAULT_RETAIN=7
readonly ALPINE_IMAGE="alpine:latest"

BACKUP_DIR="$DEFAULT_BACKUP_DIR"
RETAIN="$DEFAULT_RETAIN"
MODE=""
TARGET_VOLUME=""
RESTORE_ARCHIVE=""

# Colors
readonly RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m'

log_info()  { echo -e "${GREEN}[INFO]${NC}  $(date '+%Y-%m-%d %H:%M:%S') $*"; }
log_warn()  { echo -e "${YELLOW}[WARN]${NC}  $(date '+%Y-%m-%d %H:%M:%S') $*"; }
log_error() { echo -e "${RED}[ERROR]${NC} $(date '+%Y-%m-%d %H:%M:%S') $*" >&2; }
log_step()  { echo -e "${BLUE}[STEP]${NC}  $(date '+%Y-%m-%d %H:%M:%S') $*"; }

show_help() {
    cat << EOF
Usage: $SCRIPT_NAME [OPTIONS]

Backup and restore Docker named volumes using tar archives with compression.

OPTIONS:
    --backup [VOLUME]     Backup all named volumes, or a specific volume if given
    --restore ARCHIVE     Restore a volume from the specified tar.gz archive
    --list                List available backups
    --backup-dir PATH     Backup directory (default: $DEFAULT_BACKUP_DIR)
    --retain N            Number of backups to keep per volume (default: $DEFAULT_RETAIN)
    --help, -h            Show this help message

EXAMPLES:
    $SCRIPT_NAME --backup
    $SCRIPT_NAME --backup my_volume
    $SCRIPT_NAME --restore $DEFAULT_BACKUP_DIR/my_volume_20260309_143022.tar.gz
    $SCRIPT_NAME --list
    $SCRIPT_NAME --backup --backup-dir /mnt/backups --retain 14
EOF
    exit 0
}

parse_args() {
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --backup)
                MODE="backup"; shift
                [[ $# -gt 0 && ! "$1" =~ ^-- ]] && { TARGET_VOLUME="$1"; shift; }
                ;;
            --restore)
                MODE="restore"
                [[ $# -lt 2 ]] && { log_error "--restore requires an archive path"; exit 1; }
                RESTORE_ARCHIVE="$2"; shift 2
                ;;
            --list)       MODE="list"; shift ;;
            --backup-dir)
                [[ $# -lt 2 ]] && { log_error "--backup-dir requires a path"; exit 1; }
                BACKUP_DIR="$2"; shift 2
                ;;
            --retain)
                [[ $# -lt 2 ]] && { log_error "--retain requires a number"; exit 1; }
                RETAIN="$2"; shift 2
                ;;
            --help|-h) show_help ;;
            *) log_error "Unknown option: $1"; show_help ;;
        esac
    done
    if [[ -z "$MODE" ]]; then
        log_error "No action specified. Use --backup, --restore, or --list."
        show_help
    fi
}

check_dependencies() {
    if ! command -v docker &>/dev/null; then
        log_error "docker is required but not installed"; exit 1
    fi
    if ! docker info &>/dev/null; then
        log_error "Cannot connect to Docker daemon. Is it running?"; exit 1
    fi
}

backup_volume() {
    local volume_name="$1"
    local timestamp archive_name final_path tmp_file size
    timestamp=$(date +%Y%m%d_%H%M%S)
    archive_name="${volume_name}_${timestamp}.tar.gz"
    final_path="${BACKUP_DIR}/${archive_name}"

    log_step "Backing up volume: ${volume_name}"

    if ! docker volume inspect "$volume_name" &>/dev/null; then
        log_error "Volume '$volume_name' does not exist"; return 1
    fi

    mkdir -p "$BACKUP_DIR"
    tmp_file=$(mktemp "${BACKUP_DIR}/.backup_XXXXXX.tar.gz")

    if docker run --rm \
        -v "${volume_name}:/source:ro" \
        -v "${BACKUP_DIR}:/backup" \
        "$ALPINE_IMAGE" \
        tar czf "/backup/$(basename "$tmp_file")" -C /source . 2>/dev/null; then
        mv "$tmp_file" "$final_path"
        size=$(du -h "$final_path" | cut -f1)
        log_info "Created backup: ${final_path} (${size})"
    else
        rm -f "$tmp_file"
        log_error "Failed to backup volume: ${volume_name}"; return 1
    fi
}

do_backup() {
    log_step "Starting Docker volume backup"
    log_info "Backup directory: ${BACKUP_DIR}"

    local volumes=()
    if [[ -n "$TARGET_VOLUME" ]]; then
        volumes=("$TARGET_VOLUME")
    else
        while IFS= read -r vol; do
            [[ -n "$vol" ]] && volumes+=("$vol")
        done < <(docker volume ls --format '{{.Name}}' | sort)
    fi

    if [[ ${#volumes[@]} -eq 0 ]]; then
        log_warn "No Docker named volumes found"; return 0
    fi
    log_info "Found ${#volumes[@]} volume(s) to backup"

    local success=0 failed=0
    for vol in "${volumes[@]}"; do
        if backup_volume "$vol"; then
            success=$((success + 1))
        else
            failed=$((failed + 1))
        fi
    done

    apply_retention
    log_step "Backup complete: ${success} succeeded, ${failed} failed"
    [[ $failed -gt 0 ]] && return 1
    return 0
}

do_restore() {
    if [[ ! -f "$RESTORE_ARCHIVE" ]]; then
        log_error "Archive not found: ${RESTORE_ARCHIVE}"; exit 1
    fi

    local basename_archive volume_name
    basename_archive=$(basename "$RESTORE_ARCHIVE")
    volume_name=$(echo "$basename_archive" | sed 's/_[0-9]\{8\}_[0-9]\{6\}\.tar\.gz$//')

    if [[ -z "$volume_name" || "$volume_name" == "$basename_archive" ]]; then
        log_error "Cannot determine volume name from archive: ${basename_archive}"
        log_error "Expected format: volumename_YYYYMMDD_HHMMSS.tar.gz"; exit 1
    fi

    log_step "Restoring volume: ${volume_name} from ${RESTORE_ARCHIVE}"

    if ! docker volume inspect "$volume_name" &>/dev/null; then
        log_info "Creating volume: ${volume_name}"
        docker volume create "$volume_name" >/dev/null
    else
        log_warn "Volume '${volume_name}' already exists — contents will be overwritten"
    fi

    local archive_abs archive_dir archive_file
    archive_abs=$(realpath "$RESTORE_ARCHIVE")
    archive_dir=$(dirname "$archive_abs")
    archive_file=$(basename "$archive_abs")

    if docker run --rm \
        -v "${volume_name}:/target" \
        -v "${archive_dir}:/backup:ro" \
        "$ALPINE_IMAGE" \
        sh -c "rm -rf /target/* /target/..?* /target/.[!.]* 2>/dev/null; tar xzf /backup/${archive_file} -C /target" 2>/dev/null; then
        log_info "Volume '${volume_name}' restored successfully"
    else
        log_error "Failed to restore volume: ${volume_name}"; exit 1
    fi
}

do_list() {
    if [[ ! -d "$BACKUP_DIR" ]]; then
        log_info "No backups found (directory does not exist: ${BACKUP_DIR})"; return 0
    fi

    local count=0
    log_step "Available backups in ${BACKUP_DIR}:"
    printf "\n  %-40s  %-10s  %s\n" "ARCHIVE" "SIZE" "VOLUME"
    printf "  %-40s  %-10s  %s\n" "-------" "----" "------"

    for archive in "$BACKUP_DIR"/*.tar.gz; do
        [[ -f "$archive" ]] || continue
        count=$((count + 1))
        local name size vol_name
        name=$(basename "$archive")
        size=$(du -h "$archive" | cut -f1)
        vol_name=$(echo "$name" | sed 's/_[0-9]\{8\}_[0-9]\{6\}\.tar\.gz$//')
        printf "  %-40s  %-10s  %s\n" "$name" "$size" "$vol_name"
    done

    echo ""
    if [[ $count -eq 0 ]]; then
        log_info "No backup archives found in ${BACKUP_DIR}"
    else
        log_info "${count} backup(s) found"
    fi
}

apply_retention() {
    log_step "Applying retention policy (keep last ${RETAIN} per volume)"

    local vol_names=()
    for archive in "$BACKUP_DIR"/*.tar.gz; do
        [[ -f "$archive" ]] || continue
        local name vol_name
        name=$(basename "$archive")
        vol_name=$(echo "$name" | sed 's/_[0-9]\{8\}_[0-9]\{6\}\.tar\.gz$//')
        vol_names+=("$vol_name")
    done

    local unique_vols
    unique_vols=$(printf '%s\n' "${vol_names[@]}" 2>/dev/null | sort -u)

    while IFS= read -r vol; do
        [[ -z "$vol" ]] && continue
        local old_archives=()
        while IFS= read -r f; do
            [[ -n "$f" ]] && old_archives+=("$f")
        done < <(ls -1t "$BACKUP_DIR"/${vol}_[0-9]*_[0-9]*.tar.gz 2>/dev/null | tail -n +$((RETAIN + 1)))
        for old_archive in "${old_archives[@]}"; do
            log_info "Removing old backup: $(basename "$old_archive")"
            rm -f "$old_archive"
        done
    done <<< "$unique_vols"
}

main() {
    parse_args "$@"
    check_dependencies
    case "$MODE" in
        backup)  do_backup ;;
        restore) do_restore ;;
        list)    do_list ;;
    esac
}

main "$@"
