#!/bin/bash
# =============================================================
# Gate Cleanliness Check
# =============================================================
# Purpose: Deterministic working tree cleanliness verification for gate/retest workflows.
#
# This script distinguishes expected generated files from unexpected code changes
# using a git baseline diff approach.
#
# Usage:
#   ./scripts/gate-cleanliness-check.sh [OPTIONS]
#
# Options:
#   --baseline <dir>     Directory to store baseline file (default: .anima/gate)
#   --expected <file>    File listing expected generated files (default: .anima/gate/expected-generated.txt)
#   --save-baseline      Save current git status as baseline (run before tests)
#   --check              Check against baseline (run after tests)
#   --check-clean        Simple cleanliness check without baseline (just report)
#   --format <fmt>       Output format: text, json (default: text)
#   --strict             Fail on any dirty files (no exceptions)
#
# Exit Codes:
#   0 - Clean (no unexpected changes)
#   1 - Dirty (unexpected changes found)
#   2 - Missing baseline (run --save-baseline first)
#   3 - Invalid arguments
#
# Examples:
#   # Before running tests:
#   ./scripts/gate-cleanliness-check.sh --save-baseline
#
#   # After running tests:
#   ./scripts/gate-cleanliness-check.sh --check
#
#   # Simple clean check (no baseline):
#   ./scripts/gate-cleanliness-check.sh --check-clean
#
# =============================================================

set -euo pipefail

# Default configuration
BASELINE_DIR="${BASELINE_DIR:-.anima/gate}"
EXPECTED_FILE="${EXPECTED_FILE:-${BASELINE_DIR}/expected-generated.txt}"
OUTPUT_FORMAT="text"
MODE=""
STRICT=false

# Expected generated files (defaults if file doesn't exist)
DEFAULT_EXPECTED=(
    "plan.yaml"
    "plan-dashboard.html"
    ".anima/gate/baseline-status.txt"
)

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

usage() {
    cat <<EOF
Usage: $(basename "$0") [OPTIONS]

Options:
  --baseline <dir>     Directory to store baseline file (default: .anima/gate)
  --expected <file>    File listing expected generated files (default: .anima/gate/expected-generated.txt)
  --save-baseline      Save current git status as baseline (run before tests)
  --check              Check against baseline (run after tests)
  --check-clean        Simple cleanliness check without baseline (just report)
  --format <fmt>       Output format: text, json (default: text)
  --strict             Fail on any dirty files (no exceptions)
  -h, --help           Show this help

Environment Variables:
  BASELINE_DIR         Override baseline directory
  EXPECTED_FILE        Override expected files list

Exit Codes:
  0 - Clean (no unexpected changes)
  1 - Dirty (unexpected changes found)
  2 - Missing baseline (run --save-baseline first)
  3 - Invalid arguments

Examples:
  # Save baseline before tests:
  $(basename "$0") --save-baseline

  # Check for unexpected changes after tests:
  $(basename "$0") --check

  # Simple clean check without baseline:
  $(basename "$0") --check-clean

  # Check with JSON output (for tooling):
  $(basename "$0") --check --format json
EOF
}

log_info() {
    if [[ "$OUTPUT_FORMAT" == "text" ]]; then
        echo -e "${GREEN}[INFO]${NC} $1"
    fi
}

log_warn() {
    if [[ "$OUTPUT_FORMAT" == "text" ]]; then
        echo -e "${YELLOW}[WARN]${NC} $1"
    fi
}

log_error() {
    if [[ "$OUTPUT_FORMAT" == "text" ]]; then
        echo -e "${RED}[ERROR]${NC} $1" >&2
    fi
}

# Get current git status in a normalized format
get_git_status() {
    git status --porcelain 2>/dev/null || echo ""
}

