#!/bin/bash

################################################
#### Salt Key Manager                       ####
#### Automate salt-key operations           ####
####                                        ####
#### Author: Phil Connor                    ####
#### Contact: contact@mylinux.work          ####
#### Version: 1.00-030526                   ####
################################################

set -o pipefail

SCRIPT_NAME=$(basename "$0")
readonly SCRIPT_NAME

# Default configuration
readonly DEFAULT_STALE_DAYS=30
readonly DEFAULT_CACHE_DIR="/var/cache/salt/master/minions"

# Configuration variables
DEBUG=${DEBUG:-}

# Runtime flags
ACTION=""
TARGET_MINION=""
STALE_DAYS=$DEFAULT_STALE_DAYS
EXPORT_PATH=""
BULK_FILE=""
AUTO_YES=false
USE_COLOR=true

# Colors
C_GREEN=""
C_YELLOW=""
C_RED=""
C_CYAN=""
C_RESET=""

handle_error() {
    local exit_code=$1
    local line_number=$2
    echo "Error: $SCRIPT_NAME failed at line $line_number with exit code $exit_code" >&2
    exit "$exit_code"
}

trap 'handle_error $? $LINENO' ERR

debug_echo() {
    if [[ -n "$DEBUG" ]]; then
        echo "[DEBUG] $*" >&2
    fi
}

log_info() {
    echo "[INFO] $*"
}

log_warn() {
    echo "[WARN] $*" >&2
}

log_error() {
    echo "[ERROR] $*" >&2
}

setup_colors() {
    if [[ "$USE_COLOR" == true ]] && [[ -t 1 ]]; then
        C_GREEN='\033[0;32m'
        C_YELLOW='\033[0;33m'
        C_RED='\033[0;31m'
        C_CYAN='\033[0;36m'
        C_RESET='\033[0m'
    fi
}

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

Manage Salt minion keys — accept, reject, delete, verify, rotate, and
clean up stale keys.

ACTIONS:
    --list                  List all keys by status with counts
    --verify                Show pending keys with fingerprints for verification
    --accept-all            Accept all pending keys
    --accept MINION         Accept a specific minion key
    --reject MINION         Reject a specific minion key
    --delete MINION         Delete a specific minion key
    --rotate MINION         Rotate a minion key (delete, re-accept on reconnect)
    --cleanup-stale [DAYS]  Delete keys for minions not seen in DAYS days (default: $DEFAULT_STALE_DAYS)
    --export PATH           Export all accepted key fingerprints to a file
    --bulk-accept FILE      Accept minions listed in a file (one per line)

OPTIONS:
    --yes                   Skip confirmation prompts
    --no-color              Disable colored output
    --help, -h              Show this help message

ENVIRONMENT VARIABLES:
    DEBUG                   Enable debug output

EXAMPLES:
    # List all keys with status
    sudo $SCRIPT_NAME --list

    # Show pending keys for verification
    sudo $SCRIPT_NAME --verify

    # Accept all pending keys
    sudo $SCRIPT_NAME --accept-all --yes

    # Accept a specific minion
    sudo $SCRIPT_NAME --accept web01

    # Clean up minions not seen in 60 days
    sudo $SCRIPT_NAME --cleanup-stale 60

    # Export fingerprints for auditing
    sudo $SCRIPT_NAME --export /tmp/salt-keys.txt

    # Bulk accept from a file
    sudo $SCRIPT_NAME --bulk-accept /tmp/new-minions.txt --yes
EOF
}

count_keys() {
    local status="$1"
    salt-key --list "$status" 2>/dev/null | grep -cv "^$status\|^$" || echo 0
}

