#!/bin/bash

# gp - Git Push with AI-generated commit messages
# Improved version with better error handling, safety checks, and functionality

set -euo pipefail

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

# Function to print colored output
print_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
print_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
print_warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; }
print_error() { echo -e "${RED}[ERROR]${NC} $1"; }

# Function to show help
show_help() {
    cat << 'EOF'
gp - Git Push with smart commit messages

USAGE:
    gp [options] [message]

OPTIONS:
    -h, --help      Show this help message
    -n, --dry-run   Show what would be committed without actually doing it
    -f, --force     Skip safety checks (use with caution)
    -b, --branch    Specify branch to push to (default: current branch)
    -a, --add-all   Add all files including untracked (default)
    --staged-only   Only commit staged changes (don't add untracked files)

ARGUMENTS:
    message         Optional custom commit message (overrides auto-generation)

EXAMPLES:
    gp                          # Add all files, auto-generate commit message and push
    gp "Fix bug in parser"      # Add all files with custom commit message
    gp --dry-run                # Preview what would be committed
    gp --staged-only            # Only commit staged changes (don't add untracked)
    gp -b develop               # Push to develop branch instead of current

EOF
}

# Function to generate commit message based on changes
generate_commit_message() {
    local files_changed
    files_changed=$(git diff --cached --name-only)
    local files_count
    files_count=$(echo "$files_changed" | wc -l)
    
    if [ -z "$files_changed" ]; then
        files_changed=$(git diff --name-only)
        files_count=$(echo "$files_changed" | wc -l)
    fi
    
    # If add-all is enabled, also include untracked files
    if [ "$ADD_ALL" = true ] && [ -z "$files_changed" ]; then
        files_changed=$(git ls-files --others --exclude-standard)
        files_count=$(echo "$files_changed" | wc -l)
    fi
    
    if [ -z "$files_changed" ]; then
        echo "No changes to commit"
        return 1
    fi
    
    # Generate smart commit message based on file types and changes
    local has_source_files=false
    local has_config_files=false
    local has_docs=false
    local has_tests=false
    local message=""
    
    while IFS= read -r file; do
        [ -z "$file" ] && continue
        
        case "$file" in
            *.cpp|*.hpp|*.c|*.h|*.js|*.ts|*.py|*.go|*.rs|*.java)
                has_source_files=true
                ;;
            *.json|*.yml|*.yaml|*.toml|*.ini|*.conf|CMakeLists.txt|Makefile)
                has_config_files=true
                ;;
            *.md|*.txt|*.rst|docs/*|README*)
                has_docs=true
                ;;
            test/*|tests/*|*test*|*spec*)
                has_tests=true
                ;;
        esac
    done <<< "$files_changed"
    
    # Create descriptive commit message
    if [ "$files_count" -eq 1 ]; then
        local single_file
        single_file=$(echo "$files_changed" | head -1)
        local change_type
        change_type=$(git diff --cached --name-status -- "$single_file" 2>/dev/null || git diff --name-status -- "$single_file")
        case "${change_type:0:1}" in
            A) message="Add $single_file" ;;
            M) message="Update $single_file" ;;
            D) message="Remove $single_file" ;;
            R) message="Rename $single_file" ;;
            *) message="Modify $single_file" ;;
        esac
    else
        local prefix=""
        if $has_tests; then
            prefix="test: "
        elif $has_docs; then
            prefix="docs: "
        elif $has_config_files; then
            prefix="config: "
        elif $has_source_files; then
            prefix="feat: "
        fi
        
        message="${prefix}Update $files_count files"
    fi
    
    echo "$message"
}

# Function to check if we're in a git repository
check_git_repo() {
    if ! git rev-parse --git-dir >/dev/null 2>&1; then
        print_error "Not in a git repository"
        exit 1
    fi
}

# Function to check for uncommitted changes and unpushed commits
check_for_changes() {
    local has_staged=false
    local has_modified=false
    local has_untracked=false
    
    # Check for staged changes
    if ! git diff --cached --quiet; then
        has_staged=true
    fi
    
    # Check for modified files
    if ! git diff --quiet; then
        has_modified=true
    fi
    
    # Check for untracked files
    if [ -n "$(git ls-files --others --exclude-standard)" ]; then
        has_untracked=true
    fi
    
    # Check for unpushed commits
    local current_branch
    current_branch=$(git branch --show-current)
    local unpushed_commits=""
    if git rev-parse --verify "origin/$current_branch" >/dev/null 2>&1; then
        unpushed_commits=$(git rev-list "origin/$current_branch..HEAD" -- 2>/dev/null || true)
    fi
    
    # If add-all is enabled, check if we have any changes at all
    if [ "$ADD_ALL" = true ]; then
        if [ "$has_staged" = false ] && [ "$has_modified" = false ] && [ "$has_untracked" = false ]; then
            # No working tree changes, but check for unpushed commits
            if [ -n "$unpushed_commits" ]; then
                local commit_count
                commit_count=$(echo "$unpushed_commits" | wc -l)
                print_info "No working tree changes, but found $commit_count unpushed commit(s)"
                print_info "Latest unpushed commit: $(git log --oneline -1)"
                
                # Set a flag to indicate we should only push, not commit
                PUSH_ONLY=true
                return 0
            else
                print_info "No changes to commit (working tree clean)"
                exit 0
            fi
        fi
    else
        # If add-all is disabled, only check staged changes
        if [ "$has_staged" = false ]; then
            # No staged changes, but check for unpushed commits
            if [ -n "$unpushed_commits" ]; then
                local commit_count
                commit_count=$(echo "$unpushed_commits" | wc -l)
                print_info "No staged changes, but found $commit_count unpushed commit(s)"
                print_info "Latest unpushed commit: $(git log --oneline -1)"
                
                if [ "$has_modified" = true ] || [ "$has_untracked" = true ]; then
                    print_warning "You have unstaged changes that won't be included"
                    print_info "Use 'gp -a' to include all changes, or stage them first with 'git add'"
                fi
                
                # Set a flag to indicate we should only push, not commit
                PUSH_ONLY=true
                return 0
            else
                if [ "$has_modified" = true ] || [ "$has_untracked" = true ]; then
                    print_warning "No staged changes found"
                    print_info "Use 'gp -a' to add all files, or stage changes first with 'git add'"
                else
                    print_info "No changes to commit (working tree clean)"
                fi
                exit 0
            fi
        fi
    fi
}

# Function to show what would be committed and get user confirmation
show_status_and_confirm() {
    print_info "Current branch: $(git branch --show-current)"
    print_info "Repository: $(git remote get-url origin 2>/dev/null || echo 'No remote')"
    echo
    
    local has_staged_changes=false
    local has_unstaged_changes=false
    local has_untracked_files=false
    
    # Show staged changes
    if ! git diff --cached --quiet; then
        print_info "Staged changes:"
        git diff --cached --name-only -- | while IFS= read -r line; do echo "  $line"; done
        has_staged_changes=true
    fi
    
    # Show unstaged changes
    if ! git diff --quiet; then
        if [ "$ADD_ALL" = true ]; then
            print_info "Modified files (will be added):"
        else
            print_info "Modified files (unstaged, will NOT be included):"
        fi
        git diff --name-only -- | while IFS= read -r line; do echo "  $line"; done
        has_unstaged_changes=true
    fi
    
    # Show untracked files
    if [ "$ADD_ALL" = true ]; then
        local untracked
        untracked=$(git ls-files --others --exclude-standard)
        if [ -n "$untracked" ]; then
            print_info "Untracked files (will be added):"
            # Use while loop to add indent without sed
            while IFS= read -r line; do
                echo "  $line"
            done <<< "$untracked"
            has_untracked_files=true
        fi
    fi
    
    # Show summary of what will happen
    echo
    if [ "$ADD_ALL" = true ]; then
        if [ "$has_unstaged_changes" = true ] || [ "$has_untracked_files" = true ]; then
            print_warning "Files will be automatically added before committing"
        fi
        if [ "$has_staged_changes" = false ] && [ "$has_unstaged_changes" = false ] && [ "$has_untracked_files" = false ]; then
            print_info "No changes found to commit"
        fi
    else
        if [ "$has_unstaged_changes" = true ]; then
            print_warning "Unstaged changes will NOT be included (use -a to include them)"
        fi
        if [ "$has_staged_changes" = false ]; then
            print_info "No staged changes to commit"
        fi
    fi
    
    # Confirmation for file additions (unless forced or dry-run)
    if [ "$ADD_ALL" = true ] && [ "$FORCE" = false ] && [ "$DRY_RUN" = false ]; then
        if [ "$has_unstaged_changes" = true ] || [ "$has_untracked_files" = true ]; then
            echo
            read -p "Add these files and continue? [y/N] " -n 1 -r
            echo
            if [[ ! $REPLY =~ ^[Yy]$ ]]; then
                print_info "Aborted by user"
                exit 0
            fi
        fi
    fi
}

# Function to perform the actual commit and push
do_commit_and_push() {
    local commit_msg="$1"
    local target_branch="$2"
    
    # Add files if requested
    if [ "$ADD_ALL" = true ]; then
        print_info "Adding all files..."
        git add -A
    fi
    
    # Commit
    print_info "Committing with message: '$commit_msg'"
    git commit -m "$commit_msg" --
    
    # Push
    print_info "Pushing to $target_branch..."
    # Check if branch exists on remote to determine if we need -u flag
    if git ls-remote --exit-code --heads origin "$target_branch" >/dev/null 2>&1; then
        # Branch exists on remote, normal push
        if git push origin "$target_branch"; then
            print_success "Successfully pushed to origin/$target_branch"
        else
            print_error "Failed to push to origin/$target_branch"
            print_info "You may need to pull first: git pull origin $target_branch"
            exit 1
        fi
    else
        # Branch doesn't exist on remote, use -u to set upstream
        print_info "Setting upstream branch (first push to origin/$target_branch)"
        if git push -u origin "$target_branch"; then
            print_success "Successfully pushed and set upstream to origin/$target_branch"
        else
            print_error "Failed to push to origin/$target_branch"
            exit 1
        fi
    fi
}

# Autocomplete function for bash
autocomplete() {
    local args=("$@")
    if [ ${#args[@]} -eq 0 ]; then
        printf "%s\n" "--help" "--dry-run" "--force" "--add-all" "--staged-only" "--branch" "-h" "-n" "-f" "-a" "-b"
    fi
}

# Default values
DRY_RUN=false
FORCE=false
ADD_ALL=true  # Default to adding all files
CUSTOM_MESSAGE=""
TARGET_BRANCH=""
PUSH_ONLY=false

# Handle special commands first
case "${1:-}" in
    autocomplete)
        shift
        autocomplete "$@"
        exit 0
        ;;
    version)
        echo "gp version 2.0.0"
        exit 0
        ;;
esac

# Parse command line arguments
while [[ $# -gt 0 ]]; do
    case $1 in
        -h|--help)
            show_help
            exit 0
            ;;
        -n|--dry-run)
            DRY_RUN=true
            shift
            ;;
        -f|--force)
            FORCE=true
            shift
            ;;
        -a|--add-all)
            ADD_ALL=true  # Explicitly set (though it's default)
            shift
            ;;
        --staged-only)
            ADD_ALL=false
            shift
            ;;
        -b|--branch)
            if [ -z "${2:-}" ]; then
                print_error "Option -b/--branch requires a branch name"
                exit 1
            fi
            TARGET_BRANCH="$2"
            shift 2
            ;;
        -*)
            print_error "Unknown option: $1"
            echo "Use 'gp --help' for usage information"
            exit 1
            ;;
        *)
            CUSTOM_MESSAGE="$1"
            shift
            ;;
    esac
done

# Main execution
main() {
    # Safety checks
    check_git_repo
    
    # Set target branch if not specified
    if [ -z "$TARGET_BRANCH" ]; then
        TARGET_BRANCH=$(git branch --show-current)
    fi
    
    # Check for changes
    check_for_changes
    
    # Handle push-only case (unpushed commits but no working tree changes)
    if [ "$PUSH_ONLY" = true ]; then
        echo
        
        # Dry run mode for push-only
        if [ "$DRY_RUN" = true ]; then
            print_warning "DRY RUN MODE - No changes will be made"
            print_info "Would push existing commits to: origin/$TARGET_BRANCH"
            exit 0
        fi
        
        # Safety confirmation for push-only (unless forced)
        if [ "$FORCE" = false ]; then
            echo
            read -p "Push existing commits to origin/$TARGET_BRANCH? [y/N] " -n 1 -r
            echo
            if [[ ! $REPLY =~ ^[Yy]$ ]]; then
                print_info "Aborted by user"
                exit 0
            fi
        fi
        
        # Execute push only
        print_info "Pushing existing commits to $TARGET_BRANCH..."
        # Check if branch exists on remote to determine if we need -u flag
        if git ls-remote --exit-code --heads origin "$TARGET_BRANCH" >/dev/null 2>&1; then
            # Branch exists on remote, normal push
            if git push origin "$TARGET_BRANCH"; then
                print_success "Successfully pushed existing commits to origin/$TARGET_BRANCH"
            else
                print_error "Failed to push to origin/$TARGET_BRANCH"
                print_info "You may need to pull first: git pull origin $TARGET_BRANCH"
                exit 1
            fi
        else
            # Branch doesn't exist on remote, use -u to set upstream
            print_info "Setting upstream branch (first push to origin/$TARGET_BRANCH)"
            if git push -u origin "$TARGET_BRANCH"; then
                print_success "Successfully pushed existing commits and set upstream to origin/$TARGET_BRANCH"
            else
                print_error "Failed to push to origin/$TARGET_BRANCH"
                exit 1
            fi
        fi
        exit 0
    fi
    
    # Show current status and get confirmation for file additions
    show_status_and_confirm
    
    # Generate or use custom commit message
    if [ -n "$CUSTOM_MESSAGE" ]; then
        commit_message="$CUSTOM_MESSAGE"
        print_info "Using custom commit message: '$commit_message'"
    else
        print_info "Generating commit message..."
        if ! commit_message=$(generate_commit_message); then
            print_error "Failed to generate commit message"
            exit 1
        fi
        print_info "Generated commit message: '$commit_message'"
    fi
    
    # Dry run mode
    if [ "$DRY_RUN" = true ]; then
        print_warning "DRY RUN MODE - No changes will be made"
        print_info "Would commit with message: '$commit_message'"
        print_info "Would push to: origin/$TARGET_BRANCH"
        exit 0
    fi
    
    # Safety confirmation (unless forced)
    if [ "$FORCE" = false ]; then
        echo
        read -p "Proceed with commit and push? [y/N] " -n 1 -r
        echo
        if [[ ! $REPLY =~ ^[Yy]$ ]]; then
            print_info "Aborted by user"
            exit 0
        fi
    fi
    
    # Execute the commit and push
    do_commit_and_push "$commit_message" "$TARGET_BRANCH"
}

# Run main function
main "$@"