#!/bin/bash
# Quickly compile and run C/C++ snippets against IREE runtime.
#
# Usage: iree-bazel-try [options] [files...] [-- program-args...]
#
# Examples:
#   iree-bazel-try snippet.c
#   echo 'int main() { return 0; }' | iree-bazel-try
#   iree-bazel-try -e 'int main() { return 42; }' -c
#   iree-bazel-try --dep //runtime/src/iree/vm test.c -- --help

set -e

# Source shared library.
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/iree-bazel-lib"
iree_bazel_init "iree-bazel-try"

# Expand combined short flags (e.g., -nv -> -n -v).
eval "set -- $(iree_expand_combined_flags "$@")"

show_help() {
    cat << 'EOF'
iree-bazel-try - Compile and run C/C++ snippets against IREE runtime

Automatically infers dependencies from #include directives. Just write code
with IREE headers and it figures out what deps are needed.

USAGE
    iree-bazel-try [options] [files...] [-- program-args...]

OPTIONS
    -n, --dry_run       Show the bazel command without executing
    -v, --verbose       Show bazel commands, inferred deps, and temp paths
    -h, --help          Show this help

    NOTE: Short flags can be combined: -nv is equivalent to -n -v

INPUT (at least one required)
    files...              C/C++ source files (multiple allowed)
    -e, --execute=CODE    Inline code (repeatable, concatenated)
    (stdin)               Reads from stdin if no files and no -e

LANGUAGE
    -x c                  Force C language (default for stdin)
    -x c++                Force C++ language

COMPILATION
    --config=NAME         Bazel config (debug, asan, msan, tsan)
    --features=NAME       Bazel feature (thin_lto, etc; repeatable)
    --copt=FLAG           Compiler flag for all targets (repeatable)
    --linkopt=FLAG        Linker flag for all targets (repeatable)
    --dep=LABEL           Add explicit Bazel dependency (repeatable)
    --no_infer            Disable automatic dependency inference
    --retry=N             Max build retries for dep fixing (default: 3)

OUTPUT
    -c, --compile_only  Compile but don't run (exit 0 on success)
    -o, --output PATH   Copy binary to PATH after build

DEBUG
    --keep              Keep temp directory for inspection

CACHE MANAGEMENT
    --clean             Remove all cached data and exit
    --cache_status      Show cache usage statistics and exit

EXIT CODES
    0                   Success (build/run completed)
    1                   Build failed (compiler errors, missing deps)
    N                   Program exit code (when running)

===============================================================================
QUICK EXPLORATION - Use C like you'd use Python
===============================================================================

The primary use case: quickly answer "what does this function do?" without
writing BUILD files or hunting for dependencies.

EXAMPLE: "What does iree_unicode_utf8_decode return for an emoji?"
    $ iree-bazel-try -e '
    #include "iree/base/internal/unicode.h"
    #include <stdio.h>
    int main() {
      const char* emoji = "\xF0\x9F\x98\x80";  // U+1F600 grinning face
      iree_string_view_t text = iree_make_cstring_view(emoji);
      iree_host_size_t pos = 0;
      uint32_t cp = iree_unicode_utf8_decode(text, &pos);
      printf("Codepoint: U+%04X, bytes consumed: %zu\n", cp, pos);
      return 0;
    }'
    Codepoint: U+1F600, bytes consumed: 4

===============================================================================
MORE EXAMPLES
===============================================================================

EXAMPLE: Minimal test (stdin)
    $ echo 'int main() { return 0; }' | iree-bazel-try
    (no output, exit code 0)

EXAMPLE: Using IREE APIs (auto-infers deps)
    $ iree-bazel-try -e '#include "iree/base/api.h"
    #include <stdio.h>
    int main() {
      iree_status_t s = iree_ok_status();
      printf("ok: %d\n", iree_status_is_ok(s));
      return 0;
    }'
    ok: 1

EXAMPLE: Compile-only mode
    $ iree-bazel-try -c -e 'int main() { return 0; }'
    (exit code 0)

EXAMPLE: Pass arguments to program
    $ iree-bazel-try -e '#include <stdio.h>
    int main(int argc, char** argv) {
      for (int i = 0; i < argc; i++) printf("%d: %s\n", i, argv[i]);
      return 0;
    }' -- --flag value
    0: /path/to/.../snippet
    1: --flag
    2: value

EXAMPLE: Multiple source files
    $ echo 'int helper() { return 42; }' > helper.c
    $ echo 'extern int helper(); int main() { return helper() - 42; }' > main.c
    $ iree-bazel-try main.c helper.c
    (exit code 0)

EXAMPLE: C++ with custom flags (auto-detected from includes or use -x c++)
    $ iree-bazel-try --copt=-std=c++17 -e '
    #include <optional>
    #include <stdio.h>
    int main() {
      std::optional<int> x = 42;
      printf("%d\n", *x);
      return 0;
    }'
    42

EXAMPLE: Build with AddressSanitizer
    $ iree-bazel-try --config=asan -e '
    #include <stdlib.h>
    int main() {
      int* p = malloc(4);
      free(p);
      return *p;  // use-after-free
    }'
    ==ERROR: AddressSanitizer: heap-use-after-free...

EXAMPLE: Verbose mode (shows deps and commands)
    $ iree-bazel-try -v -c -e '#include "iree/hal/api.h"
    int main() { return 0; }'
    [iree-bazel-try] Inferred deps: //runtime/src/iree/hal
    [iree-bazel-try] Building //.iree-bazel-try/...:snippet...

===============================================================================
MLIR TRANSFORM HARNESS - Quick compiler experiments
===============================================================================

For one-shot MLIR transforms without modifying the compiler. C++ auto-detected.

EXAMPLE: Walk operations (analysis)
    $ echo 'func.func @f() { return }' | iree-bazel-try -e '
    #include "iree/compiler/Tools/MlirTransformHarness.h"
    void xform(ModuleOp m) {
      m.walk([](Operation *op) { llvm::outs() << op->getName() << "\n"; });
    }
    MLIR_TRANSFORM_MAIN_NO_PRINT(xform)
    '
    func.return
    func.func
    builtin.module

EXAMPLE: Pattern rewrite (transform)
    $ echo 'func.func @f(%a: i32) -> i32 {
      %c0 = arith.constant 0 : i32
      %r = arith.addi %a, %c0 : i32
      return %r : i32
    }' | iree-bazel-try -e '
    #include "iree/compiler/Tools/MlirTransformHarness.h"
    #include "mlir/Dialect/Arith/IR/Arith.h"
    struct AddZero : OpRewritePattern<arith::AddIOp> {
      using OpRewritePattern::OpRewritePattern;
      LogicalResult matchAndRewrite(arith::AddIOp op, PatternRewriter &rw) const override {
        if (auto c = op.getRhs().getDefiningOp<arith::ConstantIntOp>())
          if (c.value() == 0) { rw.replaceOp(op, op.getLhs()); return success(); }
        return failure();
      }
    };
    MLIR_PATTERN_MAIN(AddZero)
    '
    (outputs transformed IR with addi removed)

