#!/bin/bash
# CoderFlow Base Image Entrypoint
# Handles task execution with logging and exit code management

set -o pipefail  # Catch errors in pipes
# Note: We don't use 'set -e' because we want to capture exit codes

# Task metadata
TASK_ID="${TASK_ID:-$(date +%s)-$$}"
TASK_TYPE="${TASK_TYPE:-manual}"
ENVIRONMENT="${ENVIRONMENT:-base}"
TASK_OUTPUT_DIR="${TASK_OUTPUT_DIR:-/task-output}"
START_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
LOG_FILE="$TASK_OUTPUT_DIR/log.txt"

# Ensure output directory and log file exist
mkdir -p "$TASK_OUTPUT_DIR"
touch "$LOG_FILE"

# Decode parameters and env vars provided by server
PARAMETERS_JSON='{}'
if [ -n "$TASK_PARAMETERS_B64" ]; then
    decoded_parameters=$(printf '%s' "$TASK_PARAMETERS_B64" | base64 --decode 2>/dev/null)
    if [ $? -eq 0 ] && [ -n "$decoded_parameters" ]; then
        PARAMETERS_JSON="$decoded_parameters"
    fi
fi

ENV_VARS_JSON='{}'
if [ -n "$TASK_ENV_VARS_B64" ]; then
    decoded_env=$(printf '%s' "$TASK_ENV_VARS_B64" | base64 --decode 2>/dev/null)
    if [ $? -eq 0 ] && [ -n "$decoded_env" ]; then
        ENV_VARS_JSON="$decoded_env"
    fi
fi

# Logging functions
log() {
    local message="[$(date +'%Y-%m-%d %H:%M:%S')] $*"
    echo "$message"
    if [ -n "$LOG_FILE" ]; then
        printf '%s\n' "$message" >> "$LOG_FILE"
    fi
}

log_error() {
    log "ERROR: $*"
}

log_success() {
    log "SUCCESS: $*"
}

write_task_status() {
    local phase="$1"
    local progress="$2"
    local stage="${3:-}"
    local error_message="${4:-}"

    if [ -n "$stage" ] || [ -n "$error_message" ]; then
        jq -n \
            --argjson progress "$progress" \
            --arg phase "$phase" \
            --arg updated_at "$(date -Iseconds)" \
            --arg stage "$stage" \
            --arg error "$error_message" \
            '{
                progress: $progress,
                phase: $phase,
                updated_at: $updated_at
            }
            + (if $stage != "" then {stage: $stage} else {} end)
            + (if $error != "" then {error: $error} else {} end)' > "$TASK_OUTPUT_DIR/.status"
    else
        jq -n \
            --argjson progress "$progress" \
            --arg phase "$phase" \
            --arg updated_at "$(date -Iseconds)" \
            '{
                progress: $progress,
                phase: $phase,
                updated_at: $updated_at
            }' > "$TASK_OUTPUT_DIR/.status"
    fi
}

# Enhanced timing function with millisecond precision (only when DEBUG_TIMING is enabled)
log_timing() {
    if [ "${DEBUG_TIMING:-false}" = "true" ]; then
        local message="[$(date +'%Y-%m-%d %H:%M:%S.%3N')] [TIMING] $*"
        echo "$message"
        if [ -n "$LOG_FILE" ]; then
            printf '%s\n' "$message" >> "$LOG_FILE"
        fi
    fi
}

# Mark timing checkpoint
TIMING_START=$(date +%s%3N)
timing_checkpoint() {
    if [ "${DEBUG_TIMING:-false}" = "true" ]; then
        local label="$1"
        local now=$(date +%s%3N)
        local elapsed=$((now - TIMING_START))
        log_timing "$label (elapsed: ${elapsed}ms since start)"
    fi
}

# Setup git credentials early if available (needed for git operations)
# Git credential.helper store is pre-configured in the Dockerfile.
# Server passes credentials via CODER_GIT_CREDS environment variable (JSON array format).
setup_git_credentials() {
    # Check for git credentials passed via environment variable (JSON array)
    if [ -n "$CODER_GIT_CREDS" ]; then
        # Parse JSON array and write each credential line to file
        echo "$CODER_GIT_CREDS" | jq -r '.[]' > /home/coder/.git-credentials 2>/dev/null
        if [ $? -eq 0 ]; then
            chown coder:coder /home/coder/.git-credentials
            chmod 600 /home/coder/.git-credentials
            log "Static Git credentials configured"
        else
            log "Failed to parse static Git credentials from environment variable"
        fi
    fi
}

# Setup git user identity if provided via environment variables
# CODER_USER_NAME and CODER_USER_EMAIL are passed from the server based on the authenticated user
setup_git_user_identity() {
    if [ -n "$CODER_USER_NAME" ] || [ -n "$CODER_USER_EMAIL" ]; then
        if [ -n "$CODER_USER_NAME" ]; then
            git config --file /home/coder/.gitconfig user.name "$CODER_USER_NAME"
        fi
        if [ -n "$CODER_USER_EMAIL" ]; then
            git config --file /home/coder/.gitconfig user.email "$CODER_USER_EMAIL"
        fi
        chown coder:coder /home/coder/.gitconfig
        chmod 644 /home/coder/.gitconfig
    fi
}

# Setup cache directory for dynamic git credential helper
# This helper fetches fresh tokens from the server on-demand, solving the
# GitHub App token 1-hour expiration issue for long-running containers.
# Cache is stored at ~/.cache/coder-git-credentials/ with 700 permissions.
setup_credential_helper_cache() {
    local cache_dir="/home/coder/.cache/coder-git-credentials"

    # Create cache directory with secure permissions
    mkdir -p "$cache_dir"
    chmod 700 "$cache_dir"
    chown coder:coder "$cache_dir"

    # Also ensure parent .cache directory has correct ownership
    chown coder:coder /home/coder/.cache 2>/dev/null || true
}

# Helper: when an <REPO>_SHA env var is provided by the SCM polling service,
# resolve the polled commit and check it out. Caller must already be in the
# repo dir. Returns 0 on success (or no-op when sha is empty), non-zero on
# checkout failure. The SHA must be pre-validated; this helper does not
# re-validate.
checkout_pinned_sha() {
    local sha="$1"
    [ -z "$sha" ] && return 0
    if ! git cat-file -e "${sha}^{commit}" 2>/dev/null; then
        # Deepen the shallow clone so the SHA is reachable. --unshallow as a
        # last resort if the targeted fetch can't find it.
        git fetch --depth=200 origin "$sha" 2>&1 || git fetch --unshallow origin 2>&1 || true
    fi
    git checkout "$sha" 2>&1
    return $?
}

