#!/usr/bin/env bash
# Compare NixOS configurations between current state and HEAD
# Shows what would change if you committed the current changes
#
# Requirements: nvd must be in PATH
# Run inside `nix develop` or with direnv enabled

set -euo pipefail

# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
BLUE='\033[0;34m'
YELLOW='\033[0;33m'
NC='\033[0m' # No Color

# Normalize nix store paths by replacing 32-char hashes with placeholder
normalize_nix_paths() {
    sed -E 's|/nix/store/[a-z0-9]{32}-|/nix/store/HASH-|g'
}

# Filter diff output to remove hunks where only nix store hashes differ
# Returns: filtered diff (empty if only hash changes), exit code 0 if real changes found
filter_hash_only_diffs() {
    local diff_output="$1"
    local current_hunk=""
    local output=""
    local has_real_changes=false

    # Process line by line
    while IFS= read -r line || [ -n "$line" ]; do
        if [[ "$line" =~ ^@@ ]]; then
            # New hunk starts - process previous one if it exists
            if [ -n "$current_hunk" ]; then
                if hunk_has_real_changes "$current_hunk"; then
                    output+="$current_hunk"$'\n'
                    has_real_changes=true
                fi
            fi
            # Start new hunk
            current_hunk="$line"$'\n'
        else
            # Add line to current hunk
            current_hunk+="$line"$'\n'
        fi
    done <<< "$diff_output"

    # Process last hunk
    if [ -n "$current_hunk" ]; then
        if hunk_has_real_changes "$current_hunk"; then
            output+="$current_hunk"
            has_real_changes=true
        fi
    fi

    # Remove trailing newline
    output="${output%$'\n'}"

    if [ "$has_real_changes" = true ]; then
        echo "$output"
        return 0
    else
        return 1
    fi
}

