#!/usr/bin/env bash

#########################################################################################
#### changelog-diff.sh — Compare installed package versions between snapshots        ####
#### Supports before/after upgrade diffs, remote host comparison via SSH             ####
#### Detects additions, removals, upgrades, and downgrades across dpkg and rpm       ####
####                                                                                 ####
#### Author: Phil Connor                                                             ####
#### Contact: contact@mylinux.work                                                   ####
#### License: MIT                                                                    ####
#### Version 1.00                                                                    ####
####                                                                                 ####
#### Usage:                                                                          ####
####   ./changelog-diff.sh --snapshot                                                ####
####   ./changelog-diff.sh --diff snap1.txt snap2.txt                                ####
####   ./changelog-diff.sh --remote user@host                                        ####
####                                                                                 ####
#### See --help for all options.                                                     ####
#########################################################################################

set -euo pipefail

# ── Defaults ──────────────────────────────────────────────────────────
VERBOSE="${VERBOSE:-false}"
COLOR="${COLOR:-auto}"
SNAPSHOT_DIR="${SNAPSHOT_DIR:-/var/backups/pkg-snapshots}"
MODE=""
DIFF_FILE_A=""
DIFF_FILE_B=""
REMOTE_HOST=""

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

# ── 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"
}

detect_package_manager() {
    if command -v dpkg-query &>/dev/null; then
        echo "dpkg"
    elif command -v rpm &>/dev/null; then
        echo "rpm"
    else
        echo "unknown"
    fi
}

get_package_list() {
    local pm
    pm=$(detect_package_manager)
    case "$pm" in
        dpkg)
            dpkg-query -W -f='${Package} ${Version}\n' 2>/dev/null | sort
            ;;
        rpm)
            rpm -qa --queryformat '%{NAME} %{VERSION}-%{RELEASE}\n' 2>/dev/null | sort
            ;;
        *)
            err "No supported package manager found (need dpkg or rpm)"
            exit 1
            ;;
    esac
}

# ══════════════════════════════════════════════════════════════════════
#                       SNAPSHOT
# ══════════════════════════════════════════════════════════════════════

do_snapshot() {
    if [[ ! -d "$SNAPSHOT_DIR" ]]; then
        mkdir -p "$SNAPSHOT_DIR" 2>/dev/null || {
            err "Cannot create snapshot directory: ${SNAPSHOT_DIR}"
            exit 1
        }
    fi

    local timestamp
    timestamp=$(date +%Y%m%d-%H%M%S)
    local host
    host=$(hostname -s 2>/dev/null || hostname)
    local pm
    pm=$(detect_package_manager)
    local snapshot_file="${SNAPSHOT_DIR}/${host}-${pm}-${timestamp}.txt"

    verbose "Package manager: ${pm}"
    verbose "Snapshot file: ${snapshot_file}"

    get_package_list > "$snapshot_file"

    local pkg_count
    pkg_count=$(wc -l < "$snapshot_file")

    log "Snapshot saved: ${snapshot_file}"
    field "Packages:" "$pkg_count"
    field "Package manager:" "$pm"
    field "File:" "$snapshot_file"
}

# ══════════════════════════════════════════════════════════════════════
#                       DIFF
# ══════════════════════════════════════════════════════════════════════

