#!/usr/bin/env bash

#########################################################################################
#### backup-verify.sh — Verify backup files exist, are recent, and aren't zero-size  ####
#### Checks file existence, size, and modification time against thresholds            ####
####                                                                                 ####
#### Author: Phil Connor                                                             ####
#### Contact: contact@mylinux.work                                                   ####
#### License: MIT                                                                    ####
#### Version 1.00                                                                    ####
####                                                                                 ####
#### Usage:                                                                          ####
####   ./backup-verify.sh /var/backups/*.tar.gz                                      ####
####   ./backup-verify.sh --config backup-paths.conf                                 ####
####                                                                                 ####
#### See --help for all options.                                                     ####
#########################################################################################

set -euo pipefail

# ── Defaults ──────────────────────────────────────────────────────────
BACKUP_MAX_AGE="${BACKUP_MAX_AGE:-24h}"
BACKUP_MIN_SIZE="${BACKUP_MIN_SIZE:-1}"
VERBOSE="${VERBOSE:-false}"
COLOR="${COLOR:-auto}"
CONFIG_FILE=""
PATH_FILE=""

# ── State ─────────────────────────────────────────────────────────────
SCRIPT_NAME="$(basename "$0")"
readonly SCRIPT_NAME
ENTRIES=()
COUNT_TOTAL=0
COUNT_OK=0
COUNT_WARNING=0
COUNT_CRITICAL=0

# ── 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 "${CYAN}[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
}

human_age() {
    local seconds="$1"
    local days=$((seconds / 86400))
    local hours=$(( (seconds % 86400) / 3600 ))
    local mins=$(( (seconds % 3600) / 60 ))

    if [[ "$days" -gt 0 ]]; then
        echo "${days}d ${hours}h"
    elif [[ "$hours" -gt 0 ]]; then
        echo "${hours}h ${mins}m"
    else
        echo "${mins}m"
    fi
}

# Convert age string (24h, 7d, 2w) to seconds
parse_age_to_seconds() {
    local age="$1"
    local num="${age%[hdw]*}"
    local unit="${age##*[0-9]}"

    case "$unit" in
        h) echo $((num * 3600)) ;;
        d) echo $((num * 86400)) ;;
        w) echo $((num * 604800)) ;;
        *) echo $((num * 3600)) ;;
    esac
}

# Convert size string (1, 1K, 1M, 1G) to bytes
parse_size_to_bytes() {
    local size="$1"

    # Pure number
    if [[ "$size" =~ ^[0-9]+$ ]]; then
        echo "$size"
        return
    fi

    local num="${size%[KkMmGg]*}"
    local unit="${size##*[0-9]}"

    case "${unit^^}" in
        K) echo $((num * 1024)) ;;
        M) echo $((num * 1048576)) ;;
        G) echo $((num * 1073741824)) ;;
        *) echo "$num" ;;
    esac
}

# ══════════════════════════════════════════════════════════════════════
#                       VERIFICATION
# ══════════════════════════════════════════════════════════════════════

verify_file() {
    local file="$1"
    local max_age_str="$2"
    local min_size_str="$3"

    local max_age_secs min_size_bytes
    max_age_secs=$(parse_age_to_seconds "$max_age_str")
    min_size_bytes=$(parse_size_to_bytes "$min_size_str")

    COUNT_TOTAL=$((COUNT_TOTAL + 1))
    verbose "Checking: ${file} (max-age=${max_age_str}, min-size=${min_size_str})"

    # Check existence
    if [[ ! -e "$file" ]]; then
        printf "  %b%-50s %10s %10s %s%b\n" "$RED" "$file" "--" "--" "MISSING" "$RESET"
        COUNT_CRITICAL=$((COUNT_CRITICAL + 1))
        return
    fi

    # Get file info
    local file_size file_mtime now_epoch age_secs
    file_size=$(stat -c%s "$file" 2>/dev/null || echo "0")
    file_mtime=$(stat -c%Y "$file" 2>/dev/null || echo "0")
    now_epoch=$(date +%s)
    age_secs=$((now_epoch - file_mtime))

    local size_str age_str
    size_str=$(human_bytes "$file_size")
    age_str=$(human_age "$age_secs")

    # Check zero-size
    if [[ "$file_size" -eq 0 ]]; then
        printf "  %b%-50s %10s %10s %s%b\n" "$RED" "$file" "$size_str" "$age_str" "EMPTY" "$RESET"
        COUNT_CRITICAL=$((COUNT_CRITICAL + 1))
        return
    fi

    # Check minimum size
    if [[ "$file_size" -lt "$min_size_bytes" ]]; then
        printf "  %b%-50s %10s %10s %s%b\n" "$YELLOW" "$file" "$size_str" "$age_str" "SMALL" "$RESET"
        COUNT_WARNING=$((COUNT_WARNING + 1))
        return
    fi

    # Check age
    if [[ "$age_secs" -gt "$max_age_secs" ]]; then
        printf "  %b%-50s %10s %10s %s%b\n" "$YELLOW" "$file" "$size_str" "$age_str" "STALE" "$RESET"
        COUNT_WARNING=$((COUNT_WARNING + 1))
        return
    fi

    # All good
    printf "  %b%-50s %10s %10s %s%b\n" "$GREEN" "$file" "$size_str" "$age_str" "OK" "$RESET"
    COUNT_OK=$((COUNT_OK + 1))
}