===============================================================================
GTEST HARNESS - Quick runtime tests
===============================================================================

Run gtests without BUILD files. Includes status matchers. C++ auto-detected.

EXAMPLE: Test with status matchers
    $ iree-bazel-try -e '
    #include "iree/testing/gtest_harness.h"
    TEST(StatusTest, OkStatus) {
      IREE_EXPECT_OK(iree_ok_status());
      EXPECT_THAT(iree_ok_status(), IsOk());
    }
    TEST(StatusTest, ErrorStatus) {
      EXPECT_THAT(iree_make_status(IREE_STATUS_INVALID_ARGUMENT),
                  StatusIs(StatusCode::kInvalidArgument));
    }
    '

===============================================================================
GBENCHMARK HARNESS - Quick benchmarks
===============================================================================

Run Google Benchmark microbenchmarks. C++ auto-detected.

EXAMPLE: Benchmark memory allocation
    $ iree-bazel-try -e '
    #include "iree/testing/gbenchmark_harness.h"
    void BM_Alloc(benchmark::State& state) {
      for (auto _ : state) {
        void* p = NULL;
        iree_allocator_malloc(iree_allocator_system(), 1024, &p);
        DoNotOptimize(p);
        iree_allocator_free(iree_allocator_system(), p);
      }
    }
    BENCHMARK(BM_Alloc);
    '

===============================================================================
AUTOMATIC DEPENDENCY INFERENCE
===============================================================================

The tool automatically scans source files for #include "iree/..." directives
and infers the corresponding Bazel dependencies:

    #include "iree/base/api.h"      -> //runtime/src/iree/base
    #include "iree/hal/buffer.h"    -> //runtime/src/iree/hal
    #include "iree/vm/module.h"     -> //runtime/src/iree/vm
    #include "iree/io/file_handle.h" -> //runtime/src/iree/io

If the build fails with missing symbols, the tool will attempt to find the
correct dependency using bazel query and retry the build (up to --retry times).

Use --no_infer to disable this and rely only on explicit --dep flags.

AI AGENT INTEGRATION
    --agent-md          Print a concise snippet for CLAUDE.md/AGENT.md files

SEE ALSO
    iree-bazel-build, iree-bazel-test, iree-bazel-run
EOF
}

# Maximum number of cache slots to keep (LRU eviction beyond this).
MAX_CACHE_SLOTS=8

# Portable hash command: use md5sum on Linux, md5 on macOS/BSD.
_portable_hash() {
    if command -v md5sum &>/dev/null; then
        md5sum | cut -c1-12
    else
        md5 -r | cut -c1-12
    fi
}

# Portable file locking using mkdir (atomic on all POSIX systems).
# flock is Linux-specific; mkdir-based locking works everywhere.
#
# Stale lock detection: if the process that created the lock is dead,
# we reclaim the lock immediately rather than waiting forever.
_acquire_lock() {
    local lock_dir="${1}.d"
    local timeout="${2:-60}"
    local waited=0

    while true; do
        # Try to atomically create lock directory.
        if mkdir "${lock_dir}" 2>/dev/null; then
            # Successfully acquired lock - write our PID.
            echo $$ > "${lock_dir}/pid"
            return 0
        fi

        # Lock exists - check if holder is still alive.
        if [[ -f "${lock_dir}/pid" ]]; then
            local holder_pid
            holder_pid=$(cat "${lock_dir}/pid" 2>/dev/null)
            if [[ -n "${holder_pid}" ]]; then
                # kill -0 checks if process exists without sending signal.
                if ! kill -0 "${holder_pid}" 2>/dev/null; then
                    # Holder process is dead - reclaim the stale lock.
                    iree_debug "Reclaiming stale lock from dead process ${holder_pid}"
                    rm -rf "${lock_dir}"
                    continue  # Retry acquisition immediately.
                fi
            fi
        elif [[ -d "${lock_dir}" ]]; then
            # Lock directory exists but no PID file - likely a very old stale lock
            # or mid-creation crash. Give it a moment, then reclaim.
            if [[ "${waited}" -gt 2 ]]; then
                iree_debug "Reclaiming orphaned lock directory (no PID file)"
                rm -rf "${lock_dir}"
                continue
            fi
        fi

        # Lock is held by a live process (or we're giving it a grace period).
        sleep 1
        waited=$((waited + 1))
        if [[ "${waited}" -ge "${timeout}" ]]; then
            # Provide actionable error message.
            local msg="Timeout waiting for lock on cache slot"
            if [[ -f "${lock_dir}/pid" ]]; then
                local holder_pid
                holder_pid=$(cat "${lock_dir}/pid" 2>/dev/null)
                msg="${msg} (held by PID ${holder_pid})"
            fi
            iree_warn "${msg}"
            return 1
        fi
    done
}

_release_lock() {
    local lock_dir="${1}.d"
    rm -rf "${lock_dir}"
}