# Update or clone repositories from configuration
update_repositories() {
    timing_checkpoint "update_repositories: START"
    local workspace_dir="${WORKSPACE_DIR:-/workspace}"

    # Create workspace directory
    mkdir -p "$workspace_dir"

    # Check if REPOS_CONFIG is set
    if [ -z "$REPOS_CONFIG" ]; then
        log "No REPOS_CONFIG provided, skipping repository update" >&2
        timing_checkpoint "update_repositories: END (no config)"
        return 0
    fi

    # Configure Git to use SSH without strict host key checking
    export GIT_SSH_COMMAND="ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"

    log "Updating repositories in $workspace_dir..." >&2
    timing_checkpoint "update_repositories: Config loaded"

    # Parse JSON array using jq
    # Expected format: [{"name":"repo1","url":"https://...","branch":"main"},...]
    local repo_count=$(echo "$REPOS_CONFIG" | jq 'length' 2>/dev/null)

    if [ -z "$repo_count" ] || [ "$repo_count" -eq 0 ]; then
        log "No repositories configured" >&2
        return 0
    fi

    log "Found $repo_count repositories" >&2

    # Feature flag for parallel updates (can be disabled via env var if issues arise)
    # Skip parallel mode if only 1 repo
    local parallel_updates="${PARALLEL_REPO_UPDATES:-true}"
    if [ "$repo_count" -eq 1 ]; then
        parallel_updates="false"
    fi

    if [ "$parallel_updates" = "true" ]; then
        log "Using parallel repository updates" >&2
        timing_checkpoint "update_repositories: Starting parallel updates"
    else
        log "Using sequential repository updates" >&2
    fi

    # Array to store background job PIDs
    declare -a update_pids=()
    local failed_count=0

    # Process each repository using jq
    for index in $(seq 0 $((repo_count - 1))); do
        # Extract repository details using jq, supporting both object and string entries
        local repo_json=$(echo "$REPOS_CONFIG" | jq -c ".[$index]" 2>/dev/null)

        if [ -z "$repo_json" ] || [ "$repo_json" = "null" ]; then
            log_error "Invalid repository configuration at index $index" >&2
            failed_count=$((failed_count + 1))
            continue
        fi

        local repo_name=$(echo "$repo_json" | jq -r 'if type == "object" then (.name // empty) else . end' 2>/dev/null)
        local repo_url=$(echo "$repo_json" | jq -r 'if type == "object" then (.url // empty) else empty end' 2>/dev/null)
        local repo_branch=$(echo "$repo_json" | jq -r 'if type == "object" then (.branch // "main") else "main" end' 2>/dev/null)
        local repo_path_from_json=$(echo "$repo_json" | jq -r 'if type == "object" then (.path // empty) else empty end' 2>/dev/null)

        if [ -z "$repo_name" ] || [ "$repo_name" = "null" ]; then
            log_error "Invalid repository configuration at index $index" >&2
            failed_count=$((failed_count + 1))
            continue
        fi

        if [ -z "$repo_url" ] || [ "$repo_url" = "null" ]; then
            log "Skipping automated update for $repo_name (no repository URL provided)" >&2
            continue
        fi

        # Use path field from JSON if available, otherwise default to repo_name
        local repo_path
        if [ -n "$repo_path_from_json" ] && [ "$repo_path_from_json" != "null" ]; then
            repo_path="$workspace_dir/$repo_path_from_json"
        else
            repo_path="$workspace_dir/$repo_name"
        fi

        # Define update function (will be run either inline or in background)
        update_single_repo() {
            local repo_name="$1"
            local repo_url="$2"
            local repo_branch="$3"
            local repo_path="$4"

            # Check for repo-specific branch override (e.g., PROFOUNDJS_BRANCH).
            # Use ${!var} indirection (safe) rather than eval.
            local repo_env_prefix=$(echo "$repo_name" | tr '[:lower:]' '[:upper:]' | tr '-' '_')
            local env_var_name="${repo_env_prefix}_BRANCH"
            local branch_override="${!env_var_name:-}"

            # Strip origin/ prefix if present (handles case where UI sends full remote ref)
            if [[ "$branch_override" == origin/* ]]; then
                branch_override="${branch_override#origin/}"
                log "Stripped origin/ prefix from branch override: $branch_override" >&2
            fi

            # Determine final branch: repo-specific override > CODER_BRANCH > CHECKOUT_BRANCH > config branch
            local branch="${branch_override:-${CODER_BRANCH:-${CHECKOUT_BRANCH:-$repo_branch}}}"

            # SHA pin (set by SCM polling, mirrors <REPO>_BRANCH naming).
            # When present, the clone resolves the branch as usual but then
            # checks out this exact commit so the task runs against the SHA
            # the trigger observed, even if the branch has advanced since.
            # The SHA must be a full-length hex string (40 chars for SHA-1,
            # 64 for SHA-256); reject anything else so a malformed env var
            # cannot smuggle shell metacharacters into the git invocation
            # below or silently downgrade to "branch HEAD" — that would
            # re-open the post-poll race the SHA pin exists to close.
            local sha_env_var_name="${repo_env_prefix}_SHA"
            local sha_override="${!sha_env_var_name:-}"
            if [ -n "$sha_override" ] && ! [[ "$sha_override" =~ ^([0-9a-fA-F]{40}|[0-9a-fA-F]{64})$ ]]; then
                log_error "Refusing to update $repo_name: ${sha_env_var_name} is set but is not a valid Git SHA ('${sha_override}'). Aborting to avoid silently running against branch HEAD instead of the polled commit." >&2
                return 1
            fi

            # Check if repo already exists (cloned during image build)
            if [ -d "$repo_path/.git" ]; then
                timing_checkpoint "update_repositories: Updating $repo_name START"
                log "Updating $repo_name..." >&2

                # Log branch override if used
                if [ -n "$branch_override" ]; then
                    log "Using branch override $env_var_name=$branch (config: $repo_branch)" >&2
                elif [ "$branch" != "$repo_branch" ]; then
                    log "Using branch override: $branch (config: $repo_branch)" >&2
                fi

                # Run git operations as coder user - output to both stdout and log file
                log "Running git operations for $repo_name..."

                # Check if we're running as root or coder user
                if [ "$(id -u)" -eq 0 ]; then
                    # Running as root, use runuser to switch to coder. Pass
                    # repo_path/branch/sha as positional args ($1/$2/$3) so
                    # the embedded script never interpolates them — keeps the
                    # subprocess robust against unusual but valid characters
                    # in repo names or branches.
                    GIT_SSH_COMMAND="$GIT_SSH_COMMAND" runuser -u coder -- bash -c "
                        $(declare -f checkout_pinned_sha)
"'
                        cd "$1" || exit 1

                        # Temporarily disable post-checkout hook during startup
                        if [ -f ".git/hooks/post-checkout" ] && [ -x ".git/hooks/post-checkout" ]; then
                            mv ".git/hooks/post-checkout" ".git/hooks/post-checkout.disabled" 2>/dev/null || true
                        fi

                        # Optimize git performance for startup operations
                        git config --local fetch.prune true 2>/dev/null || true
                        git config --local diff.autoRefreshIndex false 2>/dev/null || true

                        # Fetch latest refs from remote (required for new branches not known locally)
                        git fetch --depth=50 origin "$2" 2>&1 || true

                        # Checkout branch
                        git checkout "$2" 2>&1 || exit $?

                        # Reset package-lock.json to avoid merge conflicts from npm version differences
                        git checkout -- package-lock.json "**/package-lock.json" 2>/dev/null || true

                        if [ -n "$3" ]; then
                            # SHA pin: skip the pull (branch HEAD may have
                            # advanced past the polled SHA) and use the
                            # helper to deepen the fetch and check out the
                            # exact commit.
                            checkout_pinned_sha "$3" || exit $?
                            pull_exit=0
                        else
                            git pull --depth=50 --no-stat 2>&1
                            pull_exit=$?
                        fi

                        if [ -f ".git/hooks/post-checkout.disabled" ]; then
                            mv ".git/hooks/post-checkout.disabled" ".git/hooks/post-checkout" 2>/dev/null || true
                        fi

                        exit $pull_exit
                    ' bash "$repo_path" "$branch" "$sha_override" 2>&1 | while IFS= read -r line; do
                        # Filter out routine messages, keep important ones
                        case "$line" in
                            *"Already on"*|*"Your branch is up to date"*|*"Already up to date"*|*"Warning: Permanently added"*)
                                # Skip routine messages
                                ;;
                            *)
                                # Show important messages (errors, updates, etc.)
                                echo "   $line"
                                [ -n "$LOG_FILE" ] && echo "   $line" >> "$LOG_FILE"
                                ;;
                        esac
                    done
                    local update_exit=${PIPESTATUS[0]}
                else
                    # Already running as coder, execute directly
                    (
                        cd "$repo_path" || exit 1

                        # Temporarily disable post-checkout hook during startup
                        if [ -f '.git/hooks/post-checkout' ] && [ -x '.git/hooks/post-checkout' ]; then
                            mv '.git/hooks/post-checkout' '.git/hooks/post-checkout.disabled' 2>/dev/null || true
                        fi

                        # Optimize git performance for startup operations
                        git config --local fetch.prune true 2>/dev/null || true
                        git config --local diff.autoRefreshIndex false 2>/dev/null || true

                        # Fetch latest refs from remote (required for new branches not known locally)
                        git fetch --depth=50 origin "$branch" 2>&1 || true

                        # Checkout branch
                        git checkout "$branch" 2>&1 || exit $?

                        # Reset package-lock.json to avoid merge conflicts from npm version differences
                        git checkout -- package-lock.json '**/package-lock.json' 2>/dev/null || true

                        if [ -n "$sha_override" ]; then
                            checkout_pinned_sha "$sha_override" || exit $?
                            pull_exit=0
                        else
                            git pull --depth=50 --no-stat 2>&1
                            pull_exit=$?
                        fi

                        # Re-enable post-checkout hook
                        if [ -f '.git/hooks/post-checkout.disabled' ]; then
                            mv '.git/hooks/post-checkout.disabled' '.git/hooks/post-checkout' 2>/dev/null || true
                        fi

                        exit $pull_exit
                    ) 2>&1 | while IFS= read -r line; do
                        # Filter out routine messages, keep important ones
                        case "$line" in
                            *"Already on"*|*"Your branch is up to date"*|*"Already up to date"*|*"Warning: Permanently added"*)
                                # Skip routine messages
                                ;;
                            *)
                                # Show important messages (errors, updates, etc.)
                                echo "   $line"
                                [ -n "$LOG_FILE" ] && echo "   $line" >> "$LOG_FILE"
                                ;;
                        esac
                    done
                    local update_exit=${PIPESTATUS[0]}
                fi

                if [ $update_exit -eq 0 ]; then
                    timing_checkpoint "update_repositories: $repo_name git pull SUCCESS"
                else
                    timing_checkpoint "update_repositories: $repo_name git pull FAILED"
                fi

                timing_checkpoint "update_repositories: Updating $repo_name END"
                return $update_exit
            else
                # Fallback: clone if not present (shouldn't happen in proper setup)
                log "Cloning $repo_name from $repo_url (branch: $branch)..." >&2

                # Ensure parent directory exists
                mkdir -p "$(dirname "$repo_path")"

                # Run clone as coder user with environment preserved
                if [ "$(id -u)" -eq 0 ]; then
                    # Running as root, use runuser. Pass branch/url/path/sha
                    # as positional args; the SHA helper is inlined.
                    GIT_SSH_COMMAND="$GIT_SSH_COMMAND" runuser -u coder -- bash -c "
                        $(declare -f checkout_pinned_sha)