# Load expected generated files list
load_expected_files() {
    local files=()
    
    # Load from file if it exists
    if [[ -f "$EXPECTED_FILE" ]]; then
        while IFS= read -r line || [[ -n "$line" ]]; do
            # Skip empty lines and comments
            line="${line%%#*}"  # Remove comments
            line="${line%%[[:space:]]}"  # Trim trailing whitespace
            [[ -z "$line" ]] && continue
            files+=("$line")
        done < "$EXPECTED_FILE"
    fi
    
    # Add defaults if file didn't exist or was empty
    if [[ ${#files[@]} -eq 0 ]]; then
        files=("${DEFAULT_EXPECTED[@]}")
    fi
    
    printf '%s\n' "${files[@]}"
}

# Get list of dirty files that are NOT in expected list
get_unexpected_changes() {
    local status
    status=$(get_git_status)
    
    if [[ -z "$status" ]]; then
        return 0
    fi
    
    local expected_files
    expected_files=$(load_expected_files)
    
    # Parse git status output
    while IFS= read -r line; do
        [[ -z "$line" ]] && continue
        
        # Extract file path (after status codes)
        # Format: XY PATH or XY PATH -> PATH2
        local file
        file=$(echo "$line" | awk '{print $NF}')
        
        # Check if this file is in expected list
        local is_expected=false
        while IFS= read -r expected; do
            if [[ "$file" == "$expected" ]]; then
                is_expected=true
                break
            fi
        done <<< "$expected_files"
        
        if [[ "$is_expected" == "false" ]]; then
            echo "$line"
        fi
    done <<< "$status"
}

# Save baseline
save_baseline() {
    log_info "Saving baseline git status to ${BASELINE_DIR}/baseline-status.txt"
    mkdir -p "$BASELINE_DIR"
    get_git_status > "${BASELINE_DIR}/baseline-status.txt"
    
    # Also save the list of expected files if it doesn't exist
    if [[ ! -f "$EXPECTED_FILE" ]]; then
        log_info "Creating expected files list at $EXPECTED_FILE"
        cat > "$EXPECTED_FILE" <<'EOF'
# Expected Generated Files for Gate Cleanliness Check
# One file pattern per line. Lines starting with # are comments.
# These files are expected to be modified during normal operations
# and will NOT cause gate failures.

# vectl plan artifacts
plan.yaml
plan-dashboard.html

# Gate baseline directory (self-referential)
.anima/gate/baseline-status.txt
EOF
        log_info "Edit $EXPECTED_FILE to customize expected files"
    fi
    
    log_info "Baseline saved successfully"
    exit 0
}

# Check against baseline
check_baseline() {
    local baseline_file="${BASELINE_DIR}/baseline-status.txt"
    
    if [[ ! -f "$baseline_file" ]]; then
        log_error "Baseline not found at $baseline_file"
        log_error "Run: $0 --save-baseline first"
        exit 2
    fi
    
    local current_status
    current_status=$(get_git_status)
    
    local baseline_status
    baseline_status=$(cat "$baseline_file")
    
    # Parse into sets of files
    local current_files=()
    local baseline_files=()
    
    while IFS= read -r line; do
        [[ -z "$line" ]] && continue
        current_files+=("$(echo "$line" | awk '{print $NF}')")
    done <<< "$current_status"
    
    while IFS= read -r line; do
        [[ -z "$line" ]] && continue
        baseline_files+=("$(echo "$line" | awk '{print $NF}')")
    done <<< "$baseline_status"
    
    # Find new changes (in current but not in baseline)
    local unexpected=()
    for current_file in "${current_files[@]}"; do
        local found=false
        for baseline_file_entry in "${baseline_files[@]}"; do
            if [[ "$current_file" == "$baseline_file_entry" ]]; then
                found=true
                break
            fi
        done
        
        if [[ "$found" == "false" ]]; then
            # Check if it's in expected list
            local expected_files
            expected_files=$(load_expected_files)
            local is_expected=false
            
            while IFS= read -r expected; do
                if [[ "$current_file" == "$expected" ]]; then
                    is_expected=true
                    break
                fi
            done <<< "$expected_files"
            
            if [[ "$is_expected" == "false" && "$STRICT" == "true" ]]; then
                unexpected+=("$current_file")
            elif [[ "$is_expected" == "false" ]]; then
                # In non-strict mode, only report if not expected
                unexpected+=("$current_file")
            fi
        fi
    done
    
    # Output results
    if [[ "$OUTPUT_FORMAT" == "json" ]]; then
        echo "{"
        echo "  \"status\": \"$([[ ${#unexpected[@]} -eq 0 ]] && echo "clean" || echo "dirty")\","
        echo "  \"unexpected_changes\": ["
        for i in "${!unexpected[@]}"; do
            echo "    \"${unexpected[$i]}\"$([[ $i -lt $((${#unexpected[@]} - 1)) ]] && echo ",")"
        done
        echo "  ],"
        echo "  \"expected_allowed\": $(load_expected_files | wc -l | tr -d ' ')"
        echo "}"
    else
        if [[ ${#unexpected[@]} -eq 0 ]]; then
            log_info "Gate cleanliness check: CLEAN"
            log_info "No unexpected changes detected"
            exit 0
        else
            log_error "Gate cleanliness check: DIRTY"
            log_error "Unexpected changes found:"
            for file in "${unexpected[@]}"; do
                log_error "  - $file"
            done
            log_info ""
            log_info "If these are expected generated files, add them to $EXPECTED_FILE"
            exit 1
        fi
    fi
    
    [[ ${#unexpected[@]} -eq 0 ]] && exit 0 || exit 1
}

# Simple cleanliness check without baseline
check_clean() {
    local unexpected
    unexpected=$(get_unexpected_changes)
    
    if [[ "$OUTPUT_FORMAT" == "json" ]]; then
        local clean=true
        [[ -n "$unexpected" ]] && clean=false
        
        echo "{"
        echo "  \"status\": \"$($clean && echo "clean" || echo "dirty")\","
        echo "  \"unexpected_changes\": ["
        if [[ -n "$unexpected" ]]; then
            local first=true
            while IFS= read -r line; do
                local file
                file=$(echo "$line" | awk '{print $NF}')
                [[ "$first" == "true" ]] && first=false || echo ","
                echo -n "    \"$file\""
            done <<< "$unexpected"
            echo ""
        fi
        echo "  ],"
        echo "  \"expected_allowed\": $(load_expected_files | wc -l | tr -d ' ')"
        echo "}"
    else
        if [[ -z "$unexpected" ]]; then
            log_info "Working tree: CLEAN"
            log_info "All dirty files are in expected-generated list"
            exit 0
        else
            log_error "Working tree: DIRTY"
            log_error "Unexpected changes found:"
            while IFS= read -r line; do
                log_error "  $line"
            done <<< "$unexpected"
            log_info ""
            log_info "Expected generated files (allowed):"
            while IFS= read -r expected; do
                log_info "  $expected"
            done <<< "$(load_expected_files)"
            log_info ""
            log_info "To allow these files, add them to $EXPECTED_FILE"
            exit 1
        fi
    fi
    
    [[ -z "$unexpected" ]] && exit 0 || exit 1
}

# Parse arguments
while [[ $# -gt 0 ]]; do
    case "$1" in
        --baseline)
            BASELINE_DIR="$2"
            EXPECTED_FILE="${BASELINE_DIR}/expected-generated.txt"
            shift 2
            ;;
        --expected)
            EXPECTED_FILE="$2"
            shift 2
            ;;
        --save-baseline)
            MODE="save"
            shift
            ;;
        --check)
            MODE="check"
            shift
            ;;
        --check-clean)
            MODE="check-clean"
            shift
            ;;
        --format)
            OUTPUT_FORMAT="$2"
            shift 2
            ;;
        --strict)
            STRICT=true
            shift
            ;;
        -h|--help)
            usage
            exit 0
            ;;
        *)
            log_error "Unknown option: $1"
            usage
            exit 3
            ;;
    esac
done

# Validate mode
if [[ -z "$MODE" ]]; then
    log_error "No mode specified. Use --save-baseline, --check, or --check-clean"
    usage
    exit 3
fi

# Execute mode
case "$MODE" in
    save)
        save_baseline
        ;;
    check)
        check_baseline
        ;;
    check-clean)
        check_clean
        ;;
esac