#!/usr/bin/env bash

#########################################################################################
#### jenkins-backup.sh — Backup and restore Jenkins configuration and jobs           ####
#### Supports JENKINS_HOME tar, job XML export, and credential backup                ####
#### Requires: bash 4+, tar, curl                                                   ####
####                                                                                 ####
#### Author: Phil Connor                                                             ####
#### Contact: contact@mylinux.work                                                   ####
#### License: MIT                                                                    ####
#### Version 1.01                                                                    ####
####                                                                                 ####
#### Usage:                                                                          ####
####   export JENKINS_HOME="/var/lib/jenkins"                                        ####
####   ./jenkins-backup.sh --backup                                                  ####
####                                                                                 ####
#### See --help for all options.                                                     ####
#########################################################################################

set -euo pipefail

# ── Defaults ──────────────────────────────────────────────────────────
JENKINS_HOME="${JENKINS_HOME:-/var/lib/jenkins}"
BACKUP_DIR="${BACKUP_DIR:-/var/backups/jenkins}"
RETENTION_COUNT="${RETENTION_COUNT:-7}"
JENKINS_URL="${JENKINS_URL:-}"
JENKINS_USER="${JENKINS_USER:-}"
JENKINS_TOKEN="${JENKINS_TOKEN:-}"
BACKUP_TYPE="${BACKUP_TYPE:-full}"
EXCLUDE_PLUGINS="${EXCLUDE_PLUGINS:-false}"
CURL_TIMEOUT="${CURL_TIMEOUT:-30}"
CURL_INSECURE="${CURL_INSECURE:-false}"
VERBOSE="${VERBOSE:-false}"
COLOR="${COLOR:-auto}"

# ── State ─────────────────────────────────────────────────────────────
SCRIPT_NAME="$(basename "$0")"
readonly SCRIPT_NAME
RUN_MODE="backup"
RESTORE_DIR=""
DRY_RUN="false"
CONFIG_ONLY="false"
API_BACKUP="false"
TMPDIR_WORK=""
START_TIME=""

# ── Colors ────────────────────────────────────────────────────────────
setup_colors() {
    if [[ "$COLOR" == "never" ]]; then
        RED="" GREEN="" YELLOW="" BLUE="" BOLD="" RESET=""
        return
    fi
    if [[ "$COLOR" == "always" ]] || [[ -t 1 ]]; then
        RED='\033[0;31m'
        GREEN='\033[0;32m'
        YELLOW='\033[0;33m'
        BLUE='\033[0;34m'
        BOLD='\033[1m'
        RESET='\033[0m'
    else
        RED="" GREEN="" YELLOW="" BLUE="" BOLD="" RESET=""
    fi
}

# ── Logging ───────────────────────────────────────────────────────────
log()     { echo -e "${BLUE}[INFO]${RESET} $*"; }
warn()    { echo -e "${YELLOW}[WARN]${RESET} $*" >&2; }
err()     { echo -e "${RED}[ERROR]${RESET} $*" >&2; }
verbose() { if [[ "$VERBOSE" == "true" ]]; then echo -e "${BLUE}[DEBUG]${RESET} $*"; fi; }

# ── Cleanup ───────────────────────────────────────────────────────────
cleanup() {
    if [[ -n "$TMPDIR_WORK" && -d "$TMPDIR_WORK" ]]; then
        verbose "Cleaning up temp directory: $TMPDIR_WORK"
        rm -rf "$TMPDIR_WORK"
    fi
}

trap cleanup EXIT

# ── Helpers ───────────────────────────────────────────────────────────
human_size() {
    local bytes="$1"
    if [[ "$bytes" -ge 1073741824 ]]; then
        echo "$(( bytes / 1073741824 ))G"
    elif [[ "$bytes" -ge 1048576 ]]; then
        echo "$(( bytes / 1048576 ))M"
    elif [[ "$bytes" -ge 1024 ]]; then
        echo "$(( bytes / 1024 ))K"
    else
        echo "${bytes}B"
    fi
}

elapsed_time() {
    local end_time
    end_time=$(date +%s)
    local duration=$(( end_time - START_TIME ))
    local mins=$(( duration / 60 ))
    local secs=$(( duration % 60 ))
    if [[ "$mins" -gt 0 ]]; then
        echo "${mins}m ${secs}s"
    else
        echo "${secs}s"
    fi
}