# Clean up any stale lock directories in the cache.
# Called during cache maintenance to prevent lock accumulation.
_cleanup_stale_locks() {
    local cache_dir="${1}"
    if [[ ! -d "${cache_dir}" ]]; then
        return
    fi

    # Find all .lock.d directories.
    while IFS= read -r lock_dir; do
        [[ -z "${lock_dir}" ]] && continue
        if [[ -f "${lock_dir}/pid" ]]; then
            local holder_pid
            holder_pid=$(cat "${lock_dir}/pid" 2>/dev/null)
            if [[ -n "${holder_pid}" ]] && ! kill -0 "${holder_pid}" 2>/dev/null; then
                iree_debug "Cleaning stale lock from dead process ${holder_pid}"
                rm -rf "${lock_dir}"
            fi
        elif [[ -d "${lock_dir}" ]]; then
            # No PID file - orphaned lock.
            iree_debug "Cleaning orphaned lock directory: ${lock_dir}"
            rm -rf "${lock_dir}"
        fi
    done < <(find "${cache_dir}" -maxdepth 1 -name "*.lock.d" -type d 2>/dev/null)
}

# Compute a hash for cache slot naming based on deps-affecting inputs.
# Hash is based on #includes + explicit deps + build options + language.
# File inputs also include content hash to avoid stale builds.
# This allows multiple runs with same deps config to share the cached BUILD.bazel.
# Different build flags (copts, linkopts, features) get separate cache slots so Bazel's
# action cache doesn't thrash when alternating between flag combinations.
# Args: source_files array, extra_deps array, copts array, linkopts array, language,
#       bazel_user_flags array, include_contents(0/1)
compute_cache_hash() {
    local -n _source_files=${1}
    local -n _extra_deps=${2}
    local -n _copts=${3}
    local -n _linkopts=${4}
    local _language=${5:-c}
    local -n _bazel_user_flags=${6}
    local _include_contents=${7:-0}
    {
        # Hash language (determines file extension and compilation mode).
        printf 'language:%s\n' "${_language}"
        # Hash #include directives (determines inferred deps).
        printf 'includes:\n'
        for src in "${_source_files[@]}"; do
            grep -h '#include\s*"[^"]*"' "${src}" 2>/dev/null | \
                sed 's/.*#include\s*"\([^"]*\)".*/\1/' | \
                grep -E '^(iree/|llvm/|mlir/)'
        done | sort -u
        # Hash explicit deps (user-provided --dep flags).
        printf 'extra_deps:\n'
        printf '%s\n' "${_extra_deps[@]}" | sort -u
        # Hash build flags that affect compilation output.
        printf 'copts:\n'
        printf '%s\n' "${_copts[@]}" | sort -u
        printf 'linkopts:\n'
        printf '%s\n' "${_linkopts[@]}" | sort -u
        printf 'bazel_flags:\n'
        printf '%s\n' "${_bazel_user_flags[@]}" | sort -u
        if [[ "${_include_contents}" == "1" ]]; then
            printf 'contents:\n'
            for src in "${_source_files[@]}"; do
                if [[ -f "${src}" ]]; then
                    printf '%s:%s\n' "$(basename "${src}")" "$(cat "${src}" | _portable_hash)"
                fi
            done | sort -u
        fi
    } | _portable_hash
}

# Ensure cache doesn't exceed MAX_CACHE_SLOTS by evicting oldest slots.
# Uses mtime for LRU ordering (we touch slots on use).
# Also cleans up any stale locks from crashed processes.
enforce_cache_limit() {
    local cache_dir="${1}"
    if [[ ! -d "${cache_dir}" ]]; then
        return
    fi

    # First, clean up any stale lock directories.
    _cleanup_stale_locks "${cache_dir}"

    # Count only actual cache slot directories (not .lock.d directories).
    local slot_count
    slot_count=$(find "${cache_dir}" -mindepth 1 -maxdepth 1 -type d ! -name "*.lock.d" 2>/dev/null | wc -l)
    if [[ "${slot_count}" -gt "${MAX_CACHE_SLOTS}" ]]; then
        local excess=$((slot_count - MAX_CACHE_SLOTS))
        iree_debug "Cache has ${slot_count} slots, evicting ${excess} oldest"
        # Find oldest directories by mtime and remove them (exclude lock dirs).
        find "${cache_dir}" -mindepth 1 -maxdepth 1 -type d ! -name "*.lock.d" -printf '%T@ %p\n' 2>/dev/null | \
            sort -n | head -n "${excess}" | cut -d' ' -f2- | \
            while read -r old_slot; do
                rm -rf "${old_slot}"
                iree_debug "Evicted cache slot: ${old_slot}"
            done
    fi
}

# Show cache usage statistics.
show_cache_status() {
    local base_dir="${1}"
    local cache_dir="${base_dir}/cache"
    local output_base="${base_dir}/output_base"

    echo "iree-bazel-try cache status"
    echo "==========================="
    echo ""
    echo "Base directory: ${base_dir}"
    echo ""

    # Cache slots.
    if [[ -d "${cache_dir}" ]]; then
        local slot_count
        slot_count=$(find "${cache_dir}" -mindepth 1 -maxdepth 1 -type d ! -name "*.lock.d" 2>/dev/null | wc -l)
        local slot_size
        slot_size=$(du -sh "${cache_dir}" 2>/dev/null | cut -f1)
        echo "Cache slots: ${slot_count}/${MAX_CACHE_SLOTS} (${slot_size})"

        # List slots with age.
        if [[ "${slot_count}" -gt 0 ]]; then
            echo ""
            echo "Slots (newest first):"
            local now
            now=$(date +%s)
            find "${cache_dir}" -mindepth 1 -maxdepth 1 -type d ! -name "*.lock.d" -printf '%T@ %f\n' 2>/dev/null | \
                sort -rn | \
                while read -r mtime slot; do
                    local age_seconds=$((now - ${mtime%.*}))
                    local age_str
                    if [[ "${age_seconds}" -lt 60 ]]; then
                        age_str="${age_seconds}s ago"
                    elif [[ "${age_seconds}" -lt 3600 ]]; then
                        age_str="$((age_seconds / 60))m ago"
                    elif [[ "${age_seconds}" -lt 86400 ]]; then
                        age_str="$((age_seconds / 3600))h ago"
                    else
                        age_str="$((age_seconds / 86400))d ago"
                    fi
                    local slot_size
                    slot_size=$(du -sh "${cache_dir}/${slot}" 2>/dev/null | cut -f1)
                    echo "  ${slot} (${slot_size}, ${age_str})"
                done
        fi

        # Stale locks.
        local lock_count
        lock_count=$(find "${cache_dir}" -maxdepth 1 -name "*.lock.d" -type d 2>/dev/null | wc -l)
        if [[ "${lock_count}" -gt 0 ]]; then
            echo ""
            echo "Lock directories: ${lock_count}"
            find "${cache_dir}" -maxdepth 1 -name "*.lock.d" -type d 2>/dev/null | \
                while read -r lock_dir; do
                    local status="active"
                    if [[ -f "${lock_dir}/pid" ]]; then
                        local holder_pid
                        holder_pid=$(cat "${lock_dir}/pid" 2>/dev/null)
                        if [[ -n "${holder_pid}" ]] && ! kill -0 "${holder_pid}" 2>/dev/null; then
                            status="STALE (PID ${holder_pid} dead)"
                        else
                            status="held by PID ${holder_pid}"
                        fi
                    else
                        status="ORPHANED (no PID)"
                    fi
                    echo "  $(basename "${lock_dir}"): ${status}"
                done
        fi
    else
        echo "Cache slots: 0/${MAX_CACHE_SLOTS} (not created yet)"
    fi

    echo ""

    # Bazel output_base (can be large).
    if [[ -d "${output_base}" ]]; then
        local output_size
        output_size=$(du -sh "${output_base}" 2>/dev/null | cut -f1)
        echo "Bazel output_base: ${output_size}"
        echo "  (This is used when --copt/--features are specified)"
    else
        echo "Bazel output_base: not created"
    fi

    echo ""

    # Total size.
    if [[ -d "${base_dir}" ]]; then
        local total_size
        total_size=$(du -sh "${base_dir}" 2>/dev/null | cut -f1)
        echo "Total: ${total_size}"
    fi
}