"'
                        branch="$1"
                        url="$2"
                        target="$3"
                        sha="$4"
                        repo_label="$5"
                        if git clone --depth=50 --no-single-branch --quiet --branch "$branch" "$url" "$target" 2>&1; then
                            if [ -n "$sha" ]; then
                                cd "$target" || exit 1
                                checkout_pinned_sha "$sha" || exit $?
                            fi
                            echo "Successfully cloned $repo_label" >&2
                            exit 0
                        else
                            echo "ERROR: Failed to clone $repo_label from $url" >&2
                            exit 1
                        fi
                    ' bash "$branch" "$repo_url" "$repo_path" "$sha_override" "$repo_name"
                else
                    # Already running as coder
                    if git clone --depth=50 --no-single-branch --quiet --branch "$branch" "$repo_url" "$repo_path" 2>&1; then
                        if [ -n "$sha_override" ]; then
                            (
                                cd "$repo_path" || exit 1
                                checkout_pinned_sha "$sha_override"
                            ) || return $?
                        fi
                        echo "Successfully cloned $repo_name" >&2
                        exit 0
                    else
                        echo "ERROR: Failed to clone $repo_name from $repo_url" >&2
                        exit 1
                    fi
                fi
                return $?
            fi
        }

        # Run update either in background or inline
        if [ "$parallel_updates" = "true" ]; then
            # Launch in background and track PID
            update_single_repo "$repo_name" "$repo_url" "$repo_branch" "$repo_path" &
            update_pids+=($!)
        else
            # Run immediately (sequential)
            if ! update_single_repo "$repo_name" "$repo_url" "$repo_branch" "$repo_path"; then
                failed_count=$((failed_count + 1))
            fi
        fi
    done

    # Wait for all background jobs if using parallel mode
    if [ "$parallel_updates" = "true" ]; then
        timing_checkpoint "update_repositories: Waiting for parallel updates"
        log "Waiting for ${#update_pids[@]} repository updates to complete..." >&2

        for pid in "${update_pids[@]}"; do
            if ! wait $pid; then
                failed_count=$((failed_count + 1))
            fi
        done

        if [ $failed_count -gt 0 ]; then
            log_error "$failed_count repository update(s) failed" >&2
        fi
    fi

    if [ $failed_count -gt 0 ]; then
        timing_checkpoint "update_repositories: END (failed)"
        return 1
    fi

    log "Repository update complete" >&2
    timing_checkpoint "update_repositories: END (all repos complete)"
    return 0
}

