#!/usr/bin/env bash
# caprover-backup.sh — Comprehensive CapRover backup script
# Author: Phil Connor <contact@mylinux.work>
# License: MIT
# Version: 1.11
#
# Backs up /captain config, Docker volumes (captain-- prefixed),
# and app definitions via CapRover API.
# Supports local, NFS, and S3 (via aws cli or rclone) destinations.
#
# Migration mode (--migrate) stops all CapRover app containers before
# backing up volumes, ensuring database-consistent snapshots. Produces
# a single migration tarball for transfer to a new server.
#
# Usage:
#   ./caprover-backup.sh                          # local backup to /backups/caprover
#   ./caprover-backup.sh --migrate                # full server migration (stops containers)
#   BACKUP_DEST=s3 S3_BUCKET=my-bucket ./caprover-backup.sh
#   BACKUP_DEST=rclone RCLONE_REMOTE=myremote:backups ./caprover-backup.sh
#   BACKUP_DEST=nfs NFS_MOUNT=/mnt/nfs/backups ./caprover-backup.sh

set -euo pipefail

# ---------------------------------------------------------------------------
# Configuration — override via environment variables
# ---------------------------------------------------------------------------
BACKUP_DIR="${BACKUP_DIR:-/backups/caprover}"
BACKUP_DEST="${BACKUP_DEST:-local}"          # local | nfs | s3 | rclone
RETENTION_DAYS="${RETENTION_DAYS:-30}"
DATE=$(date +%Y%m%d-%H%M%S)
LOG_FILE="${LOG_FILE:-/var/log/caprover-backup.log}"
MIGRATE=false

# CapRover API settings (for app definition export)
CAPROVER_URL="${CAPROVER_URL:-https://captain.apps.example.com}"
CAPROVER_PASSWORD="${CAPROVER_PASSWORD:-}"

# S3 settings
S3_BUCKET="${S3_BUCKET:-}"
S3_PREFIX="${S3_PREFIX:-caprover-backups}"

# rclone settings
RCLONE_REMOTE="${RCLONE_REMOTE:-}"

# NFS settings
NFS_MOUNT="${NFS_MOUNT:-/mnt/nfs/backups}"

# ---------------------------------------------------------------------------
# Logging
# ---------------------------------------------------------------------------
log() {
    local msg
    msg="[$(date '+%Y-%m-%d %H:%M:%S')] $1"
    echo "$msg" | tee -a "$LOG_FILE"
}

log_error() {
    log "ERROR: $1"
}

# ---------------------------------------------------------------------------
# Argument parsing
# ---------------------------------------------------------------------------
for arg in "$@"; do
    case "$arg" in
        --migrate)  MIGRATE=true ;;
        -h|--help)
            sed -n '2,/^$/{ s/^# \?//; p }' "$0"
            exit 0
            ;;
    esac
done

# ---------------------------------------------------------------------------
# Pre-flight checks
# ---------------------------------------------------------------------------
preflight() {
    if [ "$(id -u)" -ne 0 ]; then
        log_error "Run as root."
        exit 1
    fi

    if ! command -v docker &>/dev/null; then
        log_error "Docker not found."
        exit 1
    fi

    if [ "$BACKUP_DEST" = "s3" ] && ! command -v aws &>/dev/null; then
        log_error "aws CLI not found. Install it or use BACKUP_DEST=rclone."
        exit 1
    fi

    if [ "$BACKUP_DEST" = "rclone" ] && ! command -v rclone &>/dev/null; then
        log_error "rclone not found."
        exit 1
    fi

    mkdir -p "$BACKUP_DIR"
    mkdir -p "$(dirname "$LOG_FILE")"
}

# ---------------------------------------------------------------------------
# Backup /captain directory
# ---------------------------------------------------------------------------
backup_captain_config() {
    log "Backing up /captain directory..."
    local dest="${BACKUP_DIR}/captain-config-${DATE}.tar.gz"

    if [ ! -d /captain ]; then
        log_error "/captain directory not found. Is CapRover installed?"
        return 1
    fi

    tar czf "$dest" -C / captain
    log "Captain config saved: $dest ($(du -sh "$dest" | cut -f1))"
}

# ---------------------------------------------------------------------------
# Backup Docker volumes (captain-- prefixed)
# ---------------------------------------------------------------------------
backup_volumes() {
    log "Backing up Docker volumes..."
    local volumes
    volumes=$(docker volume ls -q | grep "^captain--" || true)

    if [ -z "$volumes" ]; then
        log "No captain-- volumes found. Skipping."
        return 0
    fi

    for vol in $volumes; do
        local app_name="${vol#captain--}"
        local dest="${BACKUP_DIR}/vol-${app_name}-${DATE}.tar.gz"

        log "  Backing up volume: $vol"
        docker run --rm \
            -v "${vol}:/source:ro" \
            -v "${BACKUP_DIR}:/backup" \
            alpine tar czf "/backup/vol-${app_name}-${DATE}.tar.gz" -C /source .

        log "  Volume $vol saved: $dest ($(du -sh "$dest" | cut -f1))"
    done
}