verify_glob() {
    local pattern="$1"
    local max_age_str="$2"
    local min_size_str="$3"

    local found=false
    # Use compgen to safely expand globs
    local files
    files=$(compgen -G "$pattern" 2>/dev/null || true)

    if [[ -z "$files" ]]; then
        COUNT_TOTAL=$((COUNT_TOTAL + 1))
        COUNT_CRITICAL=$((COUNT_CRITICAL + 1))
        printf "  %b%-50s %10s %10s %s%b\n" "$RED" "$pattern" "--" "--" "MISSING" "$RESET"
        return
    fi

    while IFS= read -r file; do
        found=true
        verify_file "$file" "$max_age_str" "$min_size_str"
    done <<< "$files"

    if [[ "$found" == "false" ]]; then
        COUNT_TOTAL=$((COUNT_TOTAL + 1))
        COUNT_CRITICAL=$((COUNT_CRITICAL + 1))
        printf "  %b%-50s %10s %10s %s%b\n" "$RED" "$pattern" "--" "--" "MISSING" "$RESET"
    fi
}

# ══════════════════════════════════════════════════════════════════════
#                       INPUT PARSING
# ══════════════════════════════════════════════════════════════════════

add_entry() {
    local path="$1"
    local max_age="${2:-$BACKUP_MAX_AGE}"
    local min_size="${3:-$BACKUP_MIN_SIZE}"
    ENTRIES+=("${path}|${max_age}|${min_size}")
}

