#!/usr/bin/env bash
set -euo pipefail

# block-run: Execute code files block-by-block, like a notebook
# Blocks are separated by blank lines (two+ newlines)
# Dispatches to language-specific wrappers in block-run.d/wrappers/

SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
WRAPPER_DIR="$SCRIPT_DIR/block-run.d/wrappers"
CONFIG_FILE="$SCRIPT_DIR/block-run.d/config"

usage() {
    cat <<EOF
Usage: $(basename "$0") [options] <script>

Run a script block-by-block, showing output after each block.
Blocks are separated by blank lines.

Options:
  --hierarchical    Use '## ' headers to separate blocks instead of blank lines
  --help            Show this help message

The interpreter is determined from the shebang line.
Wrappers live in: $WRAPPER_DIR
EOF
    exit "${1:-0}"
}

die() {
    echo "error: $*" >&2
    exit 1
}

# Extract binary path from shebang
# Handles: #!/path/to/bin, #!/usr/bin/env bin, #!/path/to/bin args
parse_shebang() {
    local shebang="$1"

    # Remove #! prefix
    shebang="${shebang#\#!}"
    shebang="${shebang# }"  # trim leading space if present

    # Handle /usr/bin/env case
    if [[ "$shebang" =~ ^/usr/bin/env[[:space:]]+([^[:space:]]+) ]]; then
        local cmd="${BASH_REMATCH[1]}"
        # Resolve to full path
        if command -v "$cmd" &>/dev/null; then
            command -v "$cmd"
        else
            echo "$cmd"
        fi
        return
    fi

    # Direct path - extract just the binary (first word)
    echo "${shebang%% *}"
}

# Find wrapper for a given binary
find_wrapper() {
    local binary="$1"
    local basename="${binary##*/}"

    # 1. Check config for exact path match
    if [[ -f "$CONFIG_FILE" ]]; then
        local wrapper
        wrapper=$(grep "^${binary}=" "$CONFIG_FILE" 2>/dev/null | cut -d= -f2 | head -1)
        if [[ -n "$wrapper" && -x "$WRAPPER_DIR/$wrapper" ]]; then
            echo "$WRAPPER_DIR/$wrapper"
            return 0
        fi
    fi

    # 2. Check for basename match in wrappers dir
    if [[ -x "$WRAPPER_DIR/$basename" ]]; then
        echo "$WRAPPER_DIR/$basename"
        return 0
    fi

    # 3. No wrapper found
    return 1
}

# Split content into blocks (separated by blank lines)
# Outputs each block as a null-terminated string for safe handling
split_blocks_blank_lines() {
    local content="$1"
    local current_block=""
    local in_block=false

    while IFS= read -r line || [[ -n "$line" ]]; do
        if [[ -z "$line" ]]; then
            # Empty line
            if [[ "$in_block" == true && -n "$current_block" ]]; then
                # End of block - output it
                printf '%s\0' "$current_block"
                current_block=""
                in_block=false
            fi
        else
            # Non-empty line
            if [[ "$in_block" == true ]]; then
                current_block+=$'\n'"$line"
            else
                current_block="$line"
                in_block=true
            fi
        fi
    done <<< "$content"

    # Output final block if any
    if [[ -n "$current_block" ]]; then
        printf '%s\0' "$current_block"
    fi
}

# Split content into blocks (separated by ## headers)
split_blocks_hierarchical() {
    local content="$1"
    local current_block=""
    local first=true

    while IFS= read -r line || [[ -n "$line" ]]; do
        if [[ "$line" =~ ^##\  ]]; then
            # Header line - start new block
            if [[ -n "$current_block" ]]; then
                printf '%s\0' "$current_block"
            fi
            current_block="$line"
            first=false
        else
            # Regular line
            if [[ -n "$current_block" ]]; then
                current_block+=$'\n'"$line"
            elif [[ -n "$line" ]]; then
                # Content before first header
                current_block="$line"
            fi
        fi
    done <<< "$content"

    # Output final block if any
    if [[ -n "$current_block" ]]; then
        printf '%s\0' "$current_block"
    fi
}

main() {
    local script=""
    local split_mode="blank_lines"

    # Parse arguments
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --hierarchical)
                split_mode="hierarchical"
                shift
                ;;
            --help|-h)
                usage 0
                ;;
            -*)
                die "unknown option: $1"
                ;;
            *)
                [[ -z "$script" ]] || die "multiple scripts specified"
                script="$1"
                shift
                ;;
        esac
    done

    [[ -n "$script" ]] || die "no script specified"
    [[ -f "$script" ]] || die "script not found: $script"

    # Read shebang
    local shebang
    shebang=$(head -1 "$script")
    [[ "$shebang" =~ ^#! ]] || die "no shebang found in $script"

    # Parse binary from shebang
    local binary
    binary=$(parse_shebang "$shebang")
    [[ -n "$binary" ]] || die "could not parse shebang: $shebang"

    # Find wrapper
    local wrapper
    if ! wrapper=$(find_wrapper "$binary"); then
        die "no wrapper found for: $binary (basename: ${binary##*/})"
    fi

    # Read content (skip shebang)
    local content
    content=$(tail -n +2 "$script")

    # Split into blocks
    local blocks=()
    if [[ "$split_mode" == "hierarchical" ]]; then
        while IFS= read -r -d '' block; do
            blocks+=("$block")
        done < <(split_blocks_hierarchical "$content")
    else
        while IFS= read -r -d '' block; do
            blocks+=("$block")
        done < <(split_blocks_blank_lines "$content")
    fi

    [[ ${#blocks[@]} -gt 0 ]] || die "no blocks found in script"

    # Dispatch to wrapper
    exec "$wrapper" --binary "$binary" -- "${blocks[@]}"
}

main "$@"