jenkins_api() {
    local endpoint="$1"
    local output="${2:-}"
    local url="${JENKINS_URL%/}${endpoint}"
    local curl_opts=(-s -S --max-time "$CURL_TIMEOUT" -f)

    [[ "$CURL_INSECURE" == "true" ]] && curl_opts+=(-k)

    if [[ -n "$JENKINS_USER" && -n "$JENKINS_TOKEN" ]]; then
        curl_opts+=(-u "${JENKINS_USER}:${JENKINS_TOKEN}")
    fi

    if [[ -n "$output" ]]; then
        curl "${curl_opts[@]}" "$url" -o "$output"
    else
        curl "${curl_opts[@]}" "$url"
    fi
}

# ── Show Help ─────────────────────────────────────────────────────────
show_help() {
    cat << EOF
Usage: $SCRIPT_NAME [OPTIONS]

Backup and restore Jenkins configuration, jobs, credentials, and plugins.
Supports full JENKINS_HOME archives, config-only snapshots, and remote
API exports with automatic retention.

OPTIONS:
    --backup            Run a backup (default)
    --config-only       Backup configuration files only (no workspaces/builds)
    --api-backup        Export job configs via Jenkins API (remote, no filesystem)
    --restore DIR       Restore from the specified backup directory or archive
    --dry-run           With --restore, show what would be restored without changes
    --list              List available backups
    --verify DIR        Verify backup integrity via checksums
    --help, -h          Show this help message

ENVIRONMENT VARIABLES:
    JENKINS_HOME        Jenkins home directory (default: /var/lib/jenkins)
    BACKUP_DIR          Root backup directory (default: /var/backups/jenkins)
    RETENTION_COUNT     Number of backups to retain (default: 7)
    JENKINS_URL         Jenkins base URL (required for --api-backup)
    JENKINS_USER        Jenkins username (required for --api-backup)
    JENKINS_TOKEN       Jenkins API token (required for --api-backup)
    BACKUP_TYPE         Backup type: full, config, api (default: full)
    EXCLUDE_PLUGINS     Skip plugin JPI files in config backup (default: false)
    CURL_TIMEOUT        HTTP timeout in seconds (default: 30)
    CURL_INSECURE       Allow self-signed certs (default: false)
    VERBOSE             Enable verbose output (default: false)
    COLOR               Color output: auto, always, never (default: auto)

EXAMPLES:
    # Full JENKINS_HOME backup
    JENKINS_HOME=/var/lib/jenkins ./jenkins-backup.sh --backup

    # Config-only backup (XML configs + plugins, no build data)
    ./jenkins-backup.sh --config-only

    # Remote API backup (no filesystem access needed)
    JENKINS_URL=http://localhost:8080 JENKINS_USER=admin JENKINS_TOKEN=xxxx \\
        ./jenkins-backup.sh --api-backup

    # Restore from a backup
    ./jenkins-backup.sh --restore /var/backups/jenkins/20260404-120000-full

    # Dry-run restore
    ./jenkins-backup.sh --restore /var/backups/jenkins/20260404-120000-full --dry-run

    # List available backups
    ./jenkins-backup.sh --list

    # Verify a backup
    ./jenkins-backup.sh --verify /var/backups/jenkins/20260404-120000-full

EOF
    exit 0
}

# ── Parse Arguments ───────────────────────────────────────────────────
parse_args() {
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --backup)       RUN_MODE="backup"; shift ;;
            --config-only)  RUN_MODE="backup"; CONFIG_ONLY="true"; BACKUP_TYPE="config"; shift ;;
            --api-backup)   RUN_MODE="backup"; API_BACKUP="true"; BACKUP_TYPE="api"; shift ;;
            --restore)
                RUN_MODE="restore"
                if [[ $# -lt 2 ]]; then
                    err "--restore requires a directory argument"
                    exit 1
                fi
                RESTORE_DIR="$2"; shift 2
                ;;
            --dry-run)      DRY_RUN="true"; shift ;;
            --list)         RUN_MODE="list"; shift ;;
            --verify)
                RUN_MODE="verify"
                if [[ $# -lt 2 ]]; then
                    err "--verify requires a directory argument"
                    exit 1
                fi
                RESTORE_DIR="$2"; shift 2
                ;;
            --help|-h)      show_help ;;
            *)
                err "Unknown option: $1"
                echo "Run '$SCRIPT_NAME --help' for usage." >&2
                exit 1
                ;;
        esac
    done
}

