#!/usr/bin/env bash

#########################################################################################
#### config-backup.sh — Snapshot system configs into a timestamped tarball            ####
#### Backs up /etc, crontabs, package lists, systemd units, and firewall rules        ####
#### Dry-run by default — nothing is written without --force                          ####
####                                                                                 ####
#### Author: Phil Connor                                                             ####
#### Contact: contact@mylinux.work                                                   ####
#### License: MIT                                                                    ####
#### Version 1.01                                                                    ####
####                                                                                 ####
#### Usage:                                                                          ####
####   ./config-backup.sh                                                            ####
####   ./config-backup.sh --force                                                    ####
####                                                                                 ####
#### See --help for all options.                                                     ####
#########################################################################################

set -euo pipefail

# ── Defaults ──────────────────────────────────────────────────────────
BACKUP_DIR="${BACKUP_DIR:-/var/backups/config-snapshots}"
DRY_RUN="${DRY_RUN:-true}"
VERBOSE="${VERBOSE:-false}"
COLOR="${COLOR:-auto}"

# ── State ─────────────────────────────────────────────────────────────
SCRIPT_NAME="$(basename "$0")"
readonly SCRIPT_NAME
INCLUDE_PATHS=()
EXCLUDE_PATHS=()
STAGING_DIR=""

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

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

# ── Helpers ───────────────────────────────────────────────────────────
section_header() {
    echo ""
    echo -e "  ${BOLD}${CYAN}── $1 ──${RESET}"
    echo ""
}

field() {
    printf "  ${BOLD}%-22s${RESET} %s\n" "$1" "$2"
}

field_color() {
    printf "  ${BOLD}%-22s${RESET} %b\n" "$1" "$2"
}

human_bytes() {
    local bytes="$1"
    if [[ "$bytes" -ge 1073741824 ]]; then
        awk "BEGIN { printf \"%.1f GiB\", $bytes / 1073741824 }"
    elif [[ "$bytes" -ge 1048576 ]]; then
        awk "BEGIN { printf \"%.1f MiB\", $bytes / 1048576 }"
    elif [[ "$bytes" -ge 1024 ]]; then
        awk "BEGIN { printf \"%.1f KiB\", $bytes / 1024 }"
    else
        echo "${bytes} B"
    fi
}

cleanup_staging() {
    if [[ -n "$STAGING_DIR" && -d "$STAGING_DIR" ]]; then
        rm -rf "$STAGING_DIR"
        verbose "Cleaned up staging directory"
    fi
}