do_list() {
    echo "Salt Key Status"
    echo "==============="
    echo ""

    local accepted unaccepted denied rejected
    accepted=$(count_keys "accepted")
    unaccepted=$(count_keys "unaccepted")
    denied=$(count_keys "denied")
    rejected=$(count_keys "rejected")

    printf '  %bAccepted:%b   %d\n' "$C_GREEN" "$C_RESET" "$accepted"
    printf '  %bPending:%b    %d\n' "$C_YELLOW" "$C_RESET" "$unaccepted"
    printf '  %bDenied:%b     %d\n' "$C_RED" "$C_RESET" "$denied"
    printf '  %bRejected:%b   %d\n' "$C_RED" "$C_RESET" "$rejected"
    echo ""

    if ((accepted > 0)); then
        printf '%bAccepted Keys:%b\n' "$C_GREEN" "$C_RESET"
        salt-key --list accepted 2>/dev/null | grep -v "^Accepted Keys:" | sed 's/^/  /'
        echo ""
    fi

    if ((unaccepted > 0)); then
        printf '%bPending Keys:%b\n' "$C_YELLOW" "$C_RESET"
        salt-key --list unaccepted 2>/dev/null | grep -v "^Unaccepted Keys:" | sed 's/^/  /'
        echo ""
    fi

    if ((denied > 0)); then
        printf '%bDenied Keys:%b\n' "$C_RED" "$C_RESET"
        salt-key --list denied 2>/dev/null | grep -v "^Denied Keys:" | sed 's/^/  /'
        echo ""
    fi

    if ((rejected > 0)); then
        printf '%bRejected Keys:%b\n' "$C_RED" "$C_RESET"
        salt-key --list rejected 2>/dev/null | grep -v "^Rejected Keys:" | sed 's/^/  /'
        echo ""
    fi
}

do_verify() {
    local pending
    pending=$(salt-key --list unaccepted 2>/dev/null | grep -v "^Unaccepted Keys:$" | grep -v "^$")

    if [[ -z "$pending" ]]; then
        log_info "No pending keys to verify"
        return 0
    fi

    echo "Master Fingerprint:"
    printf '  %b' "$C_CYAN"
    salt-key -F master 2>/dev/null | grep -A1 "master.pub" | tail -1 | tr -d ' '
    printf '%b\n\n' "$C_RESET"

    echo "Pending Keys with Fingerprints:"
    echo ""

    while IFS= read -r minion; do
        [[ -z "$minion" ]] && continue
        minion=$(echo "$minion" | tr -d '[:space:]')
        local fingerprint
        fingerprint=$(salt-key -f "$minion" 2>/dev/null | grep -v "^Unaccepted Keys:" | awk '{print $2}' | head -1)
        printf '  %b%-30s%b %s\n' "$C_YELLOW" "$minion" "$C_RESET" "${fingerprint:-unknown}"
    done <<< "$pending"

    echo ""
    log_info "Verify each fingerprint matches the minion's local fingerprint:"
    log_info "  (on minion) salt-call --local key.finger"
}

do_accept_all() {
    local pending
    pending=$(count_keys "unaccepted")

    if ((pending == 0)); then
        log_info "No pending keys to accept"
        return 0
    fi

    log_info "Accepting $pending pending key(s)..."

    if [[ "$AUTO_YES" != true ]]; then
        echo "Accept all $pending pending keys? [y/N] "
        read -r confirm
        if [[ "$confirm" != "y" && "$confirm" != "Y" ]]; then
            log_info "Aborted"
            return 0
        fi
    fi

    salt-key -A -y 2>/dev/null
    log_info "All pending keys accepted"
}

do_accept() {
    local minion="$1"
    log_info "Accepting key for: $minion"

    if [[ "$AUTO_YES" != true ]]; then
        local fingerprint
        fingerprint=$(salt-key -f "$minion" 2>/dev/null | grep -v "^Unaccepted Keys:" | awk '{print $2}' | head -1)
        echo "Fingerprint: ${fingerprint:-unknown}"
        echo "Accept key for $minion? [y/N] "
        read -r confirm
        if [[ "$confirm" != "y" && "$confirm" != "Y" ]]; then
            log_info "Aborted"
            return 0
        fi
    fi

    salt-key -a "$minion" -y 2>/dev/null
    log_info "Key accepted for $minion"
}

do_reject() {
    local minion="$1"
    log_info "Rejecting key for: $minion"

    if [[ "$AUTO_YES" != true ]]; then
        echo "Reject key for $minion? [y/N] "
        read -r confirm
        if [[ "$confirm" != "y" && "$confirm" != "Y" ]]; then
            log_info "Aborted"
            return 0
        fi
    fi

    salt-key -r "$minion" -y 2>/dev/null
    log_info "Key rejected for $minion"
}