# Check if a diff hunk has real changes (not just hash changes)
hunk_has_real_changes() {
    local hunk="$1"

    # Use temp file to avoid bash here-string issues
    local temp_hunk=$(mktemp)
    printf '%s' "$hunk" > "$temp_hunk"

    local minus_lines=()
    local plus_lines=()

    # Extract - and + lines (skip @@ and context lines)
    while IFS= read -r line || [ -n "$line" ]; do
        if [[ "$line" =~ ^- && ! "$line" =~ ^--- ]]; then
            minus_lines+=("${line:1}")  # Remove the - prefix
        elif [[ "$line" =~ ^\+ && ! "$line" =~ ^\+\+\+ ]]; then
            plus_lines+=("${line:1}")  # Remove the + prefix
        fi
    done < "$temp_hunk"

    rm -f "$temp_hunk"

    # If counts don't match, there are structural changes
    if [ ${#minus_lines[@]} -ne ${#plus_lines[@]} ]; then
        return 0  # Has real changes
    fi

    # If no changes at all, skip
    if [ ${#minus_lines[@]} -eq 0 ]; then
        return 1  # No real changes
    fi

    # Compare each pair of lines after normalization
    for i in "${!minus_lines[@]}"; do
        local minus_norm=$(echo "${minus_lines[$i]}" | normalize_nix_paths)
        local plus_norm=$(echo "${plus_lines[$i]}" | normalize_nix_paths)

        if [ "$minus_norm" != "$plus_norm" ]; then
            return 0  # Has real changes
        fi
    done

    # All changes are hash-only
    return 1
}

# Check for nvd
if ! command -v nvd &> /dev/null; then
    echo "Error: nvd not found in PATH"
    echo "Run this script inside 'nix develop' or enable direnv"
    exit 1
fi

# Parse flags
verbose=false
deep=false
hosts_args=()

while [[ $# -gt 0 ]]; do
    case $1 in
        -h|--help)
            echo "Usage: $0 [-v|--verbose] [-d|--deep] [HOST...]"
            echo "Compare NixOS configurations between working tree and HEAD"
            echo ""
            echo "Options:"
            echo "  -v, --verbose   Show detailed list of added/removed store paths"
            echo "  -d, --deep      Show content diffs of changed files (implies -v)"
            echo ""
            echo "Arguments:"
            echo "  HOST            One or more hostnames to compare (default: all)"
            echo ""
            echo "Examples:"
            echo "  $0                      # Compare all hosts (summary)"
            echo "  $0 -v c1                # Compare c1 with path list"
            echo "  $0 --deep c1            # Compare c1 with content diffs"
            echo "  $0 c1 c2 c3            # Compare only c1, c2, c3"
            exit 0
            ;;
        -v|--verbose)
            verbose=true
            shift
            ;;
        -d|--deep)
            deep=true
            verbose=true  # deep implies verbose
            shift
            ;;
        *)
            hosts_args+=("$1")
            shift
            ;;
    esac
done

# Restore positional parameters
set -- "${hosts_args[@]}"

# Check if we're in a git repo
if ! git rev-parse --git-dir > /dev/null 2>&1; then
    echo "Error: Not in a git repository"
    exit 1
fi

# Check if there are any changes
if git diff --quiet && git diff --cached --quiet; then
    echo "No changes detected between working tree and HEAD"
    exit 0
fi

echo "Comparing configurations: current working tree vs HEAD"
echo "======================================================="
echo

# Get list of hosts to compare
if [ $# -gt 0 ]; then
    # Use hosts provided as arguments
    hosts="$@"
    echo -e "${YELLOW}Comparing selected hosts: $hosts${NC}"
else
    # Get all hosts from flake
    echo "Discovering all hosts from flake..."
    hosts=$(nix eval --raw .#deploy.nodes --apply 'nodes: builtins.concatStringsSep "\n" (builtins.attrNames nodes)' 2>/dev/null)

    if [ -z "$hosts" ]; then
        echo "Error: No hosts found in flake"
        exit 1
    fi
fi

echo

# Create temp worktree at HEAD
worktree=$(mktemp -d)
trap "git worktree remove --force '$worktree' &>/dev/null || true; rm -rf '$worktree'" EXIT

echo "Creating temporary worktree at HEAD..."
git worktree add --quiet --detach "$worktree" HEAD

echo "Building and comparing configurations..."
echo

any_changes=false

for host in $hosts; do
    echo -e "${BLUE}━━━ $host ━━━${NC}"

    # Build current (with uncommitted changes)
    echo -n "  Building current... "
    if ! current=$(nix build --no-link --print-out-paths \
        ".#nixosConfigurations.$host.config.system.build.toplevel" 2>/dev/null); then
        echo -e "${RED}FAILED${NC}"
        # Re-run to show error
        nix build --no-link ".#nixosConfigurations.$host.config.system.build.toplevel" 2>&1 | head -20 | sed 's/^/  /'
        continue
    fi
    echo "done"

    # Build HEAD
    echo -n "  Building HEAD...    "
    if ! head=$(nix build --no-link --print-out-paths \
        "$worktree#nixosConfigurations.$host.config.system.build.toplevel" 2>/dev/null); then
        echo -e "${RED}FAILED${NC}"
        # Re-run to show error
        nix build --no-link "$worktree#nixosConfigurations.$host.config.system.build.toplevel" 2>&1 | head -20 | sed 's/^/  /'
        continue
    fi
    echo "done"

    # Compare
    if [ "$head" = "$current" ]; then
        echo -e "  ${GREEN}✓ No changes${NC}"
    else
        any_changes=true
        echo -e "  ${RED}⚠ Configuration changed${NC}"
        echo

        # Show nvd summary
        if ! nvd diff "$head" "$current" 2>&1; then
            echo -e "  ${RED}(nvd diff failed - see error above)${NC}"
        fi

        # Show detailed closure diff if verbose
        if [ "$verbose" = true ]; then
            echo
            echo -e "  ${YELLOW}Changed store paths:${NC}"

            # Get paths unique to HEAD and current
            head_only=$(comm -23 <(nix-store -q --requisites "$head" 2>/dev/null | sort) \
                                  <(nix-store -q --requisites "$current" 2>/dev/null | sort))
            current_only=$(comm -13 <(nix-store -q --requisites "$head" 2>/dev/null | sort) \
                                    <(nix-store -q --requisites "$current" 2>/dev/null | sort))

            # Count changes
            removed_count=$(echo "$head_only" | wc -l)
            added_count=$(echo "$current_only" | wc -l)

            echo -e "    ${RED}Removed ($removed_count paths):${NC}"
            echo "$head_only" | head -10 | sed 's|^/nix/store/[^-]*-|    - |'
            if [ "$removed_count" -gt 10 ]; then
                echo "    ... and $((removed_count - 10)) more"
            fi

            echo
            echo -e "    ${GREEN}Added ($added_count paths):${NC}"
            echo "$current_only" | head -10 | sed 's|^/nix/store/[^-]*-|    - |'
            if [ "$added_count" -gt 10 ]; then
                echo "    ... and $((added_count - 10)) more"
            fi

            # Show content diffs if deep mode
            if [ "$deep" = true ]; then
                echo
                echo -e "  ${YELLOW}Content diffs of changed files:${NC}"

                # Extract basenames for matching
                declare -A head_paths
                while IFS= read -r path; do
                    [ -z "$path" ] && continue
                    basename="${path#/nix/store/[a-z0-9]*-}"
                    head_paths["$basename"]="$path"
                done <<< "$head_only"

                # Find matching pairs and diff them
                matched=false
                while IFS= read -r path; do
                    [ -z "$path" ] && continue
                    basename="${path#/nix/store/[a-z0-9]*-}"

                    # Check if we have a matching path in head
                    if [ -n "${head_paths[$basename]:-}" ]; then
                        old_path="${head_paths[$basename]}"
                        new_path="$path"
                        matched=true

                        echo
                        echo -e "    ${BLUE}▸ $basename${NC}"

                        # If it's a directory, diff all files within it
                        if [ -d "$old_path" ] && [ -d "$new_path" ]; then
                            # Count files to avoid processing huge directories
                            file_count=$(find "$new_path" -maxdepth 3 -type f 2>/dev/null | wc -l)

                            # Skip very large directories (e.g., system-path with 900+ files)
                            if [ "$file_count" -gt 100 ]; then
                                echo "      (skipping directory with $file_count files - too large)"
                            else
                                # Diff all files in the directory
                                for file in $(find "$new_path" -maxdepth 3 -type f 2>/dev/null); do
                                    [ -z "$file" ] && continue
                                    relpath="${file#$new_path/}"
                                    old_file="$old_path/$relpath"

                                    if [ -f "$old_file" ] && [ -f "$file" ]; then
                                        # Check if file is text
                                        if file "$file" | grep -q "text"; then
                                            # Get diff output
                                            diff_output=$(diff -u "$old_file" "$file" 2>/dev/null | head -50 | tail -n +3 || true)

                                            # Filter hash-only changes
                                            if [ -n "$diff_output" ]; then
                                                filtered_diff=$(filter_hash_only_diffs "$diff_output" || true)

                                                if [ -n "$filtered_diff" ]; then
                                                    echo -e "      ${YELLOW}$relpath:${NC}"
                                                    echo "$filtered_diff" | sed 's/^/        /'
                                                fi
                                            fi
                                        fi
                                    fi
                                done
                            fi
                        # If it's a file, diff it directly
                        elif [ -f "$old_path" ] && [ -f "$new_path" ]; then
                            if file "$new_path" | grep -q "text"; then
                                # Get diff output
                                diff_output=$(diff -u "$old_path" "$new_path" 2>/dev/null | head -50 | tail -n +3 || true)

                                # Filter hash-only changes
                                if [ -n "$diff_output" ]; then
                                    filtered_diff=$(filter_hash_only_diffs "$diff_output" || true)

                                    if [ -n "$filtered_diff" ]; then
                                        echo "$filtered_diff" | sed 's/^/      /'
                                    else
                                        echo "      (only hash changes)"
                                    fi
                                fi
                            else
                                echo "      (binary file)"
                            fi
                        fi
                    fi
                done <<< "$current_only"

                if [ "$matched" = false ]; then
                    echo "    (no matching paths found to compare)"
                fi
            fi
        fi
    fi
    echo
done

if [ "$any_changes" = false ]; then
    echo -e "${GREEN}✓ All configurations unchanged${NC}"
else
    echo -e "${RED}⚠ Some configurations changed - review carefully before committing${NC}"
fi