# Execute environment setup script if provided
run_setup_script() {
    if [ -z "$SETUP_SCRIPT" ]; then
        log "No setup script specified" >&2
        return 0
    fi

    if [ ! -f "$SETUP_SCRIPT" ]; then
        log_error "Setup script not found: $SETUP_SCRIPT" >&2
        return 1
    fi

    log "Executing setup script: $SETUP_SCRIPT" >&2

    # Run setup script as coder user with .bashrc sourced
    if [ "$(id -u)" -eq 0 ]; then
        # Running as root, use runuser
        local setup_output=$(runuser -u coder -- bash -c "
            # Source .bashrc if it exists to get user environment (for env vars like NODE_PATH, etc.)
            [ -f ~/.bashrc ] && source ~/.bashrc 2>/dev/null || true
            # Execute the setup script
            bash '$SETUP_SCRIPT'
        " 2>&1)
        local setup_exit=$?
    else
        # Already running as coder
        local setup_output=$(bash -c "
            # Source .bashrc if it exists to get user environment (for env vars like NODE_PATH, etc.)
            [ -f ~/.bashrc ] && source ~/.bashrc 2>/dev/null || true
            # Execute the setup script
            bash '$SETUP_SCRIPT'
        " 2>&1)
        local setup_exit=$?
    fi

    # Output the captured setup script output to stderr for logging
    if [ -n "$setup_output" ]; then
        echo "$setup_output" >&2
    fi

    if [ $setup_exit -eq 0 ]; then
        log "Setup script completed successfully" >&2
        return 0
    else
        log_error "Setup script failed with exit code $setup_exit" >&2
        return 1
    fi
}

# Generate git patches from changed repositories
# Optimized version: parallel processing, uses git diff --stat for faster stats
generate_patches() {
    local workspace_dir="${WORKSPACE_DIR:-/workspace}"
    local patches_dir="$TASK_OUTPUT_DIR/patches"
    local repos_changed_json="[]"

    log "Checking for repository changes in $workspace_dir..." >&2

    # Create patches directory and temp directory for parallel results
    mkdir -p "$patches_dir"
    local temp_dir=$(mktemp -d)
    trap "rm -rf $temp_dir" RETURN

    # Check if workspace directory exists
    if [ ! -d "$workspace_dir" ]; then
        log "No workspace directory found at $workspace_dir" >&2
        echo "$repos_changed_json"
        return
    fi

    # Helper function to process a single repository (runs in subshell for parallel execution)
    # Writes JSON result to a temp file instead of stdout
    _process_repo_parallel() {
        local repo_name="$1"
        local repo_path="$2"
        local output_file="$3"
        local patches_dir="$4"
        local ignore_lockfile="${IGNORE_LOCKFILE_CHANGES:-false}"

        cd "$repo_path" || return 1

        # Quick check: any changes at all? (single git status call)
        local status_output=$(git status --porcelain 2>/dev/null)
        if [ -z "$status_output" ]; then
            log "No changes in $repo_name" >&2
            return 1
        fi

        log "Changes detected in $repo_name, generating patch..." >&2

        # Stage all changes
        git add -A

        # Generate patch with optional lockfile exclusion
        local patch_file="$repo_name.patch"
        if [ "$ignore_lockfile" = "true" ]; then
            git diff --cached -- ':!package-lock.json' ':!**/package-lock.json' > "$patches_dir/$patch_file"
        else
            git diff --cached > "$patches_dir/$patch_file"
        fi

        # Check if patch has content
        if [ ! -s "$patches_dir/$patch_file" ]; then
            log "No changes to patch for $repo_name" >&2
            rm -f "$patches_dir/$patch_file"
            return 1
        fi

        # Use git diff --stat for fast stats
        local stat_output
        if [ "$ignore_lockfile" = "true" ]; then
            stat_output=$(git diff --cached --stat -- ':!package-lock.json' ':!**/package-lock.json' 2>/dev/null | tail -1)
        else
            stat_output=$(git diff --cached --stat 2>/dev/null | tail -1)
        fi

        # Parse stat output: "X files changed, Y insertions(+), Z deletions(-)"
        local files_changed=0
        local lines_added=0
        local lines_deleted=0

        if [ -n "$stat_output" ]; then
            files_changed=$(echo "$stat_output" | grep -oE '[0-9]+ file' | grep -oE '[0-9]+' || echo "0")
            lines_added=$(echo "$stat_output" | grep -oE '[0-9]+ insertion' | grep -oE '[0-9]+' || echo "0")
            lines_deleted=$(echo "$stat_output" | grep -oE '[0-9]+ deletion' | grep -oE '[0-9]+' || echo "0")
        fi

        # Ensure we have valid numbers
        files_changed=${files_changed:-0}
        lines_added=${lines_added:-0}
        lines_deleted=${lines_deleted:-0}

        # Get list of changed files using git diff --name-only
        # Write files JSON to a temp file to avoid "Argument list too long"
        # when large imports (thousands of files) exceed ARG_MAX.
        local files_list
        if [ "$ignore_lockfile" = "true" ]; then
            files_list=$(git diff --cached --name-only -- ':!package-lock.json' ':!**/package-lock.json' 2>/dev/null)
        else
            files_list=$(git diff --cached --name-only 2>/dev/null)
        fi
        local files_detail_file="$output_file.files"
        echo "$files_list" | jq -R -s -c 'split("\n") | map(select(length > 0)) | map({path: ., added: 0, deleted: 0})' > "$files_detail_file"
        [ ! -s "$files_detail_file" ] && echo '[]' > "$files_detail_file"

        log "Patch generated: $patch_file ($files_changed files, +$lines_added -$lines_deleted)" >&2

        # Write JSON to output file (for parallel collection)
        # Use --slurpfile to read the files array from disk instead of
        # passing it as a command-line argument.
        jq -n \
            --arg name "$repo_name" \
            --arg path "$repo_path" \
            --argjson files_changed "${files_changed:-0}" \
            --argjson lines_added "${lines_added:-0}" \
            --argjson lines_deleted "${lines_deleted:-0}" \
            --arg patch_file "$patch_file" \
            --slurpfile files_arr "$files_detail_file" \
            '{
                name: $name,
                path: $path,
                files_changed: $files_changed,
                lines_added: $lines_added,
                lines_deleted: $lines_deleted,
                patch_file: $patch_file,
                files: $files_arr[0]
            }' > "$output_file"
        rm -f "$files_detail_file"
        return 0
    }

    # Export function and variables for subshells
    export -f _process_repo_parallel
    export -f log
    export IGNORE_LOCKFILE_CHANGES

    local pids=()
    local repo_index=0

    # If REPOS_CONFIG is set, use it to find repositories (respects path property)
    if [ -n "$REPOS_CONFIG" ]; then
        local config_repo_count=$(echo "$REPOS_CONFIG" | jq 'length' 2>/dev/null)

        if [ -n "$config_repo_count" ] && [ "$config_repo_count" -gt 0 ]; then
            log "Using REPOS_CONFIG to check $config_repo_count repositories (parallel)" >&2

            for index in $(seq 0 $((config_repo_count - 1))); do
                local repo_json=$(echo "$REPOS_CONFIG" | jq -c ".[$index]" 2>/dev/null)

                if [ -z "$repo_json" ] || [ "$repo_json" = "null" ]; then
                    continue
                fi

                local repo_name=$(echo "$repo_json" | jq -r 'if type == "object" then (.name // empty) else . end' 2>/dev/null)
                local repo_path_from_json=$(echo "$repo_json" | jq -r 'if type == "object" then (.path // empty) else empty end' 2>/dev/null)

                if [ -z "$repo_name" ] || [ "$repo_name" = "null" ]; then
                    continue
                fi

                # Use path field from JSON if available, otherwise default to repo_name
                local repo_path
                if [ -n "$repo_path_from_json" ] && [ "$repo_path_from_json" != "null" ]; then
                    repo_path="$workspace_dir/$repo_path_from_json"
                else
                    repo_path="$workspace_dir/$repo_name"
                fi

                # Check if this is a git repository
                if [ ! -d "$repo_path/.git" ]; then
                    log "Skipping $repo_name (not a git repository at $repo_path)" >&2
                    continue
                fi

                # Launch repo processing in background
                local output_file="$temp_dir/repo_$repo_index.json"
                (
                    _process_repo_parallel "$repo_name" "$repo_path" "$output_file" "$patches_dir"
                ) &
                pids+=($!)
                repo_index=$((repo_index + 1))
            done
        fi
    else
        # Fallback: no REPOS_CONFIG, scan top-level directories
        log "No REPOS_CONFIG found, scanning top-level directories (parallel)" >&2
        for repo_path in "$workspace_dir"/*; do
            if [ -d "$repo_path/.git" ]; then
                local repo_name=$(basename "$repo_path")

                # Launch repo processing in background
                local output_file="$temp_dir/repo_$repo_index.json"
                (
                    _process_repo_parallel "$repo_name" "$repo_path" "$output_file" "$patches_dir"
                ) &
                pids+=($!)
                repo_index=$((repo_index + 1))
            fi
        done
    fi

    # Wait for all background jobs to complete
    for pid in "${pids[@]}"; do
        wait $pid 2>/dev/null || true
    done

    # Collect results from temp files into JSON array
    local repo_count=0
    for result_file in "$temp_dir"/repo_*.json; do
        if [ -f "$result_file" ] && [ -s "$result_file" ]; then
            local repo_result=$(cat "$result_file")
            if [ -n "$repo_result" ]; then
                if [ "$repo_count" -eq 0 ]; then
                    repos_changed_json="[$repo_result"
                else
                    repos_changed_json="$repos_changed_json,$repo_result"
                fi
                repo_count=$((repo_count + 1))
            fi
        fi
    done

    # Close JSON array
    if [ "$repo_count" -gt 0 ]; then
        repos_changed_json="$repos_changed_json]"
    fi

    log "Patch generation complete: $repo_count repositories with changes" >&2
    echo "$repos_changed_json"
}

write_init_failure_summary() {
    local stage="$1"
    local summary_file="$TASK_OUTPUT_DIR/summary.md"

    if [ -s "$summary_file" ]; then
        return 0
    fi

    cat <<SUMMARY_EOF > "$summary_file"
Task ${TASK_ID} failed during initialization.

- Task Type: ${TASK_TYPE}
- Environment: ${ENVIRONMENT}
- Stage: ${stage}
- Next Step: Review log.txt for the specific initialization error.
SUMMARY_EOF

    log "Summary file created at $summary_file"
}

# Generate task.json metadata file
generate_task_json() {
    local exit_code=$1
    shift
    local end_time=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
    local status="completed"

    if [ $exit_code -eq 130 ]; then
        status="interrupted"
    elif [ $exit_code -ne 0 ]; then
        status="failed"
    fi

    # Source environment variables set by setup script (e.g., IGNORE_LOCKFILE_CHANGES)
    if [ -f /home/coder/.bash_env ]; then
        source /home/coder/.bash_env
    fi

    # Generate patches and get repos_changed JSON
    local repos_changed=$(generate_patches)

    # Read commit message if available
    local commit_message=""
    local commit_message_file="$TASK_OUTPUT_DIR/commit-message.txt"
    if [ -f "$commit_message_file" ] && [ -s "$commit_message_file" ]; then
        commit_message=$(cat "$commit_message_file")
        # Properly escape commit message for JSON
        commit_message="${commit_message//\\/\\\\}"  # Escape backslashes first
        commit_message="${commit_message//\"/\\\"}"  # Escape quotes
        commit_message="${commit_message//$'\n'/\\n}" # Escape newlines
        commit_message="${commit_message//$'\r'/\\r}" # Escape carriage returns
        commit_message="${commit_message//$'\t'/\\t}" # Escape tabs
    fi

    # Properly escape command for JSON
    local escaped_command="$*"
    escaped_command="${escaped_command//\\/\\\\}"  # Escape backslashes first
    escaped_command="${escaped_command//\"/\\\"}"  # Escape quotes
    escaped_command="${escaped_command//$'\n'/\\n}" # Escape newlines
    escaped_command="${escaped_command//$'\r'/\\r}" # Escape carriage returns
    escaped_command="${escaped_command//$'\t'/\\t}" # Escape tabs

    # Preserve turn_patches accumulated by auto-sync from previous runs
    local existing_turn_patches='null'
    if [ -f "$TASK_OUTPUT_DIR/task.json" ]; then
        existing_turn_patches=$(jq -c '.turn_patches // null' "$TASK_OUTPUT_DIR/task.json" 2>/dev/null || echo 'null')
    fi

    # Create task.json in the output directory
    local turn_patches_field=""
    if [ "$existing_turn_patches" != "null" ] && [ -n "$existing_turn_patches" ]; then
        turn_patches_field=',"turn_patches": '"$existing_turn_patches"
    fi
    cat > "$TASK_OUTPUT_DIR/task.json.tmp" <<EOF
{
  "task_id": "$TASK_ID",
  "task_type": "$TASK_TYPE",
  "environment": "$ENVIRONMENT",
  "status": "$status",
  "started_at": "$START_TIME",
  "completed_at": "$end_time",
  "exit_code": $exit_code,
  "container_id": "$(hostname)",
  "command": "$escaped_command",
  "parameters": $PARAMETERS_JSON,
  "env_vars": $ENV_VARS_JSON,
  "summary_path": "summary.md",
  "log_path": "log.txt",
  "instructions_path": "task-instructions.md",
  "commit_message": "$commit_message",
  "repos_changed": $repos_changed$turn_patches_field
}
EOF
    mv "$TASK_OUTPUT_DIR/task.json.tmp" "$TASK_OUTPUT_DIR/task.json"

    log "Task metadata written to $TASK_OUTPUT_DIR/task.json"
}

# IBM i build library creation (called from main after repos are updated)
setup_ibmi_build_library() {
    # All five env vars must be set (via Docker Env from external-connections.js)
    if [ -z "${IBMI_HOST}" ] || [ -z "${IBMI_USER}" ] || [ -z "${IBMI_KEY}" ] || [ -z "${IBMI_BUILD_PREFIX}" ] || [ -z "${IBMI_BUILD_HOME}" ]; then
        return 0
    fi

    local IBMI_SSH_OPTS="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR -o BatchMode=yes -T"

    # Generate UUID suffix: strip dashes, remove version/variant nibbles, take first 24 chars
    local IBMI_RAW_UUID=$(cat /proc/sys/kernel/random/uuid | tr -d '-')
    local IBMI_BUILD_UUID=$(echo "${IBMI_RAW_UUID:0:12}${IBMI_RAW_UUID:13:3}${IBMI_RAW_UUID:17:15}" | head -c 24 | tr '[:lower:]' '[:upper:]')
    IBMI_BUILD_SCHEMA="${IBMI_BUILD_PREFIX}_${IBMI_BUILD_UUID}"

    log "Creating IBM i build library..."

    # Create schema on IBM i via SSH + QShell db2
    local IBMI_CMD_OUT
    IBMI_CMD_OUT=$(echo "db2 \"CREATE SCHEMA ${IBMI_BUILD_SCHEMA}\"" | ssh -i ${IBMI_KEY} ${IBMI_SSH_OPTS} ${IBMI_USER}@${IBMI_HOST} qsh 2>&1)
    if [ $? -ne 0 ]; then
        log_error "${IBMI_CMD_OUT}"
        log_error "Failed to create IBM i build library"
        return 1
    fi

    # Resolve the system library name (10-char *LIB name)
    IBMI_BUILD_LIBRARY=$(echo "db2 -S \"SELECT SYSTEM_SCHEMA_NAME FROM QSYS2.SYSSCHEMAS WHERE SCHEMA_NAME = '${IBMI_BUILD_SCHEMA}'\"" | ssh -i ${IBMI_KEY} ${IBMI_SSH_OPTS} ${IBMI_USER}@${IBMI_HOST} qsh 2>/dev/null | sed -n '4p' | tr -d '[:space:]')
    if [ -z "${IBMI_BUILD_LIBRARY}" ]; then
        log_error "Failed to resolve system library name for schema ${IBMI_BUILD_SCHEMA}"
        return 1
    fi

    export IBMI_BUILD_LIBRARY
    export IBMI_BUILD_SCHEMA

    # Tag library with task ID for traceability
    IBMI_CMD_OUT=$(echo "system \"QSYS/CHGOBJD OBJ(QSYS/${IBMI_BUILD_LIBRARY}) OBJTYPE(*LIB) TEXT('CoderFlow task ${TASK_ID}')\"" | ssh -i ${IBMI_KEY} ${IBMI_SSH_OPTS} ${IBMI_USER}@${IBMI_HOST} /QOpenSys/usr/bin/sh 2>&1)
    if [ $? -ne 0 ]; then
        log_error "${IBMI_CMD_OUT}"
        log_error "Failed to tag build library ${IBMI_BUILD_LIBRARY}"
    fi

    log "IBM i build library ${IBMI_BUILD_LIBRARY} created"

    # Write env vars to .bash_env so other processes (such as terminals) can access them
    echo "export IBMI_BUILD_LIBRARY=\"${IBMI_BUILD_LIBRARY}\"" >> /home/coder/.bash_env
    echo "export IBMI_BUILD_SCHEMA=\"${IBMI_BUILD_SCHEMA}\"" >> /home/coder/.bash_env

    # Initialize codermake (must run from the build home directory)
    log "Initializing codermake build in ${IBMI_BUILD_HOME}..."
    local codermake_out
    codermake_out=$( (cd "${IBMI_BUILD_HOME}" && codermake -t) 2>&1) || true
    if [ -n "$codermake_out" ]; then
        log "$codermake_out"
    fi

    # Write cleanup script for CMD SIGTERM trap to source
    # Values are baked in — no conditional check needed since this file is only
    # written after successful library creation
    cat > /home/coder/.ibmi_build_cleanup.sh << CLEANUP_EOF
IBMI_CMD_OUT=\$(ssh -i ${IBMI_KEY} ${IBMI_SSH_OPTS} ${IBMI_USER}@${IBMI_HOST} "qsh -c 'db2 \"DROP SCHEMA ${IBMI_BUILD_SCHEMA} CASCADE\"'" 2>&1)
if [ \$? -ne 0 ]; then
    echo "[EXTERNAL-CONN] ERROR: Failed to delete build library ${IBMI_BUILD_LIBRARY}" >&2
    echo "\${IBMI_CMD_OUT}" >&2
fi
CLEANUP_EOF
}

# Main execution
main() {
    timing_checkpoint "main: START"
    log "CoderFlow task execution starting..."
    log "Working directory: $(pwd)"

    setup_git_credentials
    setup_git_user_identity
    setup_credential_helper_cache

    # Log once if skipping all initialization (for follow-up tasks)
    if [ "$SKIP_INIT" = "true" ]; then
        log "Skipping initialization: repository updates, local state, AGENTS.md, setup script (SKIP_INIT=true)"
    fi

    timing_checkpoint "main: Before repository updates"
    # Update repositories before task execution (pull if exists, clone if not)
    # Skip if SKIP_INIT is set (e.g., for follow-up tasks where initialization is already done)
    # Always update repos first, even with local state (get latest from remote, then apply local changes on top)
    # For interactive containers WITHOUT local state, run in background to avoid blocking terminal startup
    # For task containers OR when local state will be applied, run synchronously
    if [ "$SKIP_INIT" = "true" ]; then
        timing_checkpoint "main: Repository updates skipped"
    elif [ "$CONTAINER_MODE" = "interactive" ] && [ ! -f "/task-output/local-state.json" ]; then
        log "Starting repository updates in background..."
        update_repositories &
        REPO_UPDATE_PID=$!
        timing_checkpoint "main: Repository updates backgrounded (PID: $REPO_UPDATE_PID)"
    else
        if ! update_repositories; then
            log_error "Repository synchronization failed; aborting task execution"
            write_task_status "InitFailed" "0" "repository_sync" "Repository synchronization failed; aborting task execution"
            write_init_failure_summary "repository_sync"
            generate_task_json 1 "repository_sync"
            return 1
        fi
        timing_checkpoint "main: Repository updates complete (synchronous)"
    fi

    timing_checkpoint "main: Before local state application"
    # Apply local repository state if provided (branch setup, staged/unstaged changes, untracked files)
    # Skip if SKIP_INIT is set
    # Local state is applied ON TOP of the latest remote state (git pull already completed synchronously above)
    if [ "$SKIP_INIT" = "true" ]; then
        timing_checkpoint "main: Local state application skipped"
    elif [ -f "/task-output/local-state.json" ]; then
        log "Applying local repository state..."
        # Run as coder user to avoid permission issues with git files
        if [ "$(id -u)" -eq 0 ]; then
            # Running as root, use runuser
            if runuser -u coder bash /usr/local/bin/apply-local-state.sh; then
                log "Local state applied successfully" >&2
                timing_checkpoint "main: Local state application complete"
            else
                log_error "Failed to apply local state (non-fatal, continuing...)" >&2
                timing_checkpoint "main: Local state application failed"
            fi
        else
            # Already running as coder
            if bash /usr/local/bin/apply-local-state.sh; then
                log "Local state applied successfully" >&2
                timing_checkpoint "main: Local state application complete"
            else
                log_error "Failed to apply local state (non-fatal, continuing...)" >&2
                timing_checkpoint "main: Local state application failed"
            fi
        fi

        # Create marker to signal local state is complete (startup script will wait for this)
        touch /tmp/.local-state-ready 2>/dev/null || true
        log "Local state ready marker created"
    else
        timing_checkpoint "main: No local state file found"
    fi

    # Copy AGENTS.md to workspace if available (centralized for all environments)
    # This provides AI agents with environment-specific instructions
    # NOTE: Only AGENTS.md is copied - README.md stays in the environment directory
    #       README.md is for human users/admins, not for AI agents
    # Skip if SKIP_INIT is set (e.g., for follow-up tasks where AGENTS.md is already present)
    if [ "$SKIP_INIT" = "true" ]; then
        : # Skip - already logged above
    else
        local agents_path="/workspace/AGENTS.md"
        local memory_index_path=""
        if [ -n "$ENVIRONMENT" ]; then
            memory_index_path="/coder-setup/$ENVIRONMENT/memory/MEMORY.md"
        fi

        append_agents_separator() {
            if [ -f "$agents_path" ] && [ -s "$agents_path" ]; then
                printf '\n' >> "$agents_path"
            fi
        }

        if [ -n "$ENVIRONMENT" ] && [ -f "/coder-setup/$ENVIRONMENT/AGENTS.md" ]; then
            log "Copying AGENTS.md from /coder-setup/$ENVIRONMENT/" >&2
            cp "/coder-setup/$ENVIRONMENT/AGENTS.md" "$agents_path"
        elif [ -n "$STANDARD_INSTRUCTIONS" ] || [ -n "$REPO_PROTECTION_INSTRUCTIONS_B64" ] || [ -f "$memory_index_path" ]; then
            : > "$agents_path"
            log "Creating runtime AGENTS.md for environment: $ENVIRONMENT" >&2
        fi

        if [ -f "$agents_path" ]; then
            # Append standard instruction blocks to AGENTS.md if configured
            # STANDARD_INSTRUCTIONS env var contains space-separated block names (e.g. "output-requirements screenshot-guidelines")
            STANDARD_INSTRUCTIONS_DIR="/usr/local/share/standard-instructions"
            if [ -n "$STANDARD_INSTRUCTIONS" ] && [ -d "$STANDARD_INSTRUCTIONS_DIR" ]; then
                for block in $STANDARD_INSTRUCTIONS; do
                    if [ -f "$STANDARD_INSTRUCTIONS_DIR/$block.md" ]; then
                        append_agents_separator
                        cat "$STANDARD_INSTRUCTIONS_DIR/$block.md" >> "$agents_path"
                        log "Standard instructions appended: $block" >&2
                    fi
                done
            fi

            if [ -n "$REPO_PROTECTION_INSTRUCTIONS_B64" ]; then
                decoded_repo_protection=$(printf '%s' "$REPO_PROTECTION_INSTRUCTIONS_B64" | base64 --decode 2>/dev/null)
                if [ $? -eq 0 ] && [ -n "$decoded_repo_protection" ]; then
                    append_agents_separator
                    printf '%s' "$decoded_repo_protection" >> "$agents_path"
                    log "Repository protection instructions appended to AGENTS.md" >&2
                else
                    log "Failed to decode repository protection instructions" >&2
                fi
            fi

            # Append memory index to AGENTS.md if memory is mounted
            if [ -f "$memory_index_path" ]; then
                append_agents_separator
                printf '## Memory Index\n\n' >> "$agents_path"
                cat "$memory_index_path" >> "$agents_path"
                log "Memory index appended to AGENTS.md" >&2
            fi

            # Create agent-specific symlink based on CODER_AGENT or default_agent
            AGENT="${CODER_AGENT:-${default_agent:-claude}}"
            if [ "$AGENT" = "claude" ]; then
                ln -sf "$agents_path" /workspace/CLAUDE.md
                log "AGENTS.md copied and CLAUDE.md symlink created" >&2
            elif [ "$AGENT" = "gemini" ]; then
                ln -sf "$agents_path" /workspace/GEMINI.md
                log "AGENTS.md copied and GEMINI.md symlink created" >&2
            elif [ "$AGENT" = "bob" ]; then
                # Bob-Shell reads AGENTS.md directly (no agent-specific symlink needed)
                log "AGENTS.md copied (Bob reads AGENTS.md directly)" >&2
            elif [ "$AGENT" = "codex" ]; then
                # Codex doesn't use agent-specific files, just AGENTS.md
                log "AGENTS.md copied (no agent-specific symlink for Codex)" >&2
            elif [ "$AGENT" = "grok" ]; then
                # Grok Build reads AGENTS.md directly (no agent-specific symlink needed)
                log "AGENTS.md copied (Grok reads AGENTS.md directly)" >&2
            else
                log "AGENTS.md copied (unknown agent: $AGENT)" >&2
            fi
        else
            log "No AGENTS.md found for environment: $ENVIRONMENT" >&2
        fi
    fi

    timing_checkpoint "main: Before setup script"
    # Run environment-specific setup script when available
    # Skip if SKIP_INIT is set (e.g., for follow-up tasks where environment is already configured)
    if [ "$SKIP_INIT" = "true" ]; then
        timing_checkpoint "main: Setup script skipped"
    elif ! run_setup_script; then
        log_error "Environment setup script failed; aborting task execution"
        write_task_status "InitFailed" "0" "setup_script" "Environment setup script failed; aborting task execution"
        write_init_failure_summary "setup_script"
        generate_task_json 1 "setup_script"
        return 1
    else
        timing_checkpoint "main: Setup script complete"
    fi

    # IBM i build library creation (after repos are updated)
    # Skip if SKIP_INIT is set (e.g., for follow-up tasks where the library already exists)
    if [ "$SKIP_INIT" != "true" ]; then
        setup_ibmi_build_library
    fi

    local exit_code=0

    # Update status to indicate initialization is complete
    write_task_status "Ready" "0.01"
    timing_checkpoint "main: Status file written, ready for task execution"

    # If SETUP_ONLY mode, return here without executing tasks (for interactive containers)
    if [ "${SETUP_ONLY:-false}" = "true" ]; then
        log "Setup complete (SETUP_ONLY mode), skipping task execution"
        return 0
    fi

    # If STAGED_TASK mode, complete setup but skip agent execution (user will start later)
    if [ "${STAGED_TASK:-false}" = "true" ]; then
        write_task_status "Staged" "0.01"
        log "Setup complete (STAGED_TASK mode), waiting for instructions"
        return 0
    fi

    # If a command was passed, execute it
    # Otherwise, execute default task
    if [ $# -gt 0 ]; then
        timing_checkpoint "main: Before task execution"
        log "Executing task..."

        {
            # Source ~/.bashrc to load environment variables written by setup.sh
            # This is needed for non-interactive shells (tasks) where .bashrc isn't auto-sourced
            if [ -f ~/.bashrc ]; then
                source ~/.bashrc
            fi
            "$@"
        } 2>&1 | tee -a "$LOG_FILE"
        exit_code=${PIPESTATUS[0]}

        if [ $exit_code -eq 0 ]; then
            log_success "Task completed successfully (exit code: $exit_code)"
        elif [ $exit_code -eq 130 ]; then
            log "Task interrupted (exit code: $exit_code)"
        else
            log_error "Task failed (exit code: $exit_code)"
        fi
    else
        # Default task: simple hello world
        log "No command specified, executing default task..."

        {
            echo "Hello from CoderFlow!"
            echo "Node.js version: $(node --version)"
            echo "Git version: $(git --version)"
            echo "Current directory: $(pwd)"
            echo "Task completed successfully!"
        } 2>&1 | tee -a "$LOG_FILE"

        exit_code=0
        log_success "Default task completed (exit code: $exit_code)"
    fi

    log "Task execution finished"

    local summary_file="$TASK_OUTPUT_DIR/summary.md"

    if [ ! -s "$summary_file" ]; then
        if [ $exit_code -eq 0 ]; then
            cat <<SUMMARY_EOF > "$summary_file"
Task ${TASK_ID} completed successfully.

- Task Type: ${TASK_TYPE}
- Environment: ${ENVIRONMENT}
SUMMARY_EOF
        elif [ $exit_code -eq 130 ]; then
            cat <<SUMMARY_EOF > "$summary_file"
Task ${TASK_ID} was interrupted.

- Task Type: ${TASK_TYPE}
- Environment: ${ENVIRONMENT}
SUMMARY_EOF
        else
            cat <<SUMMARY_EOF > "$summary_file"
Task ${TASK_ID} failed with exit code ${exit_code}.

- Task Type: ${TASK_TYPE}
- Environment: ${ENVIRONMENT}
SUMMARY_EOF
        fi
        log "Summary file created at $summary_file"
    fi

    # Generate task metadata
    generate_task_json $exit_code "$@"

    timing_checkpoint "main: END"
    return $exit_code
}

# Run main and preserve exit code (unless SKIP_MAIN_EXECUTION is set)
if [ "${SKIP_MAIN_EXECUTION:-false}" != "true" ]; then
    main "$@"
    exit $?
fi