do_delete() {
    local minion="$1"
    log_info "Deleting key for: $minion"

    if [[ "$AUTO_YES" != true ]]; then
        echo "Delete key for $minion? This cannot be undone. [y/N] "
        read -r confirm
        if [[ "$confirm" != "y" && "$confirm" != "Y" ]]; then
            log_info "Aborted"
            return 0
        fi
    fi

    salt-key -d "$minion" -y 2>/dev/null
    log_info "Key deleted for $minion"
}

do_rotate() {
    local minion="$1"
    log_info "Rotating key for: $minion"
    log_info "This will delete the current key — the minion must reconnect to get a new key accepted"

    if [[ "$AUTO_YES" != true ]]; then
        echo "Rotate key for $minion? [y/N] "
        read -r confirm
        if [[ "$confirm" != "y" && "$confirm" != "Y" ]]; then
            log_info "Aborted"
            return 0
        fi
    fi

    salt-key -d "$minion" -y 2>/dev/null
    log_info "Key deleted for $minion — accept the new key when the minion reconnects"
    log_info "On the minion, restart salt-minion: systemctl restart salt-minion"
}

do_cleanup_stale() {
    local days="$1"
    log_info "Finding minions not seen in $days days..."

    if [[ ! -d "$DEFAULT_CACHE_DIR" ]]; then
        log_error "Minion cache directory not found: $DEFAULT_CACHE_DIR"
        return 1
    fi

    local stale_minions=()
    local cutoff
    cutoff=$(date -d "-${days} days" +%s 2>/dev/null) || cutoff=$(date -v-"${days}"d +%s 2>/dev/null)

    while IFS= read -r minion_dir; do
        local minion_name
        minion_name=$(basename "$minion_dir")
        local last_modified
        last_modified=$(stat -c %Y "$minion_dir" 2>/dev/null) || last_modified=$(stat -f %m "$minion_dir" 2>/dev/null) || continue

        if ((last_modified < cutoff)); then
            local days_ago=$(( ($(date +%s) - last_modified) / 86400 ))
            stale_minions+=("$minion_name")
            printf '  %b%-30s%b (last seen %d days ago)\n' "$C_RED" "$minion_name" "$C_RESET" "$days_ago"
        fi
    done < <(find "$DEFAULT_CACHE_DIR" -maxdepth 1 -mindepth 1 -type d 2>/dev/null)

    if [[ ${#stale_minions[@]} -eq 0 ]]; then
        log_info "No stale minions found"
        return 0
    fi

    echo ""
    log_info "Found ${#stale_minions[@]} stale minion(s)"

    if [[ "$AUTO_YES" != true ]]; then
        echo "Delete keys for all ${#stale_minions[@]} stale minions? [y/N] "
        read -r confirm
        if [[ "$confirm" != "y" && "$confirm" != "Y" ]]; then
            log_info "Aborted"
            return 0
        fi
    fi

    for minion in "${stale_minions[@]}"; do
        salt-key -d "$minion" -y 2>/dev/null && log_info "Deleted key: $minion"
    done

    log_info "Stale key cleanup complete"
}

do_export() {
    local output_path="$1"
    log_info "Exporting accepted key fingerprints to $output_path..."

    {
        echo "# Salt Key Fingerprint Export"
        echo "# Generated: $(date -u '+%Y-%m-%d %H:%M:%S UTC')"
        echo "# Master: $(hostname -f 2>/dev/null || hostname)"
        echo "#"
        echo "# Format: minion_id fingerprint"
        echo ""
        salt-key -F accepted 2>/dev/null | grep -v "^Accepted Keys:" | while IFS= read -r line; do
            [[ -z "$line" ]] && continue
            echo "$line"
        done
    } > "$output_path"

    local count
    count=$(grep -cv "^#\|^$" "$output_path" 2>/dev/null) || count=0
    log_info "Exported $count key fingerprint(s) to $output_path"
}

do_bulk_accept() {
    local input_file="$1"

    if [[ ! -f "$input_file" ]]; then
        log_error "File not found: $input_file"
        return 1
    fi

    local count=0
    local failed=0

    while IFS= read -r line; do
        [[ -z "$line" || "$line" == \#* ]] && continue
        local minion_id="${line%%:*}"
        minion_id=$(echo "$minion_id" | tr -d '[:space:]')

        if salt-key -a "$minion_id" -y 2>/dev/null; then
            log_info "Accepted: $minion_id"
            count=$((count + 1))
        else
            log_error "Failed to accept: $minion_id"
            failed=$((failed + 1))
        fi
    done < "$input_file"

    log_info "Bulk accept complete: $count accepted, $failed failed"
}

parse_arguments() {
    while [[ $# -gt 0 ]]; do
        case $1 in
            --list)
                ACTION="list"
                shift
                ;;
            --verify)
                ACTION="verify"
                shift
                ;;
            --accept-all)
                ACTION="accept-all"
                shift
                ;;
            --accept)
                ACTION="accept"
                TARGET_MINION="$2"
                shift 2
                ;;
            --reject)
                ACTION="reject"
                TARGET_MINION="$2"
                shift 2
                ;;
            --delete)
                ACTION="delete"
                TARGET_MINION="$2"
                shift 2
                ;;
            --rotate)
                ACTION="rotate"
                TARGET_MINION="$2"
                shift 2
                ;;
            --cleanup-stale)
                ACTION="cleanup-stale"
                if [[ -n "${2:-}" && "$2" =~ ^[0-9]+$ ]]; then
                    STALE_DAYS="$2"
                    shift 2
                else
                    shift
                fi
                ;;
            --export)
                ACTION="export"
                EXPORT_PATH="$2"
                shift 2
                ;;
            --bulk-accept)
                ACTION="bulk-accept"
                BULK_FILE="$2"
                shift 2
                ;;
            --yes)
                AUTO_YES=true
                shift
                ;;
            --no-color)
                USE_COLOR=false
                shift
                ;;
            --help|-h)
                show_help
                exit 0
                ;;
            *)
                log_error "Unknown option: $1"
                show_help >&2
                exit 1
                ;;
        esac
    done
}