# ---------------------------------------------------------------------------
# Export app definitions via CapRover API
# ---------------------------------------------------------------------------
export_app_definitions() {
    if [ -z "$CAPROVER_PASSWORD" ]; then
        log "CAPROVER_PASSWORD not set. Skipping API export."
        return 0
    fi

    log "Exporting app definitions via CapRover API..."

    # Get auth token
    local token
    token=$(curl -s -X POST "${CAPROVER_URL}/api/v2/login" \
        -H "Content-Type: application/json" \
        -H "x-namespace: captain" \
        -d "{\"password\":\"${CAPROVER_PASSWORD}\"}" \
        | python3 -c "import sys,json; print(json.load(sys.stdin)['data']['token'])" 2>/dev/null) || true

    if [ -z "$token" ]; then
        log_error "Failed to authenticate with CapRover API."
        return 1
    fi

    # Export app definitions
    local dest="${BACKUP_DIR}/app-definitions-${DATE}.json"
    local http_code
    http_code=$(curl -s -o "$dest" -w "%{http_code}" \
        "${CAPROVER_URL}/api/v2/user/apps/appDefinitions" \
        -H "Content-Type: application/json" \
        -H "x-namespace: captain" \
        -H "x-captain-auth: ${token}")

    if [ "$http_code" = "200" ]; then
        log "App definitions saved: $dest"
    else
        log_error "API returned HTTP $http_code. App definitions export failed."
        rm -f "$dest"
        return 1
    fi
}