load_config_file() {
    local file="$1"
    if [[ ! -f "$file" ]]; then
        err "Config file not found: $file"
        exit 1
    fi
    while IFS= read -r line; do
        line=$(echo "$line" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
        [[ -z "$line" || "$line" == \#* ]] && continue

        local path max_age
        path=$(echo "$line" | awk '{print $1}')
        max_age=$(echo "$line" | awk '{print $2}')
        if [[ -z "$max_age" ]]; then
            max_age="$BACKUP_MAX_AGE"
        fi
        add_entry "$path" "$max_age"
    done < "$file"
}

load_paths_from_file() {
    local file="$1"
    if [[ ! -f "$file" ]]; then
        err "File not found: $file"
        exit 1
    fi
    while IFS= read -r line; do
        line=$(echo "$line" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
        [[ -z "$line" || "$line" == \#* ]] && continue
        add_entry "$line"
    done < "$file"
}

load_paths_from_stdin() {
    while IFS= read -r line; do
        line=$(echo "$line" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
        [[ -z "$line" || "$line" == \#* ]] && continue
        add_entry "$line"
    done
}

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

usage() {
    cat <<EOF
${SCRIPT_NAME} — Verify backup files exist, are recent, and aren't zero-size

USAGE:
  ${SCRIPT_NAME} [OPTIONS] [PATH/GLOB ...]
  ${SCRIPT_NAME} --config backup-paths.conf
  ${SCRIPT_NAME} --file paths.txt

OPTIONS:
  --config FILE         Config file with paths and optional max-age per line
                        Format: /path/to/backup/*.tar.gz 24h
  --file FILE           Read paths from file (one per line)
  --max-age AGE         Maximum file age (default: 24h)
                        Accepts: h (hours), d (days), w (weeks)
  --min-size SIZE       Minimum expected file size (default: 1)
                        Accepts: K, M, G suffixes
  --verbose             Enable debug output
  --no-color            Disable colored output
  --help                Show this help

ENVIRONMENT VARIABLES:
  BACKUP_MAX_AGE        Maximum file age (default: 24h)
  BACKUP_MIN_SIZE       Minimum file size (default: 1)

CONFIG FILE FORMAT:
  # One path/glob per line, optional max-age
  /var/backups/db/*.sql.gz 12h
  /var/backups/files/*.tar.gz 24h
  /opt/backups/weekly/*.tar 7d

EXAMPLES:
  # Check specific backup files
  ./backup-verify.sh /var/backups/db-latest.sql.gz

  # Check with glob patterns
  ./backup-verify.sh /var/backups/*.tar.gz

  # Use config file with per-path thresholds
  ./backup-verify.sh --config /etc/backup-verify.conf

  # Custom thresholds
  ./backup-verify.sh --max-age 48h --min-size 1M /var/backups/*.tar.gz
EOF
}

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

parse_args() {
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --config)
                CONFIG_FILE="$2"; shift 2 ;;
            --file)
                PATH_FILE="$2"; shift 2 ;;
            --max-age)
                BACKUP_MAX_AGE="$2"; shift 2 ;;
            --min-size)
                BACKUP_MIN_SIZE="$2"; shift 2 ;;
            --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 ;;
            *)
                add_entry "$1"; shift ;;
        esac
    done
}

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

main() {
    parse_args "$@"
    setup_colors

    # Load from config file
    if [[ -n "$CONFIG_FILE" ]]; then
        load_config_file "$CONFIG_FILE"
    fi

    # Load from path file
    if [[ -n "$PATH_FILE" ]]; then
        load_paths_from_file "$PATH_FILE"
    fi

    # Load from stdin if no entries yet and stdin is not a terminal
    if [[ ${#ENTRIES[@]} -eq 0 ]] && ! [[ -t 0 ]]; then
        load_paths_from_stdin
    fi

    if [[ ${#ENTRIES[@]} -eq 0 ]]; then
        err "No backup paths specified"
        echo "Run ${SCRIPT_NAME} --help for usage" >&2
        exit 1
    fi

    echo ""
    echo -e "${BOLD}Backup Verification — $(hostname -f 2>/dev/null || hostname)${RESET}"
    echo -e "${DIM}$(date '+%Y-%m-%d %H:%M:%S %Z')${RESET}"
    echo -e "${DIM}Defaults: max-age=${BACKUP_MAX_AGE}, min-size=${BACKUP_MIN_SIZE}${RESET}"

    section_header "Backup Status"

    printf "  ${BOLD}%-50s %10s %10s %s${RESET}\n" "FILE" "SIZE" "AGE" "STATUS"
    printf "  %s\n" "$(printf '%.0s─' {1..85})"

    for entry in "${ENTRIES[@]}"; do
        local path max_age min_size
        path=$(echo "$entry" | cut -d'|' -f1)
        max_age=$(echo "$entry" | cut -d'|' -f2)
        min_size=$(echo "$entry" | cut -d'|' -f3)

        # Check if path contains glob characters
        if [[ "$path" == *\** || "$path" == *\?* || "$path" == *\[* ]]; then
            verify_glob "$path" "$max_age" "$min_size"
        else
            verify_file "$path" "$max_age" "$min_size"
        fi
    done

    section_header "Summary"
    field "Total checked:" "$COUNT_TOTAL"
    field_color "OK:" "${GREEN}${COUNT_OK}${RESET}"
    if [[ "$COUNT_WARNING" -gt 0 ]]; then
        field_color "Warnings:" "${YELLOW}${COUNT_WARNING}${RESET}"
    else
        field "Warnings:" "$COUNT_WARNING"
    fi
    if [[ "$COUNT_CRITICAL" -gt 0 ]]; then
        field_color "Critical:" "${RED}${COUNT_CRITICAL}${RESET}"
    else
        field "Critical:" "$COUNT_CRITICAL"
    fi

    echo ""

    # Exit with error code if any critical issues
    if [[ "$COUNT_CRITICAL" -gt 0 ]]; then
        return 2
    elif [[ "$COUNT_WARNING" -gt 0 ]]; then
        return 1
    fi
}

main "$@"