# Clean all cached data.
clean_cache() {
    local base_dir="${1}"

    if [[ ! -d "${base_dir}" ]]; then
        echo "No cache directory to clean."
        return 0
    fi

    echo "Cleaning iree-bazel-try cache..."

    # Show what we're about to clean.
    local total_size
    total_size=$(du -sh "${base_dir}" 2>/dev/null | cut -f1)
    echo "  Directory: ${base_dir}"
    echo "  Size: ${total_size}"

    # Clean staging directories (from any crashed processes).
    local staging_count
    staging_count=$(find "${base_dir}" -maxdepth 1 -name "staging_*" -type d 2>/dev/null | wc -l)
    if [[ "${staging_count}" -gt 0 ]]; then
        echo "  Removing ${staging_count} staging directories..."
        find "${base_dir}" -maxdepth 1 -name "staging_*" -type d -exec rm -rf {} \; 2>/dev/null
    fi

    # Clean cache slots (includes stale locks).
    if [[ -d "${base_dir}/cache" ]]; then
        local slot_count
        slot_count=$(find "${base_dir}/cache" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | wc -l)
        echo "  Removing ${slot_count} cache entries..."
        rm -rf "${base_dir}/cache"
    fi

    # Clean Bazel output_base.
    if [[ -d "${base_dir}/output_base" ]]; then
        local output_size
        output_size=$(du -sh "${base_dir}/output_base" 2>/dev/null | cut -f1)
        echo "  Removing Bazel output_base (${output_size})..."
        rm -rf "${base_dir}/output_base"
    fi

    # Clean bazel-* symlinks.
    for symlink in "${base_dir}"/bazel-*; do
        if [[ -L "${symlink}" ]]; then
            echo "  Removing symlink: $(basename "${symlink}")"
            rm -f "${symlink}"
        fi
    done

    echo "Done."
}