# ── Validation ────────────────────────────────────────────────────────
validate_backup() {
    if [[ "$API_BACKUP" == "true" ]]; then
        if [[ -z "$JENKINS_URL" ]]; then
            err "JENKINS_URL is required for --api-backup"
            exit 1
        fi
        if [[ -z "$JENKINS_USER" || -z "$JENKINS_TOKEN" ]]; then
            err "JENKINS_USER and JENKINS_TOKEN are required for --api-backup"
            exit 1
        fi
        if ! command -v curl &>/dev/null; then
            err "curl is required but not installed"
            exit 1
        fi
    else
        if [[ ! -d "$JENKINS_HOME" ]]; then
            err "JENKINS_HOME does not exist: $JENKINS_HOME"
            exit 1
        fi
        for cmd in tar sha256sum; do
            if ! command -v "$cmd" &>/dev/null; then
                err "$cmd is required but not installed"
                exit 1
            fi
        done
    fi

    mkdir -p "$BACKUP_DIR"
}

# ── Full Backup ───────────────────────────────────────────────────────
do_full_backup() {
    local timestamp
    timestamp=$(date +%Y%m%d-%H%M%S)
    local dest="$BACKUP_DIR/${timestamp}-full"
    local archive="$dest/jenkins-full-${timestamp}.tar.gz"

    mkdir -p "$dest"
    log "Starting full backup of $JENKINS_HOME"
    verbose "Destination: $dest"

    local tar_excludes=(
        --exclude='workspace'
        --exclude='*/builds/*/archive'
        --exclude='*.log'
    )

    tar czf "$archive" \
        "${tar_excludes[@]}" \
        -C "$(dirname "$JENKINS_HOME")" \
        "$(basename "$JENKINS_HOME")" 2>/dev/null || {
        err "Failed to create tar archive"
        rm -rf "$dest"
        exit 1
    }

    sha256sum "$archive" > "$dest/checksums.sha256"
    echo "full" > "$dest/.backup-type"

    local size
    size=$(stat -c%s "$archive" 2>/dev/null || stat -f%z "$archive" 2>/dev/null || echo 0)
    log "Full backup complete: $(human_size "$size") in $(elapsed_time)"
    log "Archive: $archive"
}

# ── Config-Only Backup ────────────────────────────────────────────────
do_config_backup() {
    local timestamp
    timestamp=$(date +%Y%m%d-%H%M%S)
    local dest="$BACKUP_DIR/${timestamp}-config"
    local archive="$dest/jenkins-config-${timestamp}.tar.gz"

    mkdir -p "$dest"
    log "Starting config-only backup of $JENKINS_HOME"
    verbose "Destination: $dest"

    TMPDIR_WORK=$(mktemp -d)
    local staging="$TMPDIR_WORK/jenkins-config"
    mkdir -p "$staging"

    # Core config files
    for f in config.xml credentials.xml hudson.model.UpdateCenter.xml jenkins.model.JenkinsLocationConfiguration.xml; do
        if [[ -f "$JENKINS_HOME/$f" ]]; then
            cp "$JENKINS_HOME/$f" "$staging/"
            verbose "  Copied $f"
        fi
    done

    # Directories: users, secrets, nodes
    for d in users secrets nodes; do
        if [[ -d "$JENKINS_HOME/$d" ]]; then
            cp -r "$JENKINS_HOME/$d" "$staging/"
            verbose "  Copied $d/"
        fi
    done

    # Job configs (config.xml only)
    if [[ -d "$JENKINS_HOME/jobs" ]]; then
        find "$JENKINS_HOME/jobs" -name config.xml -type f | while IFS= read -r jobxml; do
            local relpath
            relpath="${jobxml#"$JENKINS_HOME"/}"
            local target_dir
            target_dir="$staging/$(dirname "$relpath")"
            mkdir -p "$target_dir"
            cp "$jobxml" "$target_dir/"
        done
        verbose "  Copied job configs"
    fi

    # Plugin JPI files
    if [[ "$EXCLUDE_PLUGINS" != "true" && -d "$JENKINS_HOME/plugins" ]]; then
        mkdir -p "$staging/plugins"
        find "$JENKINS_HOME/plugins" -maxdepth 1 -name '*.jpi' -exec cp {} "$staging/plugins/" \;
        local plugin_count
        plugin_count=$(find "$staging/plugins" -name '*.jpi' 2>/dev/null | wc -l)
        verbose "  Copied $plugin_count plugin files"
    fi

    tar czf "$archive" -C "$TMPDIR_WORK" "jenkins-config" 2>/dev/null || {
        err "Failed to create config archive"
        rm -rf "$dest"
        exit 1
    }

    sha256sum "$archive" > "$dest/checksums.sha256"
    echo "config" > "$dest/.backup-type"

    local size
    size=$(stat -c%s "$archive" 2>/dev/null || stat -f%z "$archive" 2>/dev/null || echo 0)
    log "Config backup complete: $(human_size "$size") in $(elapsed_time)"
    log "Archive: $archive"
}