do_diff() {
    local file_a="$1"
    local file_b="$2"

    if [[ ! -f "$file_a" ]]; then
        err "File not found: ${file_a}"
        exit 1
    fi
    if [[ ! -f "$file_b" ]]; then
        err "File not found: ${file_b}"
        exit 1
    fi

    echo ""
    echo -e "${BOLD}Package Diff${RESET}"
    field "Before:" "$file_a"
    field "After:" "$file_b"

    local added=0 removed=0 upgraded=0 downgraded=0

    # Build associative arrays
    local tmp_added tmp_removed tmp_changed
    tmp_added=$(mktemp)
    tmp_removed=$(mktemp)
    tmp_changed=$(mktemp)
    trap 'rm -f "$tmp_added" "$tmp_removed" "$tmp_changed"' EXIT

    # Find added packages (in B but not A)
    while IFS=' ' read -r pkg ver; do
        if ! grep -q "^${pkg} " "$file_a"; then
            echo "${pkg} ${ver}" >> "$tmp_added"
        fi
    done < "$file_b"

    # Find removed packages (in A but not B)
    while IFS=' ' read -r pkg ver; do
        if ! grep -q "^${pkg} " "$file_b"; then
            echo "${pkg} ${ver}" >> "$tmp_removed"
        fi
    done < "$file_a"

    # Find changed packages
    while IFS=' ' read -r pkg ver_b; do
        local ver_a
        ver_a=$(grep "^${pkg} " "$file_a" 2>/dev/null | head -1 | cut -d' ' -f2-)
        if [[ -n "$ver_a" && "$ver_a" != "$ver_b" ]]; then
            echo "${pkg} ${ver_a} ${ver_b}" >> "$tmp_changed"
        fi
    done < "$file_b"

    # Display additions
    if [[ -s "$tmp_added" ]]; then
        section_header "Added Packages"
        while IFS=' ' read -r pkg ver; do
            printf "  ${CYAN}+${RESET} %-40s %s\n" "$pkg" "$ver"
            added=$((added + 1))
        done < "$tmp_added"
    fi

    # Display removals
    if [[ -s "$tmp_removed" ]]; then
        section_header "Removed Packages"
        while IFS=' ' read -r pkg ver; do
            printf "  ${RED}-${RESET} %-40s %s\n" "$pkg" "$ver"
            removed=$((removed + 1))
        done < "$tmp_removed"
    fi

    # Display upgrades and downgrades
    if [[ -s "$tmp_changed" ]]; then
        local has_upgrades=false
        local has_downgrades=false

        # First pass: categorize
        while IFS=' ' read -r pkg ver_a ver_b; do
            if dpkg --compare-versions "$ver_b" gt "$ver_a" 2>/dev/null; then
                has_upgrades=true
            elif dpkg --compare-versions "$ver_b" lt "$ver_a" 2>/dev/null; then
                has_downgrades=true
            else
                # Fallback: string comparison
                if [[ "$ver_b" > "$ver_a" ]]; then
                    has_upgrades=true
                else
                    has_downgrades=true
                fi
            fi
        done < "$tmp_changed"

        if [[ "$has_upgrades" == "true" ]]; then
            section_header "Upgraded Packages"
            while IFS=' ' read -r pkg ver_a ver_b; do
                local is_upgrade=false
                if dpkg --compare-versions "$ver_b" gt "$ver_a" 2>/dev/null; then
                    is_upgrade=true
                elif ! dpkg --compare-versions "$ver_b" lt "$ver_a" 2>/dev/null && [[ "$ver_b" > "$ver_a" ]]; then
                    is_upgrade=true
                fi
                if [[ "$is_upgrade" == "true" ]]; then
                    printf "  ${GREEN}↑${RESET} %-35s %s → %s\n" "$pkg" "$ver_a" "$ver_b"
                    upgraded=$((upgraded + 1))
                fi
            done < "$tmp_changed"
        fi

        if [[ "$has_downgrades" == "true" ]]; then
            section_header "Downgraded Packages"
            while IFS=' ' read -r pkg ver_a ver_b; do
                local is_downgrade=false
                if dpkg --compare-versions "$ver_b" lt "$ver_a" 2>/dev/null; then
                    is_downgrade=true
                elif ! dpkg --compare-versions "$ver_b" gt "$ver_a" 2>/dev/null && [[ "$ver_b" < "$ver_a" ]]; then
                    is_downgrade=true
                fi
                if [[ "$is_downgrade" == "true" ]]; then
                    printf "  ${YELLOW}↓${RESET} %-35s %s → %s\n" "$pkg" "$ver_a" "$ver_b"
                    downgraded=$((downgraded + 1))
                fi
            done < "$tmp_changed"
        fi
    fi

    # Summary
    local total=$((added + removed + upgraded + downgraded))

    echo ""
    echo -e "  ${BOLD}══════════════════════════════════════════${RESET}"
    echo -e "  ${BOLD}Change Summary${RESET}"
    echo -e "  ${BOLD}══════════════════════════════════════════${RESET}"
    field "Total changes:" "$total"
    if [[ "$added" -gt 0 ]]; then
        printf "  ${BOLD}%-22s${RESET} ${CYAN}%s${RESET}\n" "Additions:" "$added"
    else
        field "Additions:" "0"
    fi
    if [[ "$removed" -gt 0 ]]; then
        printf "  ${BOLD}%-22s${RESET} ${RED}%s${RESET}\n" "Removals:" "$removed"
    else
        field "Removals:" "0"
    fi
    if [[ "$upgraded" -gt 0 ]]; then
        printf "  ${BOLD}%-22s${RESET} ${GREEN}%s${RESET}\n" "Upgrades:" "$upgraded"
    else
        field "Upgrades:" "0"
    fi
    if [[ "$downgraded" -gt 0 ]]; then
        printf "  ${BOLD}%-22s${RESET} ${YELLOW}%s${RESET}\n" "Downgrades:" "$downgraded"
    else
        field "Downgrades:" "0"
    fi
    echo ""
}