is_excluded() {
    local path="$1"
    for exc in "${EXCLUDE_PATHS[@]}"; do
        if [[ "$path" == "$exc" || "$path" == "$exc"/* ]]; then
            return 0
        fi
    done
    return 1
}

# ══════════════════════════════════════════════════════════════════════
#                       COLLECT ITEMS
# ══════════════════════════════════════════════════════════════════════

collect_etc() {
    section_header "/etc Configuration"

    if [[ ! -d /etc ]]; then
        warn "/etc not found"
        return
    fi

    if is_excluded "/etc"; then
        log "Skipping /etc (excluded)"
        return
    fi

    local etc_size
    etc_size=$(du -sb /etc 2>/dev/null | awk '{print $1}' || echo "0")
    field "Size:" "$(human_bytes "$etc_size")"

    local etc_files
    etc_files=$(find /etc -type f 2>/dev/null | wc -l)
    field "Files:" "$etc_files"

    if [[ "$DRY_RUN" == "false" ]]; then
        cp -a /etc "$STAGING_DIR/etc" 2>/dev/null || warn "Some /etc files could not be copied"
        log "Collected /etc"
    else
        log "[DRY-RUN] Would collect /etc"
    fi
}

collect_crontabs() {
    section_header "User Crontabs"

    local crontab_dir="/var/spool/cron/crontabs"
    local count=0

    if [[ -d "$crontab_dir" ]]; then
        count=$(find "$crontab_dir" -type f 2>/dev/null | wc -l)
        field "User crontabs:" "$count"

        if [[ "$VERBOSE" == "true" && "$count" -gt 0 ]]; then
            find "$crontab_dir" -type f 2>/dev/null | while IFS= read -r f; do
                printf "    %s\n" "$(basename "$f")"
            done
        fi

        if [[ "$DRY_RUN" == "false" && "$count" -gt 0 ]]; then
            mkdir -p "$STAGING_DIR/crontabs"
            cp -a "$crontab_dir"/* "$STAGING_DIR/crontabs/" 2>/dev/null || warn "Some crontabs could not be copied"
            log "Collected user crontabs"
        fi
    else
        field "User crontabs:" "0 (${crontab_dir} not found)"
    fi

    # Root crontab via crontab -l
    if crontab -l &>/dev/null; then
        if [[ "$DRY_RUN" == "false" ]]; then
            mkdir -p "$STAGING_DIR/crontabs"
            crontab -l > "$STAGING_DIR/crontabs/root-crontab-l.txt" 2>/dev/null || true
        fi
        field "Root crontab:" "present"
    else
        field "Root crontab:" "none"
    fi

    if [[ "$DRY_RUN" == "true" && "$count" -gt 0 ]]; then
        log "[DRY-RUN] Would collect crontabs"
    fi
}

collect_package_list() {
    section_header "Package List"

    if command -v dpkg &>/dev/null; then
        local dpkg_count
        dpkg_count=$(dpkg -l 2>/dev/null | grep -c "^ii" || true)
        field "dpkg packages:" "$dpkg_count"

        if [[ "$DRY_RUN" == "false" ]]; then
            mkdir -p "$STAGING_DIR/packages"
            dpkg --get-selections > "$STAGING_DIR/packages/dpkg-selections.txt" 2>/dev/null || true
            dpkg -l > "$STAGING_DIR/packages/dpkg-list.txt" 2>/dev/null || true
            log "Collected dpkg package list"
        else
            log "[DRY-RUN] Would collect dpkg package list"
        fi
    fi

    if command -v rpm &>/dev/null; then
        local rpm_count
        rpm_count=$(rpm -qa 2>/dev/null | wc -l || echo "0")
        field "rpm packages:" "$rpm_count"

        if [[ "$DRY_RUN" == "false" ]]; then
            mkdir -p "$STAGING_DIR/packages"
            rpm -qa --qf '%{NAME}-%{VERSION}-%{RELEASE}.%{ARCH}\n' > "$STAGING_DIR/packages/rpm-list.txt" 2>/dev/null || true
            log "Collected rpm package list"
        else
            log "[DRY-RUN] Would collect rpm package list"
        fi
    fi

    if ! command -v dpkg &>/dev/null && ! command -v rpm &>/dev/null; then
        log "No package manager detected (dpkg/rpm)"
    fi
}

collect_systemd_units() {
    section_header "Systemd Units"

    if ! command -v systemctl &>/dev/null; then
        log "systemd not available"
        return
    fi

    local enabled_count
    enabled_count=$(systemctl list-unit-files --state=enabled --no-legend 2>/dev/null | wc -l)
    field "Enabled units:" "$enabled_count"

    local custom_count=0
    for unit_dir in /etc/systemd/system /etc/systemd/user; do
        if [[ -d "$unit_dir" ]]; then
            local dir_count
            dir_count=$(find "$unit_dir" -maxdepth 1 -name "*.service" -o -name "*.timer" 2>/dev/null | wc -l)
            custom_count=$((custom_count + dir_count))
        fi
    done
    field "Custom unit files:" "$custom_count"

    if [[ "$DRY_RUN" == "false" ]]; then
        mkdir -p "$STAGING_DIR/systemd"
        systemctl list-unit-files --no-legend > "$STAGING_DIR/systemd/unit-files.txt" 2>/dev/null || true

        for unit_dir in /etc/systemd/system /etc/systemd/user; do
            if [[ -d "$unit_dir" ]]; then
                cp -a "$unit_dir" "$STAGING_DIR/systemd/" 2>/dev/null || true
            fi
        done
        log "Collected systemd units"
    else
        log "[DRY-RUN] Would collect systemd units"
    fi
}

collect_firewall_rules() {
    section_header "Firewall Rules"

    local fw_found=false

    if command -v iptables &>/dev/null; then
        fw_found=true
        local ipt_rules
        ipt_rules=$(iptables -S 2>/dev/null | wc -l || echo "0")
        field "iptables rules:" "$ipt_rules"

        if [[ "$DRY_RUN" == "false" ]]; then
            mkdir -p "$STAGING_DIR/firewall"
            iptables-save > "$STAGING_DIR/firewall/iptables.rules" 2>/dev/null || warn "Could not save iptables rules"
            log "Collected iptables rules"
        fi
    fi

    if command -v nft &>/dev/null; then
        fw_found=true
        local nft_tables
        nft_tables=$(nft list tables 2>/dev/null | wc -l || echo "0")
        field "nftables tables:" "$nft_tables"

        if [[ "$DRY_RUN" == "false" ]]; then
            mkdir -p "$STAGING_DIR/firewall"
            nft list ruleset > "$STAGING_DIR/firewall/nftables.rules" 2>/dev/null || warn "Could not save nftables rules"
            log "Collected nftables rules"
        fi
    fi

    if [[ "$fw_found" == "false" ]]; then
        log "No firewall tools detected (iptables, nftables)"
    elif [[ "$DRY_RUN" == "true" ]]; then
        log "[DRY-RUN] Would collect firewall rules"
    fi
}

collect_custom_includes() {
    if [[ ${#INCLUDE_PATHS[@]} -eq 0 ]]; then
        return
    fi

    section_header "Custom Includes"

    for inc_path in "${INCLUDE_PATHS[@]}"; do
        if [[ ! -e "$inc_path" ]]; then
            warn "Include path not found: $inc_path"
            continue
        fi

        local inc_size
        inc_size=$(du -sb "$inc_path" 2>/dev/null | awk '{print $1}' || echo "0")
        field "$inc_path:" "$(human_bytes "$inc_size")"

        if [[ "$DRY_RUN" == "false" ]]; then
            local dest_dir="$STAGING_DIR/custom${inc_path}"
            mkdir -p "$(dirname "$dest_dir")"
            cp -a "$inc_path" "$dest_dir" 2>/dev/null || warn "Could not copy $inc_path"
        fi
    done

    if [[ "$DRY_RUN" == "true" ]]; then
        log "[DRY-RUN] Would collect custom paths"
    else
        log "Collected custom paths"
    fi
}

# ══════════════════════════════════════════════════════════════════════
#                       CREATE TARBALL
# ══════════════════════════════════════════════════════════════════════

create_tarball() {
    local timestamp hostname_val tarball_name tarball_path

    timestamp=$(date '+%Y%m%d-%H%M%S')
    hostname_val=$(hostname -s 2>/dev/null || hostname)
    tarball_name="config-backup-${hostname_val}-${timestamp}.tar.gz"
    tarball_path="${BACKUP_DIR}/${tarball_name}"

    section_header "Creating Backup"

    field "Output directory:" "$BACKUP_DIR"
    field "Tarball:" "$tarball_name"

    if [[ "$DRY_RUN" == "true" ]]; then
        # Estimate total size
        local est_size=0

        if [[ -d /etc ]] && ! is_excluded "/etc"; then
            est_size=$((est_size + $(du -sb /etc 2>/dev/null | awk '{print $1}' || echo 0)))
        fi

        for inc_path in "${INCLUDE_PATHS[@]}"; do
            if [[ -e "$inc_path" ]]; then
                est_size=$((est_size + $(du -sb "$inc_path" 2>/dev/null | awk '{print $1}' || echo 0)))
            fi
        done

        field_color "Estimated size:" "${YELLOW}~$(human_bytes "$est_size") (uncompressed)${RESET}"
        echo ""
        echo -e "  ${YELLOW}Dry-run mode — no backup created${RESET}"
        echo -e "  Run with --force to create the backup"
        return
    fi

    # Create output directory
    mkdir -p "$BACKUP_DIR" || { err "Cannot create ${BACKUP_DIR}"; exit 1; }

    # Create tarball from staging
    local staging_size
    staging_size=$(du -sb "$STAGING_DIR" 2>/dev/null | awk '{print $1}' || echo "0")
    field "Staging size:" "$(human_bytes "$staging_size")"

    tar -czf "$tarball_path" -C "$STAGING_DIR" . 2>/dev/null || { err "Failed to create tarball"; exit 1; }

    # Validate tarball
    log "Validating tarball..."
    local file_count
    file_count=$(tar -tzf "$tarball_path" 2>/dev/null | wc -l)

    if [[ "$file_count" -eq 0 ]]; then
        err "Tarball validation failed — archive appears empty"
        exit 1
    fi

    local tarball_size
    tarball_size=$(stat -c%s "$tarball_path" 2>/dev/null || echo "0")

    field_color "Status:" "${GREEN}Success${RESET}"
    field "Archive size:" "$(human_bytes "$tarball_size")"
    field "Files archived:" "$file_count"
    field "Location:" "$tarball_path"
}

# ══════════════════════════════════════════════════════════════════════
#                       USAGE
# ══════════════════════════════════════════════════════════════════════

usage() {
    cat <<EOF
${SCRIPT_NAME} — Snapshot system configs into a timestamped tarball

USAGE:
  ${SCRIPT_NAME} [OPTIONS]

OPTIONS:
  --output-dir DIR      Output directory (default: ${BACKUP_DIR})
  --include PATH        Additional path to include (can be repeated)
  --exclude PATH        Path to exclude (can be repeated)
  --force               Actually create backup (disables dry-run)
  --verbose             Enable debug output
  --no-color            Disable colored output
  --help                Show this help

WHAT IS BACKED UP:
  /etc                  System configuration files
  User crontabs         From /var/spool/cron/crontabs/
  Package lists         dpkg/rpm package lists
  Systemd units         Custom unit files from /etc/systemd/
  Firewall rules        iptables/nftables rule dumps

ENVIRONMENT VARIABLES:
  BACKUP_DIR            Output directory (default: /var/backups/config-snapshots)
  DRY_RUN               Dry-run mode (default: true)
  COLOR                 Color mode: auto, always, never (default: auto)

EXAMPLES:
  # Dry-run (show what would be backed up)
  ./config-backup.sh

  # Create backup
  sudo ./config-backup.sh --force

  # Custom output directory
  sudo ./config-backup.sh --force --output-dir /tmp/backups

  # Include additional paths
  sudo ./config-backup.sh --force --include /opt/myapp/config

  # Exclude paths
  sudo ./config-backup.sh --force --exclude /etc/ssl/private
EOF
}

# ══════════════════════════════════════════════════════════════════════
#                       ARGUMENT PARSING
# ══════════════════════════════════════════════════════════════════════

parse_args() {
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --output-dir)
                BACKUP_DIR="$2"; shift 2 ;;
            --include)
                INCLUDE_PATHS+=("$2"); shift 2 ;;
            --exclude)
                EXCLUDE_PATHS+=("$2"); shift 2 ;;
            --force)
                DRY_RUN="false"; shift ;;
            --verbose)
                VERBOSE="true"; shift ;;
            --no-color)
                COLOR="never"; shift ;;
            --help|-h)
                setup_colors
                usage
                exit 0 ;;
            *)
                err "Unknown option: $1"
                echo "Run ${SCRIPT_NAME} --help for usage" >&2
                exit 1 ;;
        esac
    done
}

# ══════════════════════════════════════════════════════════════════════
#                       MAIN
# ══════════════════════════════════════════════════════════════════════

main() {
    parse_args "$@"
    setup_colors

    echo ""
    echo -e "${BOLD}Config Backup — $(hostname -f 2>/dev/null || hostname)${RESET}"

    if [[ "$DRY_RUN" == "true" ]]; then
        echo -e "Safety: ${YELLOW}dry-run (use --force to create backup)${RESET}"
    else
        echo -e "Safety: ${RED}LIVE — backup will be created${RESET}"
    fi

    echo -e "Time:   $(date -u +%Y-%m-%dT%H:%M:%SZ)"

    # Create staging directory for live runs
    if [[ "$DRY_RUN" == "false" ]]; then
        STAGING_DIR=$(mktemp -d "/tmp/config-backup-XXXXXX")
        trap cleanup_staging EXIT
        verbose "Staging directory: $STAGING_DIR"
    fi

    collect_etc
    collect_crontabs
    collect_package_list
    collect_systemd_units
    collect_firewall_rules
    collect_custom_includes
    create_tarball

    echo ""
}

main "$@"