validate_requirements() {
    if [[ $EUID -ne 0 ]]; then
        log_error "This script must be run as root (use sudo)"
        exit 1
    fi

    if [[ -z "$ACTION" ]]; then
        log_error "An action is required"
        show_help >&2
        exit 1
    fi

    if ! command -v salt-key >/dev/null 2>&1; then
        log_error "salt-key not found — is salt-master installed?"
        exit 1
    fi

    if [[ "$ACTION" == "accept" || "$ACTION" == "reject" || "$ACTION" == "delete" || "$ACTION" == "rotate" ]]; then
        if [[ -z "$TARGET_MINION" ]]; then
            log_error "Minion name is required for --$ACTION"
            exit 1
        fi
    fi

    if [[ "$ACTION" == "export" && -z "$EXPORT_PATH" ]]; then
        log_error "Output path is required for --export"
        exit 1
    fi

    if [[ "$ACTION" == "bulk-accept" && -z "$BULK_FILE" ]]; then
        log_error "Input file is required for --bulk-accept"
        exit 1
    fi
}

main() {
    parse_arguments "$@"
    validate_requirements
    setup_colors

    case "$ACTION" in
        list)           do_list ;;
        verify)         do_verify ;;
        accept-all)     do_accept_all ;;
        accept)         do_accept "$TARGET_MINION" ;;
        reject)         do_reject "$TARGET_MINION" ;;
        delete)         do_delete "$TARGET_MINION" ;;
        rotate)         do_rotate "$TARGET_MINION" ;;
        cleanup-stale)  do_cleanup_stale "$STALE_DAYS" ;;
        export)         do_export "$EXPORT_PATH" ;;
        bulk-accept)    do_bulk_accept "$BULK_FILE" ;;
    esac

    debug_echo "Script completed successfully"
}

if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
    main "$@"
fi