# ══════════════════════════════════════════════════════════════════════
#                       REMOTE COMPARE
# ══════════════════════════════════════════════════════════════════════

do_remote() {
    local remote="$1"

    if ! command -v ssh &>/dev/null; then
        err "ssh is required for remote comparison"
        exit 1
    fi

    log "Fetching local package list..."
    local local_file
    local_file=$(mktemp)

    log "Fetching remote package list from ${remote}..."
    local remote_file
    remote_file=$(mktemp)
    trap 'rm -f "$local_file" "$remote_file"' EXIT

    get_package_list > "$local_file"

    local pm
    pm=$(detect_package_manager)
    case "$pm" in
        dpkg)
            ssh "$remote" "dpkg-query -W -f='\${Package} \${Version}\n' 2>/dev/null | sort" > "$remote_file" || {
                err "Failed to fetch package list from ${remote}"
                exit 1
            }
            ;;
        rpm)
            ssh "$remote" "rpm -qa --queryformat '%{NAME} %{VERSION}-%{RELEASE}\n' 2>/dev/null | sort" > "$remote_file" || {
                err "Failed to fetch package list from ${remote}"
                exit 1
            }
            ;;
    esac

    log "Comparing local vs ${remote}..."
    do_diff "$local_file" "$remote_file"
}

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

usage() {
    cat <<EOF
${SCRIPT_NAME} — Compare installed package versions between snapshots

USAGE:
  ${SCRIPT_NAME} --snapshot
  ${SCRIPT_NAME} --diff <before.txt> <after.txt>
  ${SCRIPT_NAME} --remote <user@host>

MODES:
  --snapshot             Save current package list to a timestamped file
  --diff FILE1 FILE2     Compare two snapshot files
  --remote HOST          Compare local packages with a remote host via SSH

OPTIONS:
  --snapshot-dir DIR     Directory for snapshots (default: ${SNAPSHOT_DIR})
  --verbose              Enable debug output
  --no-color             Disable colored output
  --help                 Show this help

ENVIRONMENT VARIABLES:
  SNAPSHOT_DIR           Snapshot directory (default: /var/backups/pkg-snapshots)
  COLOR                  Color mode: auto, always, never (default: auto)

EXAMPLES:
  # Take a snapshot before upgrade
  ./changelog-diff.sh --snapshot

  # Upgrade packages, take another snapshot, then diff
  ./changelog-diff.sh --snapshot
  sudo apt upgrade -y
  ./changelog-diff.sh --snapshot
  ./changelog-diff.sh --diff /var/backups/pkg-snapshots/host-*.txt

  # Compare with a remote server
  ./changelog-diff.sh --remote admin@prod-server

  # Custom snapshot directory
  ./changelog-diff.sh --snapshot --snapshot-dir /tmp/snapshots
EOF
}

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

parse_args() {
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --snapshot)
                MODE="snapshot"; shift ;;
            --diff)
                MODE="diff"
                if [[ $# -lt 3 ]]; then
                    err "--diff requires two file arguments"
                    exit 1
                fi
                DIFF_FILE_A="$2"
                DIFF_FILE_B="$3"
                shift 3 ;;
            --remote)
                MODE="remote"
                if [[ $# -lt 2 ]]; then
                    err "--remote requires a host argument"
                    exit 1
                fi
                REMOTE_HOST="$2"
                shift 2 ;;
            --snapshot-dir)
                SNAPSHOT_DIR="$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 ;;
        esac
    done

    if [[ -z "$MODE" ]]; then
        err "No mode specified. Use --snapshot, --diff, or --remote"
        echo "Run ${SCRIPT_NAME} --help for usage" >&2
        exit 1
    fi
}

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

main() {
    parse_args "$@"
    setup_colors

    echo ""
    echo -e "${BOLD}Package Changelog Diff — $(hostname -f 2>/dev/null || hostname)${RESET}"
    echo -e "${DIM}$(date '+%Y-%m-%d %H:%M:%S %Z')${RESET}"

    case "$MODE" in
        snapshot)
            do_snapshot
            ;;
        diff)
            do_diff "$DIFF_FILE_A" "$DIFF_FILE_B"
            ;;
        remote)
            do_remote "$REMOTE_HOST"
            ;;
    esac
}

main "$@"