# ---------------------------------------------------------------------------
# Upload to remote destination
# ---------------------------------------------------------------------------
upload_remote() {
    case "$BACKUP_DEST" in
        local)
            log "Backup stored locally at $BACKUP_DIR"
            ;;
        nfs)
            log "Copying backups to NFS mount: $NFS_MOUNT"
            if ! mountpoint -q "$NFS_MOUNT" 2>/dev/null; then
                log_error "$NFS_MOUNT is not mounted."
                return 1
            fi
            mkdir -p "${NFS_MOUNT}/caprover"
            cp "${BACKUP_DIR}"/*-"${DATE}"* "${NFS_MOUNT}/caprover/"
            log "Copied to NFS."
            ;;
        s3)
            log "Uploading backups to S3: s3://${S3_BUCKET}/${S3_PREFIX}/"
            for f in "${BACKUP_DIR}"/*-"${DATE}"*; do
                aws s3 cp "$f" "s3://${S3_BUCKET}/${S3_PREFIX}/$(basename "$f")" --quiet
            done
            log "S3 upload complete."
            ;;
        rclone)
            log "Uploading backups via rclone to: $RCLONE_REMOTE"
            for f in "${BACKUP_DIR}"/*-"${DATE}"*; do
                rclone copy "$f" "$RCLONE_REMOTE" --quiet
            done
            log "rclone upload complete."
            ;;
        *)
            log_error "Unknown BACKUP_DEST: $BACKUP_DEST"
            return 1
            ;;
    esac
}

# ---------------------------------------------------------------------------
# Retention — delete local backups older than RETENTION_DAYS
# ---------------------------------------------------------------------------
apply_retention() {
    log "Applying retention policy: deleting backups older than ${RETENTION_DAYS} days..."
    local count
    count=$(find "$BACKUP_DIR" -name "*.tar.gz" -o -name "*.json" | \
        xargs -I{} find {} -mtime +"$RETENTION_DAYS" 2>/dev/null | wc -l)

    find "$BACKUP_DIR" \( -name "*.tar.gz" -o -name "*.json" \) \
        -mtime +"$RETENTION_DAYS" -delete

    log "Removed $count old backup file(s)."
}

# ---------------------------------------------------------------------------
# Migration — stop all CapRover app containers
# ---------------------------------------------------------------------------
stop_captain_containers() {
    log "Stopping all CapRover app containers..."
    local containers
    containers=$(docker ps -q --filter "label=com.docker.swarm.service.name" \
        --filter "name=srv-captain--" 2>/dev/null || true)

    if [ -z "$containers" ]; then
        # fallback: stop services via Docker Swarm
        local services
        services=$(docker service ls -q --filter "name=srv-captain--" 2>/dev/null || true)
        if [ -n "$services" ]; then
            local count=0
            for svc in $services; do
                local svc_name
                svc_name=$(docker service inspect --format '{{.Spec.Name}}' "$svc")
                log "  Scaling down: $svc_name"
                docker service scale "$svc_name=0" --detach 2>/dev/null
                ((count++)) || true
            done
            log "Scaled down $count service(s). Waiting 10s for graceful shutdown..."
            sleep 10
        else
            log "No CapRover app services found."
        fi
    else
        local count
        count=$(echo "$containers" | wc -w)
        docker stop $containers
        log "Stopped $count container(s). Waiting 5s..."
        sleep 5
    fi
}

# ---------------------------------------------------------------------------
# Migration — record service state for restore on new server
# ---------------------------------------------------------------------------
save_service_state() {
    log "Saving Docker service state..."
    local dest="${BACKUP_DIR}/service-state-${DATE}.json"
    docker service ls --format '{{json .}}' > "$dest"
    log "Service state saved: $dest"

    # Save docker info for reference (Swarm tokens, node info)
    local info_dest="${BACKUP_DIR}/docker-info-${DATE}.txt"
    docker info > "$info_dest" 2>&1
    docker node ls >> "$info_dest" 2>/dev/null || true
    log "Docker info saved: $info_dest"
}

# ---------------------------------------------------------------------------
# Migration — package everything into a single tarball
# ---------------------------------------------------------------------------
create_migration_bundle() {
    local bundle="${BACKUP_DIR}/caprover-migration-${DATE}.tar.gz"
    log "Creating migration bundle..."

    # Collect all files from this run
    tar czf "$bundle" -C "$BACKUP_DIR" \
        $(ls -1 "$BACKUP_DIR" | grep "$DATE" | grep -v "caprover-migration")

    local size
    size=$(du -sh "$bundle" | cut -f1)
    log "Migration bundle ready: $bundle ($size)"
    log ""
    log "========================================="
    log "  MIGRATION INSTRUCTIONS"
    log "========================================="
    log "1. Copy bundle to new server:"
    log "     scp $bundle root@new-server:/backups/"
    log ""
    log "2. On the new server, install CapRover:"
    log "     docker run -p 80:80 -p 443:443 -p 3000:3000 \\"
    log "       -e ACCEPTED_TERMS=true -v /captain:/captain \\"
    log "       caprover/caprover-edge"
    log ""
    log "3. Extract the bundle:"
    log "     mkdir -p /backups/restore && cd /backups/restore"
    log "     tar xzf caprover-migration-${DATE}.tar.gz"
    log ""
    log "4. Stop CapRover on new server:"
    log "     docker service rm captain-captain --force"
    log ""
    log "5. Restore /captain config:"
    log "     tar xzf captain-config-${DATE}.tar.gz -C /"
    log ""
    log "6. Restore Docker volumes:"
    log "     for f in vol-*-${DATE}.tar.gz; do"
    log '       vol="captain--${f#vol-}"'
    log '       vol="${vol%-'"${DATE}"'.tar.gz}"'
    log "       docker volume create \"\$vol\""
    log "       docker run --rm -v \"\${vol}:/dest\" -v \"\$(pwd):/backup:ro\" \\"
    log "         alpine sh -c \"tar xzf /backup/\$f -C /dest\""
    log "     done"
    log ""
    log "7. Start CapRover and re-deploy apps:"
    log "     docker run -p 80:80 -p 443:443 -p 3000:3000 \\"
    log "       -e ACCEPTED_TERMS=true -v /captain:/captain \\"
    log "       caprover/caprover-edge"
    log ""
    log "8. Update DNS to point to the new server IP."
    log "========================================="
}

# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
main() {
    if $MIGRATE; then
        log "========================================="
        log "CapRover MIGRATION backup — ${DATE}"
        log "Mode: full migration (containers will be stopped)"
        log "========================================="

        preflight
        local errors=0

        export_app_definitions || { ((errors++)) || true; }
        save_service_state || { ((errors++)) || true; }
        stop_captain_containers
        backup_captain_config || { ((errors++)) || true; }
        backup_volumes || { ((errors++)) || true; }
        create_migration_bundle

        if [ "$errors" -gt 0 ]; then
            log "Migration backup completed with $errors error(s)."
            exit 1
        else
            log "Migration backup completed successfully."
            log "Containers remain stopped. This server is ready to decommission."
        fi
    else
        log "========================================="
        log "CapRover backup started — ${DATE}"
        log "Destination: ${BACKUP_DEST}"
        log "========================================="

        preflight
        local errors=0

        backup_captain_config || { ((errors++)) || true; }
        backup_volumes || { ((errors++)) || true; }
        export_app_definitions || { ((errors++)) || true; }
        upload_remote || { ((errors++)) || true; }
        apply_retention

        if [ "$errors" -gt 0 ]; then
            log "Backup completed with $errors error(s)."
            exit 1
        else
            log "Backup completed successfully."
        fi
    fi
}

main "$@"