# ── API Backup ────────────────────────────────────────────────────────
do_api_backup() {
    local timestamp
    timestamp=$(date +%Y%m%d-%H%M%S)
    local dest="$BACKUP_DIR/${timestamp}-api"

    mkdir -p "$dest/jobs" "$dest/plugins"
    log "Starting API backup from $JENKINS_URL"
    verbose "Destination: $dest"

    # Get job list — parse JSON without jq
    local job_list_json
    job_list_json=$(jenkins_api "/api/json?tree=jobs[name]") || {
        err "Failed to fetch job list from Jenkins API"
        rm -rf "$dest"
        exit 1
    }

    # Extract job names from JSON using grep/sed
    local job_names
    job_names=$(echo "$job_list_json" | { grep -o '"name" *: *"[^"]*"' || true; } | sed 's/.*: *"//;s/"$//')

    local job_count=0
    local job_fail=0
    if [[ -n "$job_names" ]]; then
        while IFS= read -r job_name; do
            [[ -z "$job_name" ]] && continue
            local encoded_name
            encoded_name="${job_name// /%20}"
            if jenkins_api "/job/${encoded_name}/config.xml" "$dest/jobs/${job_name}.xml" 2>/dev/null; then
                verbose "  Exported job: $job_name"
                job_count=$((job_count + 1))
            else
                warn "Failed to export job: $job_name"
                job_fail=$((job_fail + 1))
            fi
        done <<< "$job_names"
    fi
    log "Exported $job_count jobs ($job_fail failed)"

    # Plugin list
    local plugin_json
    if plugin_json=$(jenkins_api "/pluginManager/api/json?depth=1&tree=plugins[shortName,version,enabled,active]"); then
        echo "$plugin_json" > "$dest/plugins/plugin-list.json"
        local plugin_count
        plugin_count=$(echo "$plugin_json" | { grep -o '"shortName"' || true; } | wc -l)
        log "Exported plugin list: $plugin_count plugins"
    else
        warn "Failed to fetch plugin list"
    fi

    # Create checksum file for all exported files
    if command -v sha256sum &>/dev/null; then
        find "$dest" -type f ! -name 'checksums.sha256' -exec sha256sum {} + > "$dest/checksums.sha256" 2>/dev/null || true
    fi
    echo "api" > "$dest/.backup-type"

    local size
    size=$(du -sb "$dest" 2>/dev/null | cut -f1)
    size="${size:-0}"
    log "API backup complete: $(human_size "$size") in $(elapsed_time)"
    log "Backup directory: $dest"
}

# ── Backup Entry Point ───────────────────────────────────────────────
do_backup() {
    START_TIME=$(date +%s)
    validate_backup

    if [[ "$API_BACKUP" == "true" ]]; then
        do_api_backup
    elif [[ "$CONFIG_ONLY" == "true" ]]; then
        do_config_backup
    else
        do_full_backup
    fi

    prune_backups
}

# ── Prune ─────────────────────────────────────────────────────────────
prune_backups() {
    log "Pruning old backups (retaining $RETENTION_COUNT)..."

    local backup_count
    backup_count=$(find "$BACKUP_DIR" -mindepth 1 -maxdepth 1 -type d | wc -l)

    if [[ "$backup_count" -le "$RETENTION_COUNT" ]]; then
        log "  No pruning needed ($backup_count backups present)"
        return 0
    fi

    local remove_count=$((backup_count - RETENTION_COUNT))
    find "$BACKUP_DIR" -mindepth 1 -maxdepth 1 -type d | sort | head -n "$remove_count" | while IFS= read -r dir; do
        log "  Removing $(basename "$dir")"
        rm -rf "$dir"
    done

    log "  Pruned $remove_count old backups"
}