# Infer Bazel deps from #include directives in source files.
# Uses bazel query attr(hdrs, pattern) to find targets that export each header.
# Outputs one dep per line.
infer_deps_from_sources() {
    local -a sources=("$@")
    local BAZEL_BIN
    BAZEL_BIN=$(iree_get_bazel_command)
    local -a runtime_patterns=()
    local -a compiler_patterns=()
    local -a llvm_files=()
    local -a mlir_files=()

    # Collect all headers from all source files.
    for src in "${sources[@]}"; do
        if [[ ! -f "${src}" ]]; then
            continue
        fi

        # Extract all #include "..." directives.
        while IFS= read -r header; do
            local dir_path="${header%/*}"
            local filename="${header##*/}"

            case "${header}" in
                iree/compiler/*)
                    # Pattern: iree/compiler/Foo/Bar:file.h (use path for specificity).
                    compiler_patterns+=("${dir_path}:${filename}")
                    ;;
                iree/*)
                    # Pattern: iree/base:api.h or iree/base/internal:unicode.h.
                    runtime_patterns+=("${dir_path}:${filename}")
                    ;;
                llvm/*)
                    # LLVM uses different structure, just use filename.
                    llvm_files+=("${filename}")
                    ;;
                mlir/*)
                    # MLIR uses different structure, just use filename.
                    mlir_files+=("${filename}")
                    ;;
            esac
        done < <(grep -h '#include\s*"[^"]*"' "${src}" 2>/dev/null | \
                 sed 's/.*#include\s*"\([^"]*\)".*/\1/' | \
                 grep -E '^(iree/|llvm/|mlir/)')
    done

    # Query each category using attr(hdrs, pattern) - handles all package structures.
    local -a all_deps=()

    if [[ ${#runtime_patterns[@]} -gt 0 ]]; then
        iree_debug "Querying deps for ${#runtime_patterns[@]} runtime headers..."
        local pattern
        pattern=$(printf '%s\n' "${runtime_patterns[@]}" | sed 's/\./\\./g' | sort -u | paste -sd'|' -)
        while IFS= read -r dep; do
            [[ -n "${dep}" ]] && all_deps+=("${dep}")
        done < <("${BAZEL_BIN}" query "attr(hdrs, '(${pattern})', //runtime/src/iree/...)" 2>/dev/null 9>&-)
    fi

    if [[ ${#compiler_patterns[@]} -gt 0 ]]; then
        iree_debug "Querying deps for ${#compiler_patterns[@]} compiler headers..."
        local pattern
        pattern=$(printf '%s\n' "${compiler_patterns[@]}" | sed 's/\./\\./g' | sort -u | paste -sd'|' -)
        while IFS= read -r dep; do
            [[ -n "${dep}" ]] && all_deps+=("${dep}")
        done < <("${BAZEL_BIN}" query "attr(hdrs, '(${pattern})', //compiler/src/iree/compiler/...)" 2>/dev/null 9>&-)
    fi

    if [[ ${#llvm_files[@]} -gt 0 ]]; then
        iree_debug "Querying deps for ${#llvm_files[@]} LLVM headers..."
        local pattern
        pattern=$(printf '%s\n' "${llvm_files[@]}" | sed 's/\./\\./g' | sort -u | paste -sd'|' -)
        while IFS= read -r dep; do
            [[ -n "${dep}" ]] && all_deps+=("${dep}")
        done < <("${BAZEL_BIN}" query "attr(hdrs, '(${pattern})', @llvm-project//llvm/...)" 2>/dev/null 9>&-)
    fi

    if [[ ${#mlir_files[@]} -gt 0 ]]; then
        iree_debug "Querying deps for ${#mlir_files[@]} MLIR headers..."
        local pattern
        pattern=$(printf '%s\n' "${mlir_files[@]}" | sed 's/\./\\./g' | sort -u | paste -sd'|' -)
        while IFS= read -r dep; do
            [[ -n "${dep}" ]] && all_deps+=("${dep}")
        done < <("${BAZEL_BIN}" query "attr(hdrs, '(${pattern})', @llvm-project//mlir/...)" 2>/dev/null 9>&-)
    fi

    # Output unique deps.
    printf '%s\n' "${all_deps[@]}" | sort -u
}

# Try to find a dep for a missing symbol using bazel query.
# This is called when build fails.
find_dep_for_symbol() {
    local symbol="${1}"
    local worktree="${2}"

    # Search for the symbol in IREE runtime sources.
    local matches
    matches=$(grep -rl "^[^/]*\b${symbol}\b" "${worktree}/runtime/src/iree/" 2>/dev/null | head -5)

    for match in ${matches}; do
        # Convert file path to bazel package.
        local rel_path="${match#${worktree}/}"
        local dir_path="${rel_path%/*}"
        echo "//${dir_path}"
        return 0
    done

    return 1
}

# Parse build errors and suggest missing deps.
# Returns deps to add, one per line.
parse_build_errors() {
    local log_file="${1}"
    local worktree="${2}"
    local -A suggested_deps

    # Look for undefined reference errors.
    while IFS= read -r symbol; do
        if [[ -n "${symbol}" ]]; then
            local dep
            if dep=$(find_dep_for_symbol "${symbol}" "${worktree}" 2>/dev/null); then
                if [[ -z "${suggested_deps[${dep}]:-}" ]]; then
                    suggested_deps[${dep}]=1
                    echo "${dep}"
                fi
            fi
        fi
    done < <(grep -o "undefined reference to \`[^']*'" "${log_file}" 2>/dev/null | \
             sed "s/undefined reference to \`\\([^']*\\)'/\\1/")

    # Look for missing header errors.
    while IFS= read -r header; do
        if [[ "${header}" == iree/* ]]; then
            local package="${header%/*}"
            # Simplify to top-level package.
            local component="${package#iree/}"
            component="${component%%/*}"
            local dep="//runtime/src/iree/${component}"
            if [[ -z "${suggested_deps[${dep}]:-}" ]]; then
                suggested_deps[${dep}]=1
                echo "${dep}"
            fi
        fi
    done < <(grep -o "fatal error: '[^']*' file not found" "${log_file}" 2>/dev/null | \
             sed "s/fatal error: '\\([^']*\\)' file not found/\\1/")
}

# Main script.
DRY_RUN=0
COMPILE_ONLY=0
KEEP_TEMP=0
INFER_DEPS=1
MAX_RETRIES=3
OUTPUT_PATH=""
LANGUAGE=""
INLINE_CODE=""
declare -a SOURCE_FILES=()
declare -a EXTRA_DEPS=()
declare -a COPTS=()
declare -a LINKOPTS=()
declare -a BAZEL_ARGS=()
declare -a BAZEL_USER_FLAGS=()
declare -a PROGRAM_ARGS=()

# Parse arguments.
PARSING_PROGRAM_ARGS=0
while [[ $# -gt 0 ]]; do
    if [[ "${PARSING_PROGRAM_ARGS}" == "1" ]]; then
        PROGRAM_ARGS+=("${1}")
        shift
        continue
    fi

    case "${1}" in
        -h|--help)
            show_help
            exit 0
            ;;
        --agent-md|--agent_md)
            iree_show_agent_md
            exit 0
            ;;
        -n|--dry_run|--dry-run)
            DRY_RUN=1
            shift
            ;;
        -v|--verbose)
            IREE_BAZEL_VERBOSE=1
            shift
            ;;
        -c|--compile_only|--compile_only)
            COMPILE_ONLY=1
            shift
            ;;
        --keep)
            KEEP_TEMP=1
            shift
            ;;
        --clean)
            # Find worktree root first.
            iree_require_worktree
            clean_cache "${IREE_BAZEL_WORKTREE_DIR}/.iree-bazel-try"
            exit 0
            ;;
        --cache-status|--cache_status)
            # Find worktree root first.
            iree_require_worktree
            show_cache_status "${IREE_BAZEL_WORKTREE_DIR}/.iree-bazel-try"
            exit 0
            ;;
        --no_infer|--no_infer)
            INFER_DEPS=0
            shift
            ;;
        --retry)
            MAX_RETRIES="${2}"
            shift 2
            ;;
        --retry=*)
            MAX_RETRIES="${1#--retry=}"
            shift
            ;;
        -o|--output)
            OUTPUT_PATH="${2}"
            shift 2
            ;;
        --output=*)
            OUTPUT_PATH="${1#--output=}"
            shift
            ;;
        -x)
            LANGUAGE="${2}"
            shift 2
            ;;
        -e|--execute)
            INLINE_CODE="${INLINE_CODE}${2}"$'\n'
            shift 2
            ;;
        --execute=*)
            INLINE_CODE="${INLINE_CODE}${1#--execute=}"$'\n'
            shift
            ;;
        --dep)
            EXTRA_DEPS+=("${2}")
            shift 2
            ;;
        --dep=*)
            EXTRA_DEPS+=("${1#--dep=}")
            shift
            ;;
        --copt)
            COPTS+=("${2}")
            shift 2
            ;;
        --copt=*)
            COPTS+=("${1#--copt=}")
            shift
            ;;
        --linkopt)
            LINKOPTS+=("${2}")
            shift 2
            ;;
        --linkopt=*)
            LINKOPTS+=("${1#--linkopt=}")
            shift
            ;;
        --features)
            BAZEL_ARGS+=("--features=${2}")
            BAZEL_USER_FLAGS+=("--features=${2}")
            shift 2
            ;;
        --config=*|--features=*)
            BAZEL_ARGS+=("${1}")
            BAZEL_USER_FLAGS+=("${1}")
            shift
            ;;
        --)
            PARSING_PROGRAM_ARGS=1
            shift
            ;;
        -*)
            iree_error "Unknown option: ${1}"
            echo "Use --help for usage information."
            exit 1
            ;;
        *)
            SOURCE_FILES+=("${1}")
            shift
            ;;
    esac
done

# Find worktree root.
iree_require_worktree
cd "${IREE_BAZEL_WORKTREE_DIR}"
iree_ensure_configured

# Prepend default configs to bazel args.
iree_bazel_build_default_configs
BAZEL_ARGS=("${IREE_BAZEL_DEFAULT_CONFIGS[@]}" "${BAZEL_ARGS[@]}")

# Propagate copts and linkopts to bazel command line so they apply to all
# transitive dependencies, not just the snippet target.
for opt in "${COPTS[@]}"; do
    BAZEL_ARGS+=("--copt=${opt}")
done
for opt in "${LINKOPTS[@]}"; do
    BAZEL_ARGS+=("--linkopt=${opt}")
done

# Auto-fix ThinLTO backend compile issue: Bazel passes linker flags to the
# compiler during LTO backend compile, causing -Werror to fire on unused
# linker arguments. Suppress this automatically.
declare -a BAZEL_STARTUP_ARGS=()
HAS_CUSTOM_FLAGS=0
for flag in "${BAZEL_USER_FLAGS[@]}"; do
    if [[ "${flag}" == "--features=thin_lto" ]]; then
        BAZEL_ARGS+=("--copt=-Wno-unused-command-line-argument")
        break
    fi
done
if [[ ${#COPTS[@]} -gt 0 || ${#LINKOPTS[@]} -gt 0 || ${#BAZEL_USER_FLAGS[@]} -gt 0 ]]; then
    HAS_CUSTOM_FLAGS=1
fi

# Use a separate output_base when custom flags are present to avoid polluting
# the main Bazel server's analysis cache. This ensures that running
# iree-bazel-try with --copt/--features doesn't cause config invalidation
# for subsequent iree-bazel-build/test/run invocations.
# Also redirect workspace symlinks to avoid overwriting the main bazel-bin.
if [[ "${HAS_CUSTOM_FLAGS}" == "1" ]]; then
    BAZEL_TRY_OUTPUT_BASE="${IREE_BAZEL_WORKTREE_DIR}/.iree-bazel-try/output_base"
    mkdir -p "${BAZEL_TRY_OUTPUT_BASE}"
    BAZEL_STARTUP_ARGS+=("--output_base=${BAZEL_TRY_OUTPUT_BASE}")
    BAZEL_ARGS+=("--symlink_prefix=${IREE_BAZEL_WORKTREE_DIR}/.iree-bazel-try/bazel-")
    iree_debug "Using separate output_base for custom flags: ${BAZEL_TRY_OUTPUT_BASE}"
fi
# Ensure helper functions use the same Bazel startup args (output_base, etc.).
iree_bazel_set_startup_args "${BAZEL_STARTUP_ARGS[@]}"

# Determine input source.
HAS_STDIN=0
if [[ -z "${INLINE_CODE}" ]] && [[ ${#SOURCE_FILES[@]} -eq 0 ]]; then
    # Check if stdin has data.
    if [[ -t 0 ]]; then
        iree_error "No input provided. Specify files, use -e, or pipe to stdin."
        echo "Use --help for usage information."
        exit 1
    fi
    HAS_STDIN=1
fi

# Set up base directory (ignored via .gitignore).
TRY_BASE="${IREE_BAZEL_WORKTREE_DIR}/.iree-bazel-try"
CACHE_DIR="${TRY_BASE}/cache"
STAGING_DIR="${TRY_BASE}/staging_$$"

# Create staging directory for source files (needed for deps inference).
mkdir -p "${STAGING_DIR}"

# Cleanup staging on exit (cache slots are kept).
# Note: trap is set later, after lock acquisition, to include lock release.
cleanup_staging() {
    if [[ -n "${STAGING_DIR}" ]] && [[ -d "${STAGING_DIR}" ]]; then
        rm -rf "${STAGING_DIR}"
    fi
}

# Collect source files in staging directory.
declare -a BUILD_SRCS=()
declare -a STAGED_SOURCE_PATHS=()
declare -a HASH_SOURCE_PATHS=()
HAS_FILE_INPUT=0

# Read stdin into variable if needed (so we can scan it for auto-detection).
STDIN_CONTENT=""
if [[ "${HAS_STDIN}" == "1" ]]; then
    STDIN_CONTENT=$(cat)
fi

# Auto-detect C++ from includes if language not explicitly set.
# C++-only headers: llvm/, mlir/, iree/compiler/, iree/testing/, gtest/, gmock/, benchmark/
ALL_CODE="${INLINE_CODE}${STDIN_CONTENT}"
if [[ -z "${LANGUAGE}" ]]; then
    if echo "${ALL_CODE}" | grep -qE '#include\s*[<"](llvm/|mlir/|iree/compiler/|iree/testing/|gtest/|gmock/|benchmark/)'; then
        LANGUAGE="c++"
        iree_debug "Auto-detected C++ from includes"
    else
        LANGUAGE="c"
    fi
fi


# Handle inline code.
if [[ -n "${INLINE_CODE}" ]]; then
    if [[ "${LANGUAGE}" == "c++" ]]; then
        INLINE_FILE="${STAGING_DIR}/inline.cc"
    else
        INLINE_FILE="${STAGING_DIR}/inline.c"
    fi
    printf '%s' "${INLINE_CODE}" > "${INLINE_FILE}"
    BUILD_SRCS+=("$(basename "${INLINE_FILE}")")
    STAGED_SOURCE_PATHS+=("${INLINE_FILE}")
    HASH_SOURCE_PATHS+=("${INLINE_FILE}")
    iree_debug "Wrote inline code to ${INLINE_FILE}"
fi

# Handle stdin.
if [[ -n "${STDIN_CONTENT}" ]]; then
    if [[ "${LANGUAGE}" == "c++" ]]; then
        STDIN_FILE="${STAGING_DIR}/stdin.cc"
    else
        STDIN_FILE="${STAGING_DIR}/stdin.c"
    fi
    printf '%s' "${STDIN_CONTENT}" > "${STDIN_FILE}"
    BUILD_SRCS+=("$(basename "${STDIN_FILE}")")
    STAGED_SOURCE_PATHS+=("${STDIN_FILE}")
    HASH_SOURCE_PATHS+=("${STDIN_FILE}")
    iree_debug "Wrote stdin to ${STDIN_FILE}"
fi

if [[ ${#SOURCE_FILES[@]} -gt 0 ]]; then
    HAS_FILE_INPUT=1
fi
for src in "${SOURCE_FILES[@]}"; do
    if [[ ! -f "${src}" ]]; then
        iree_error "Source file not found: ${src}"
        exit 1
    fi
    # Copy to staging dir with #line for accurate diagnostics.
    staged_path="${STAGING_DIR}/$(basename "${src}")"
    escaped_src="${src//\\/\\\\}"
    escaped_src="${escaped_src//\"/\\\"}"
    {
        printf '#line 1 "%s"\n' "${escaped_src}"
        cat "${src}"
    } > "${staged_path}"
    BUILD_SRCS+=("$(basename "${src}")")
    STAGED_SOURCE_PATHS+=("${staged_path}")
    HASH_SOURCE_PATHS+=("${src}")
    iree_debug "Staged ${src} -> ${staged_path}"
done

# Determine if this is C++.
IS_CPP=0
if [[ "${LANGUAGE}" == "c++" ]]; then
    IS_CPP=1
else
    for src in "${BUILD_SRCS[@]}"; do
        case "${src}" in
            *.cc|*.cpp|*.cxx|*.C)
                IS_CPP=1
                break
                ;;
        esac
    done
fi

# Compute cache hash BEFORE deps inference.
# Hash is based on #includes + explicit deps + build options + language.
# File inputs also include content hash to avoid stale builds.
# This allows runs with same deps config to share BUILD.bazel analysis cache.
CACHE_HASH=$(compute_cache_hash HASH_SOURCE_PATHS EXTRA_DEPS COPTS LINKOPTS "${LANGUAGE}" BAZEL_USER_FLAGS "${HAS_FILE_INPUT}")
SLOT_DIR="${CACHE_DIR}/${CACHE_HASH}"
LOCK_FILE="${CACHE_DIR}/${CACHE_HASH}.lock"

# Ensure cache directory exists.
mkdir -p "${CACHE_DIR}"

# Acquire exclusive lock on this slot to prevent parallel runs from clobbering.
# Uses portable mkdir-based locking (flock is Linux-specific).
if ! _acquire_lock "${LOCK_FILE}" 60; then
    iree_error "Timeout waiting for lock on cache slot ${CACHE_HASH}"
    exit 1
fi
trap "_release_lock '${LOCK_FILE}'; cleanup_staging" EXIT
iree_debug "Acquired lock on slot ${CACHE_HASH}"

# Check cache AFTER acquiring lock (another process may have created it while we waited).
CACHE_HIT=0
if [[ -d "${SLOT_DIR}" ]] && [[ -f "${SLOT_DIR}/BUILD.bazel" ]]; then
    CACHE_HIT=1
    iree_debug "Cache hit: reusing slot ${CACHE_HASH} (skipping deps inference)"
fi

# Only infer deps on cache miss.
declare -A DEPS_MAP=()
declare -a DEPS=()

if [[ "${CACHE_HIT}" == "0" ]]; then
    # Infer deps from source files (expensive bazel query).
    declare -a INFERRED_DEPS=()
    if [[ "${INFER_DEPS}" == "1" ]]; then
        while IFS= read -r dep; do
            if [[ -n "${dep}" ]]; then
                INFERRED_DEPS+=("${dep}")
            fi
        done < <(infer_deps_from_sources "${STAGED_SOURCE_PATHS[@]}")

        if [[ ${#INFERRED_DEPS[@]} -gt 0 ]]; then
            iree_debug "Inferred deps: ${INFERRED_DEPS[*]}"
        fi
    fi

    # Build deps list (inferred + explicit, deduplicated).
    for dep in "${INFERRED_DEPS[@]}"; do
        if [[ -z "${DEPS_MAP[${dep}]:-}" ]]; then
            DEPS_MAP[${dep}]=1
            DEPS+=("${dep}")
        fi
    done

    for dep in "${EXTRA_DEPS[@]}"; do
        if [[ -z "${DEPS_MAP[${dep}]:-}" ]]; then
            DEPS_MAP[${dep}]=1
            DEPS+=("${dep}")
        fi
    done

    # If no deps at all, add base as minimum.
    if [[ ${#DEPS[@]} -eq 0 ]]; then
        DEPS+=("//runtime/src/iree/base")
    fi
fi

# Function to generate BUILD.bazel file.
generate_build_file() {
    local build_file="${1}"
    shift
    local -a deps=("$@")

    {
        echo '# Auto-generated by iree-bazel-try'
        echo ''
        echo 'cc_binary('
        echo '    name = "snippet",'
        echo '    testonly = True,'
        printf '    srcs = ['
        local first=1
        for src in "${BUILD_SRCS[@]}"; do
            if [[ "${first}" == "1" ]]; then
                first=0
            else
                printf ', '
            fi
            printf '"%s"' "${src}"
        done
        printf '],\n'
        printf '    deps = [\n'
        for dep in "${deps[@]}"; do
            printf '        "%s",\n' "${dep}"
        done
        printf '    ],\n'
        echo ')'
    } > "${build_file}"
}

# Set up cache slot.
if [[ "${CACHE_HIT}" == "0" ]]; then
    iree_debug "Cache miss: creating slot ${CACHE_HASH}"
    mkdir -p "${SLOT_DIR}"

    # Generate BUILD.bazel for new slot.
    BUILD_FILE="${SLOT_DIR}/BUILD.bazel"
    generate_build_file "${BUILD_FILE}" "${DEPS[@]}"

    iree_debug "Generated BUILD.bazel:"
    if [[ "${IREE_BAZEL_VERBOSE}" == "1" ]]; then
        cat "${BUILD_FILE}" | sed 's/^/    /' >&2
    fi
fi

# Copy source files from staging to cache slot.
for staged_file in "${STAGED_SOURCE_PATHS[@]}"; do
    cp "${staged_file}" "${SLOT_DIR}/"
done

# Touch the slot directory to update mtime for LRU tracking.
touch "${SLOT_DIR}"

# Enforce cache size limit (evict old slots if needed).
enforce_cache_limit "${CACHE_DIR}"

if [[ "${KEEP_TEMP}" == "1" ]]; then
    iree_info "Cache slot: ${SLOT_DIR}"
fi

# Build the target with retry logic.
TARGET="//.iree-bazel-try/cache/${CACHE_HASH}:snippet"
BUILD_LOG="${SLOT_DIR}/build.log"

do_build() {
    local BAZEL_BIN
    BAZEL_BIN=$(iree_get_bazel_command)
    if iree_is_verbose; then
        # Verbose mode: show everything, also save to log for retry logic.
        "${BAZEL_BIN}" "${BAZEL_STARTUP_ARGS[@]}" build "${TARGET}" "${BAZEL_ARGS[@]}" 9>&- 2>&1 | tee "${BUILD_LOG}"
        return "${PIPESTATUS[0]}"
    else
        # Quiet mode: capture output, only show on failure.
        # Force colors since we'll display to terminal on failure.
        if "${BAZEL_BIN}" "${BAZEL_STARTUP_ARGS[@]}" build --color=yes "${TARGET}" "${BAZEL_ARGS[@]}" 9>&- >"${BUILD_LOG}" 2>&1; then
            return 0
        else
            local exit_code=$?
            cat "${BUILD_LOG}" >&2
            return ${exit_code}
        fi
    fi
}

build_with_retry() {
    local attempt=1
    local current_deps=("${DEPS[@]}")

    while [[ ${attempt} -le ${MAX_RETRIES} ]]; do
        iree_debug "Build attempt ${attempt}/${MAX_RETRIES}"

        if do_build; then
            return 0
        fi

        # Build failed - try to find missing deps.
        if [[ "${INFER_DEPS}" == "0" ]]; then
            # User disabled inference, don't retry.
            return 1
        fi

        local new_deps_found=0
        while IFS= read -r new_dep; do
            if [[ -n "${new_dep}" ]] && [[ -z "${DEPS_MAP[${new_dep}]:-}" ]]; then
                iree_warn "Adding inferred dep: ${new_dep}"
                DEPS_MAP[${new_dep}]=1
                current_deps+=("${new_dep}")
                new_deps_found=1
            fi
        done < <(parse_build_errors "${BUILD_LOG}" "${IREE_BAZEL_WORKTREE_DIR}")

        if [[ "${new_deps_found}" == "0" ]]; then
            # No new deps to try, give up.
            iree_error "Build failed and no additional deps could be inferred"
            return 1
        fi

        # Regenerate BUILD file with new deps.
        generate_build_file "${BUILD_FILE}" "${current_deps[@]}"
        iree_debug "Regenerated BUILD.bazel with new deps"

        attempt=$((attempt + 1))
    done

    iree_error "Build failed after ${MAX_RETRIES} attempts"
    return 1
}

if [[ "${COMPILE_ONLY}" == "1" ]] || [[ -n "${OUTPUT_PATH}" ]]; then
    # Dry run: show what would be built and exit.
    if [[ "${DRY_RUN}" == "1" ]]; then
        iree_info "Command: bazel build ${TARGET} ${BAZEL_ARGS[*]}"
        iree_info "Deps: ${DEPS[*]}"
        exit 0
    fi

    iree_debug "Building ${TARGET}..."

    if ! build_with_retry; then
        exit 1
    fi

    # Copy output if requested.
    if [[ -n "${OUTPUT_PATH}" ]]; then
        BINARY_PATH=$(iree_bazel_get_binary_path "${TARGET}" "${BAZEL_ARGS[@]}")
        if [[ -n "${BINARY_PATH}" ]] && [[ -f "${BINARY_PATH}" ]]; then
            cp "${BINARY_PATH}" "${OUTPUT_PATH}"
            chmod +x "${OUTPUT_PATH}"
            iree_info "Binary saved to: ${OUTPUT_PATH}"
        else
            iree_error "Could not find built binary at ${BINARY_PATH}"
            exit 1
        fi
    fi

    if [[ "${COMPILE_ONLY}" == "1" ]]; then
        iree_debug "Build successful"
        exit 0
    fi
fi

# Run the target.
# Dry run: show what would be run and exit.
if [[ "${DRY_RUN}" == "1" ]]; then
    if [[ ${#PROGRAM_ARGS[@]} -gt 0 ]]; then
        iree_info "Command: bazel build ${TARGET} ${BAZEL_ARGS[*]} && <binary> ${PROGRAM_ARGS[*]}"
    else
        iree_info "Command: bazel build ${TARGET} ${BAZEL_ARGS[*]} && <binary>"
    fi
    iree_info "Deps: ${DEPS[*]}"
    iree_info "Run directory: ${IREE_BAZEL_ORIG_CWD}"
    exit 0
fi

iree_debug "Running ${TARGET}..."

# Build first (using retry logic).
if ! build_with_retry; then
    exit 1
fi

# Get the binary path and run from original directory.
BINARY_PATH=$(iree_bazel_get_binary_path "${TARGET}" "${BAZEL_ARGS[@]}")
if [[ -z "${BINARY_PATH}" ]] || [[ ! -x "${BINARY_PATH}" ]]; then
    iree_error "Could not find built binary at ${BINARY_PATH}"
    exit 1
fi

iree_debug "Binary: ${BINARY_PATH}"
iree_debug "Running from: ${IREE_BAZEL_ORIG_CWD}"

cd "${IREE_BAZEL_ORIG_CWD}"
exec "${BINARY_PATH}" "${PROGRAM_ARGS[@]}"