# ── Restore ───────────────────────────────────────────────────────────
do_restore() {
    if [[ -z "$RESTORE_DIR" ]]; then
        err "--restore requires a directory argument"
        exit 1
    fi

    if [[ ! -d "$RESTORE_DIR" ]]; then
        err "Restore directory does not exist: $RESTORE_DIR"
        exit 1
    fi

    # Find the tar archive in the backup directory
    local archive
    archive=$(find "$RESTORE_DIR" -maxdepth 1 -name '*.tar.gz' -type f | head -1)

    if [[ -z "$archive" ]]; then
        err "No tar archive found in: $RESTORE_DIR"
        exit 1
    fi

    log "Restore source: $archive"
    log "Restore target: $JENKINS_HOME"

    if [[ "$DRY_RUN" == "true" ]]; then
        log "${BOLD}Dry run — listing archive contents:${RESET}"
        tar tzf "$archive" | head -50
        local total
        total=$(tar tzf "$archive" | wc -l)
        log "  ($total files total)"
        log "Dry run complete — no changes made"
        return 0
    fi

    # Verify checksum if available
    if [[ -f "$RESTORE_DIR/checksums.sha256" ]]; then
        log "Verifying backup integrity..."
        if (cd "$RESTORE_DIR" && sha256sum -c checksums.sha256 --quiet 2>/dev/null); then
            log "  ${GREEN}Checksums verified${RESET}"
        else
            warn "Checksum verification failed — proceed with caution"
        fi
    fi

    if [[ ! -d "$JENKINS_HOME" ]]; then
        mkdir -p "$JENKINS_HOME"
    fi

    log "Restoring archive..."
    tar xzf "$archive" -C "$(dirname "$JENKINS_HOME")" || {
        err "Failed to extract archive"
        exit 1
    }

    warn "Jenkins must be restarted for changes to take effect"
    warn "  sudo systemctl restart jenkins"
    log "Restore complete"
}

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

    local count=0
    local format="  %-28s %-8s %s\n"

    printf "\n"
    # shellcheck disable=SC2059
    printf "$format" "BACKUP" "TYPE" "SIZE"
    # shellcheck disable=SC2059
    printf "$format" "----------------------------" "--------" "--------"

    for dir in "$BACKUP_DIR"/*/; do
        [[ -d "$dir" ]] || continue
        count=$((count + 1))

        local name
        name=$(basename "$dir")

        local btype="unknown"
        if [[ -f "$dir/.backup-type" ]]; then
            btype=$(cat "$dir/.backup-type")
        fi

        local size
        size=$(du -sh "$dir" 2>/dev/null | cut -f1)
        size="${size:-?}"

        # shellcheck disable=SC2059
        printf "$format" "$name" "$btype" "$size"
    done

    printf "\n"
    if [[ "$count" -eq 0 ]]; then
        log "No backups found in $BACKUP_DIR"
    else
        log "$count backup(s) in $BACKUP_DIR"
    fi
}

# ── Verify ────────────────────────────────────────────────────────────
do_verify() {
    if [[ -z "$RESTORE_DIR" ]]; then
        err "--verify requires a directory argument"
        exit 1
    fi

    if [[ ! -d "$RESTORE_DIR" ]]; then
        err "Backup directory does not exist: $RESTORE_DIR"
        exit 1
    fi

    log "Verifying backup: $RESTORE_DIR"

    # Check backup type
    local btype="unknown"
    if [[ -f "$RESTORE_DIR/.backup-type" ]]; then
        btype=$(cat "$RESTORE_DIR/.backup-type")
    fi
    log "  Backup type: $btype"

    # Check for checksums
    if [[ -f "$RESTORE_DIR/checksums.sha256" ]]; then
        log "  Verifying checksums..."
        if (cd "$RESTORE_DIR" && sha256sum -c checksums.sha256 2>/dev/null); then
            log "  ${GREEN}All checksums passed${RESET}"
        else
            err "Checksum verification FAILED"
            exit 1
        fi
    else
        warn "No checksum file found — cannot verify integrity"
    fi

    # Archive contents summary
    local archive
    archive=$(find "$RESTORE_DIR" -maxdepth 1 -name '*.tar.gz' -type f | head -1)
    if [[ -n "$archive" ]]; then
        local size
        size=$(stat -c%s "$archive" 2>/dev/null || stat -f%z "$archive" 2>/dev/null || echo 0)
        log "  Archive: $(basename "$archive") ($(human_size "$size"))"
        local file_count
        file_count=$(tar tzf "$archive" | wc -l)
        log "  Files in archive: $file_count"
    fi

    # API backup — list exported files
    if [[ "$btype" == "api" ]]; then
        local job_count
        job_count=$(find "$RESTORE_DIR/jobs" -name '*.xml' 2>/dev/null | wc -l)
        log "  Exported jobs: $job_count"
        if [[ -f "$RESTORE_DIR/plugins/plugin-list.json" ]]; then
            local plugin_count
            plugin_count=$({ grep -o '"shortName"' "$RESTORE_DIR/plugins/plugin-list.json" || true; } | wc -l)
            log "  Plugins recorded: $plugin_count"
        fi
    fi

    log "Verification complete"
}

# ── Main ──────────────────────────────────────────────────────────────
main() {
    setup_colors

    parse_args "$@"

    case "$RUN_MODE" in
        backup)  do_backup ;;
        restore) do_restore ;;
        list)    do_list ;;
        verify)  do_verify ;;
        *)       err "Unknown mode: $RUN_MODE"; exit 1 ;;
    esac
}

main "$@"
