#!/usr/bin/env bash
#
# jerboa — CLI entry point for the Jerboa Scheme environment
#

set -euo pipefail

# Auto-detect JERBOA_HOME as the parent of this script's directory
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
JERBOA_HOME="${JERBOA_HOME:-$(cd "$SCRIPT_DIR/.." && pwd)}"
export JERBOA_HOME

SCHEME="${SCHEME:-$JERBOA_HOME/.chez/bin/scheme}"
LIBDIRS="$JERBOA_HOME/lib"
export LIBDIRS
VERSION="$(tr -d '[:space:]' < "$JERBOA_HOME/VERSION" 2>/dev/null || true)"
VERSION="${VERSION:-0.2.0}"

if [ ! -x "$SCHEME" ]; then
    echo "Error: Chez Scheme not found at $SCHEME" >&2
    echo "Run 'make chez' from $JERBOA_HOME to build the repo-local Scheme." >&2
    exit 1
fi

# Colors for test output (disabled if not a terminal)
if [ -t 1 ]; then
    GREEN='\033[0;32m'
    RED='\033[0;31m'
    BOLD='\033[1m'
    RESET='\033[0m'
else
    GREEN='' RED='' BOLD='' RESET=''
fi

usage() {
    cat <<EOF
Usage: jerboa [command] [args...]

Commands:
  (no args)          Launch the Jerboa REPL
  repl               Launch the Jerboa REPL
  run <file> [args]  Run a Scheme script
  exec <file>        Run a self-contained script with a requires: header
                     (auto-installs declared dependencies before running)
  eval '<expr>'      Evaluate a single expression
  test [dir]         Discover and run test files (default: tests/)
  wasm <cmd> [args]  Wasm tooling: build, run, inspect, validate, package
  build              Run make build in the project directory
  install <gh-url>   Install a package (e.g. github.com/user/repo)
  uninstall <name>   Uninstall a package by name
  update [name]      Update one or all installed packages
  list               List installed packages
  expand <dir> [out] Expand a Jerboa package to pure Chez Scheme R6RS
  pkg <cmd> [args]   jpkg package manager (see: jerboa pkg --help)
  version            Print version info
  help               Print this help message

Environment:
  JERBOA_HOME        Project root (auto-detected from script location)
  SCHEME             Chez Scheme binary (default: scheme)
EOF
}

cmd_repl() {
    exec "$SCHEME" --libdirs "$LIBDIRS" --program <(cat <<'REPL'
(import (jerboa prelude) (std repl))
(jerboa-repl)
REPL
    )
}

cmd_run() {
    if [ $# -eq 0 ]; then
        echo "Error: jerboa run requires a file argument" >&2
        echo "Usage: jerboa run <file> [args...]" >&2
        exit 1
    fi
    local file="$1"
    shift
    if [ ! -f "$file" ]; then
        echo "Error: file not found: $file" >&2
        exit 1
    fi
    exec "$SCHEME" --libdirs "$LIBDIRS" --script "$file" "$@"
}

##
## parse_requires_header <file>
##
## Extract dependency URLs from a ";;; jerboa-package" header block.
## The header lives at the top of the file and looks like:
##
##     ;;; jerboa-package
##     ;;; name:    my-app
##     ;;; requires:
##     ;;;   github.com/alice/jerboa-foo
##     ;;;   github.com/bob/jerboa-bar
##     ;;;
##
## The block ends at the first line that is not a ";;;" comment, or at a
## bare ";;;" (blank comment) after the requires: key. Unknown keys are
## ignored. Any line that isn't "requires:" or an entry under it is
## skipped.
##
## Prints one URL per line. Prints nothing if no header is present.
parse_requires_header() {
    awk '
        BEGIN { in_block = 0; in_requires = 0 }
        # A line like ";;;" or ";;; " ends the requires sub-block.
        /^;;;[[:space:]]*$/ {
            if (in_block) { in_requires = 0; next }
            exit
        }
        # Non-comment line: header is over.
        !/^;;;/ {
            if (in_block) exit
            next
        }
        # Header marker
        /^;;;[[:space:]]*jerboa-package[[:space:]]*$/ { in_block = 1; next }
        # Only process comments inside the block
        in_block == 0 { next }
        # Key: value line
        /^;;;[[:space:]]*[A-Za-z_-]+:[[:space:]]*/ {
            # "requires:" with no value starts a sub-block
            if ($0 ~ /^;;;[[:space:]]*requires:[[:space:]]*$/) {
                in_requires = 1
                next
            }
            # Any other key: line exits the requires sub-block
            in_requires = 0
            next
        }
        # Entry line under requires:
        in_requires == 1 {
            line = $0
            sub(/^;;;[[:space:]]*/, "", line)
            sub(/[[:space:]]+$/, "", line)
            if (line != "") print line
        }
    ' "$1"
}

cmd_exec() {
    if [ $# -eq 0 ]; then
        echo "Error: jerboa exec requires a file argument" >&2
        echo "Usage: jerboa exec <file>" >&2
        exit 1
    fi
    local file="$1"
    shift
    if [ ! -f "$file" ]; then
        echo "Error: file not found: $file" >&2
        exit 1
    fi

    local -a deps=()
    while IFS= read -r dep; do
        [ -n "$dep" ] && deps+=("$dep")
    done < <(parse_requires_header "$file")

    if [ ${#deps[@]} -gt 0 ]; then
        local -a missing=()
        for dep in "${deps[@]}"; do
            local name
            name="$(basename "$dep")"
            if ! "$SCHEME" --libdirs "$LIBDIRS" --program <(cat <<CHECK
(import (jerboa registry))
(exit (if (package-installed? "$name") 0 1))
CHECK
            ) >/dev/null 2>&1; then
                missing+=("$dep")
            fi
        done

        if [ ${#missing[@]} -gt 0 ]; then
            echo "jerboa exec: $file declares ${#missing[@]} missing package(s):"
            for dep in "${missing[@]}"; do
                echo "  - $dep"
            done
            if [ "${JERBOA_EXEC_YES:-}" != "1" ]; then
                printf "Install now? [y/N] "
                read -r reply
                case "$reply" in
                    y|Y|yes|YES) ;;
                    *) echo "Aborted." >&2; exit 1 ;;
                esac
            fi
            for dep in "${missing[@]}"; do
                "$0" install "$dep" || {
                    echo "Failed to install $dep" >&2
                    exit 1
                }
            done
        fi
    fi

    exec "$SCHEME" --libdirs "$LIBDIRS" --script "$file" "$@"
}

cmd_eval() {
    if [ $# -eq 0 ]; then
        echo "Error: jerboa eval requires an expression" >&2
        echo "Usage: jerboa eval '<expr>'" >&2
        exit 1
    fi
    local expr="$1"
    "$SCHEME" --libdirs "$LIBDIRS" --program <(cat <<EVAL
(import (jerboa prelude))
$expr
EVAL
    )
}

cmd_test() {
    local test_dir="${1:-$JERBOA_HOME/tests}"
    if [ ! -d "$test_dir" ]; then
        echo "Error: test directory not found: $test_dir" >&2
        exit 1
    fi

    local files=()
    while IFS= read -r -d '' f; do
        files+=("$f")
    done < <(find "$test_dir" -name 'test-*.ss' -type f -print0 | sort -z)

    if [ ${#files[@]} -eq 0 ]; then
        echo "No test files (test-*.ss) found in $test_dir"
        exit 0
    fi

    local passed=0 failed=0 total=${#files[@]}
    echo -e "${BOLD}Running $total test file(s) from $test_dir${RESET}"
    echo ""

    for f in "${files[@]}"; do
        local name
        name="$(basename "$f")"
        if "$SCHEME" --libdirs "$LIBDIRS" --script "$f" 2>&1; then
            echo -e "  ${GREEN}PASS${RESET}  $name"
            ((passed++))
        else
            echo -e "  ${RED}FAIL${RESET}  $name"
            ((failed++))
        fi
    done

    echo ""
    echo -e "${BOLD}Results: $passed passed, $failed failed, $total total${RESET}"
    [ "$failed" -eq 0 ]
}

cmd_build() {
    exec make -C "$JERBOA_HOME" build
}

cmd_expand() {
    if [ $# -eq 0 ]; then
        echo "Error: jerboa expand requires a package directory" >&2
        echo "Usage: jerboa expand <package-dir> [output-dir]" >&2
        exit 1
    fi
    exec "$SCHEME" --libdirs "$LIBDIRS" --script "$JERBOA_HOME/tools/expand-package.ss" "$@"
}

cmd_install() {
    if [ $# -eq 0 ]; then
        echo "Error: jerboa install requires a GitHub path" >&2
        echo "Usage: jerboa install github.com/user/repo" >&2
        exit 1
    fi
    local gh_path="$1"
    "$SCHEME" --libdirs "$LIBDIRS" --program <(cat <<INSTALL
(import (jerboa registry))
(let ([entry (package-install! "$gh_path")])
  (display "Installed: ")
  (display "$gh_path")
  (newline))
INSTALL
    )
}

cmd_uninstall() {
    if [ $# -eq 0 ]; then
        echo "Error: jerboa uninstall requires a package name" >&2
        echo "Usage: jerboa uninstall <package-name>" >&2
        exit 1
    fi
    local name="$1"
    "$SCHEME" --libdirs "$LIBDIRS" --program <(cat <<UNINSTALL
(import (jerboa registry))
(package-uninstall! "$name")
(display "Uninstalled: ")
(display "$name")
(newline)
UNINSTALL
    )
}

cmd_update() {
    if [ $# -eq 0 ]; then
        # Update all packages
        "$SCHEME" --libdirs "$LIBDIRS" --program <(cat <<'UPDATEALL'
(import (except (chezscheme) make-hash-table hash-table? iota 1+ 1-)
        (jerboa runtime)
        (jerboa registry))
(let ([pkgs (installed-packages)])
  (if (null? pkgs)
    (display "No packages installed.\n")
    (for-each
      (lambda (e)
        (let ([name (hash-ref e "name" "")])
          (display (string-append "Updating " name "...\n"))
          (package-update! name)))
      pkgs)))
UPDATEALL
        )
    else
        local name="$1"
        "$SCHEME" --libdirs "$LIBDIRS" --program <(cat <<UPDATE
(import (jerboa registry))
(package-update! "$name")
(display "Updated: ")
(display "$name")
(newline)
UPDATE
        )
    fi
}

cmd_list() {
    "$SCHEME" --libdirs "$LIBDIRS" --program <(cat <<'LIST'
(import (except (chezscheme) make-hash-table hash-table? iota 1+ 1-)
        (jerboa runtime)
        (jerboa registry))
(let ([pkgs (installed-packages)])
  (if (null? pkgs)
    (display "No packages installed.\n")
    (for-each
      (lambda (e)
        (let ([name (hash-ref e "name" "")]
              [ver  (hash-ref e "version" "?")]
              [url  (hash-ref e "url" "")])
          (display (string-append "  " name " (" ver ") — " url "\n"))))
      pkgs)))
LIST
    )
}

cmd_wasm_build() {
    local backend="gc"
    local output=""
    local source=""
    local import_manifest=""
    local emit_ir=""
    local emit_analysis=""
    local emit_size_report=""
    local -a exports=()
    local compile_options=""
    local opt_level="0"
    local target="core"
    local debug_names="0"
    local unsafe_js_eval="0"
    local compiler_path=""
    local source_manifest=""
    local export_name=""
    local export_name_re='^[-A-Za-z0-9_+*/<>=!?$%&~^:.]+$'
    scheme_string_literal() {
        local text="$1"
        text="${text//\\/\\\\}"
        text="${text//\"/\\\"}"
        printf '"%s"' "$text"
    }
    validate_wasm_gc_export_name() {
        local name="$1"
        local flag="$2"
        if [ -z "$name" ]; then
            echo "Error: $flag requires a function name" >&2
            exit 1
        fi
        if [[ ! "$name" =~ $export_name_re ]]; then
            echo "Error: invalid Wasm-GC export name '$name'" >&2
            exit 1
        fi
    }
    while [ $# -gt 0 ]; do
        case "$1" in
            --backend)
                shift
                backend="${1:-}"
                ;;
            -o|--output)
                shift
                output="${1:-}"
                ;;
            --export)
                shift
                export_name="${1:-}"
                validate_wasm_gc_export_name "$export_name" "--export"
                exports+=("$export_name")
                ;;
            --exports)
                shift
                if [ -z "${1:-}" ]; then
                    echo "Error: --exports requires a comma-separated name list" >&2
                    exit 1
                fi
                IFS=',' read -r -a export_parts <<< "$1"
                for export_name in "${export_parts[@]}"; do
                    validate_wasm_gc_export_name "$export_name" "--exports"
                    exports+=("$export_name")
                done
                ;;
            --emit-import-manifest)
                shift
                import_manifest="${1:-}"
                if [ -z "$import_manifest" ]; then
                    echo "Error: --emit-import-manifest requires an output path" >&2
                    exit 1
                fi
                ;;
            --emit-ir)
                shift
                emit_ir="${1:-}"
                if [ -z "$emit_ir" ]; then
                    echo "Error: --emit-ir requires an output path" >&2
                    exit 1
                fi
                ;;
            --emit-analysis)
                shift
                emit_analysis="${1:-}"
                if [ -z "$emit_analysis" ]; then
                    echo "Error: --emit-analysis requires an output path" >&2
                    exit 1
                fi
                ;;
            --emit-size-report)
                shift
                emit_size_report="${1:-}"
                if [ -z "$emit_size_report" ]; then
                    echo "Error: --emit-size-report requires an output path" >&2
                    exit 1
                fi
                ;;
            --target)
                shift
                target="${1:-}"
                if [ "$target" != "core" ] && [ "$target" != "node" ] && [ "$target" != "browser" ]; then
                    echo "Error: --target expects core, node, or browser" >&2
                    exit 1
                fi
                ;;
            -O0)
                opt_level="0"
                ;;
            -O1)
                opt_level="1"
                ;;
            -O2)
                opt_level="2"
                ;;
            --debug-names)
                debug_names="1"
                ;;
            --unsafe-js-eval)
                unsafe_js_eval="1"
                ;;
            --compiler)
                shift
                compiler_path="${1:-}"
                if [ -z "$compiler_path" ]; then
                    echo "Error: --compiler requires a compiler Wasm path" >&2
                    exit 1
                fi
                ;;
            --source-manifest)
                shift
                source_manifest="${1:-}"
                if [ -z "$source_manifest" ]; then
                    echo "Error: --source-manifest requires a source manifest path" >&2
                    exit 1
                fi
                ;;
            -*)
                echo "Error: unsupported wasm build option '$1'" >&2
                exit 1
                ;;
            *)
                if [ -n "$source" ]; then
                    echo "Error: wasm build accepts one source file" >&2
                    exit 1
                fi
                source="$1"
                ;;
        esac
        shift
    done
    if [ "$backend" != "gc" ] && [ "$backend" != "wasm-gc" ]; then
        echo "Error: only --backend gc is currently supported" >&2
        exit 1
    fi
    if [ -z "$source" ]; then
        echo "Usage: jerboa wasm build --backend gc program.ss -o program.wasm" >&2
        exit 2
    fi
    if [ ! -f "$source" ]; then
        echo "Error: source file not found: $source" >&2
        exit 1
    fi
    if [ -z "$output" ]; then
        output="${source%.*}.wasm"
    fi
    compile_options="((opt-level $opt_level) (target $target)"
    if [ "${#exports[@]}" -gt 0 ]; then
        compile_options="$compile_options (exports ${exports[*]})"
    fi
    if [ "$debug_names" = "1" ]; then
        compile_options="$compile_options (debug-names #t)"
    fi
    if [ "$unsafe_js_eval" = "1" ]; then
        compile_options="$compile_options (unsafe-js-eval #t)"
    fi
    if [ -n "$compiler_path" ]; then
        if [ "$unsafe_js_eval" = "1" ]; then
            echo "Error: unsafe JS eval is not available for Wasm-GC compiler artifact builds" >&2
            exit 1
        fi
        if [ -n "$import_manifest" ] || [ -n "$emit_ir" ] || [ -n "$emit_analysis" ] || [ -n "$emit_size_report" ]; then
            echo "Error: --compiler cannot emit stage0 IR, analysis, size, or import-manifest artifacts" >&2
            exit 1
        fi
        compiler_args=(node "$JERBOA_HOME/support/wasm-gc/compiler-runner.mjs" --compiler "$compiler_path" --target "$target" "-O$opt_level")
        if [ -n "$source_manifest" ]; then
            compiler_args+=(--source-manifest "$source_manifest")
        fi
        if [ "$debug_names" = "1" ]; then
            compiler_args+=(--debug-names)
        fi
        if [ "${#exports[@]}" -gt 0 ]; then
            for export_name in "${exports[@]}"; do
                compiler_args+=(--export "$export_name")
            done
        fi
        compiler_args+=("$source" -o "$output")
        exec "${compiler_args[@]}"
    elif [ -n "$source_manifest" ]; then
        echo "Error: --source-manifest requires --compiler" >&2
        exit 1
    fi
    compile_options="$compile_options)"
    JERBOA_WASM_SOURCE="$source" JERBOA_WASM_OUTPUT="$output" JERBOA_WASM_OPTIONS="$compile_options" JERBOA_WASM_IMPORT_MANIFEST="$import_manifest" JERBOA_WASM_IR_OUTPUT="$emit_ir" JERBOA_WASM_ANALYSIS_OUTPUT="$emit_analysis" JERBOA_WASM_SIZE_REPORT_OUTPUT="$emit_size_report" \
        "$SCHEME" --libdirs "$LIBDIRS" --program "$JERBOA_HOME/support/wasm-gc/build-driver.ss"
}

cmd_wasm() {
    local sub="${1:-}"
    if [ $# -gt 0 ]; then shift; fi
    case "$sub" in
        build)
            cmd_wasm_build "$@"
            ;;
        validate)
            if [ $# -eq 0 ]; then
                echo "Usage: jerboa wasm validate module.wasm ..." >&2
                exit 2
            fi
            exec node "$JERBOA_HOME/support/wasm-gc/validate.mjs" "$@"
            ;;
        bench)
            if [ $# -eq 0 ]; then
                echo "Usage: jerboa wasm bench [--quick] [--engine node[,chromium,firefox,safari]] benchmarks/wasm-gc/manifest.json [report.json]" >&2
                exit 2
            fi
            exec node "$JERBOA_HOME/support/wasm-gc/benchmark-report.mjs" "$@"
            ;;
        package)
            exec node "$JERBOA_HOME/support/wasm-gc/package-runtime.mjs" "$@"
            ;;
        build-compiler)
            exec node "$JERBOA_HOME/support/wasm-gc/build-compiler-artifact.mjs" "$@"
            ;;
        self-host-check)
            exec node "$JERBOA_HOME/support/wasm-gc/self-host-check.mjs" "$@"
            ;;
        inspect)
            if [ $# -ne 1 ]; then
                echo "Usage: jerboa wasm inspect module.wasm" >&2
                exit 2
            fi
            node "$JERBOA_HOME/support/wasm-gc/check-abi.mjs" require "$1"
            node "$JERBOA_HOME/support/wasm-gc/check-imports.mjs" manifest "$1"
            ;;
        run)
            local backend="gc"
            if [ "${1:-}" = "--backend" ]; then
                shift
                backend="${1:-}"
                shift || true
            fi
            if [ "$backend" != "gc" ] && [ "$backend" != "wasm-gc" ]; then
                echo "Error: only --backend gc is currently supported" >&2
                exit 1
            fi
            if [ $# -eq 0 ]; then
                echo "Usage: jerboa wasm run [--backend gc] module.wasm [export [expected [args...]]]" >&2
                exit 2
            fi
            local file="$1"
            shift
            local export_name="${1:-main}"
            if [ $# -gt 0 ]; then shift; fi
            exec node "$JERBOA_HOME/support/wasm-gc/run-export.mjs" "$file" "$export_name" "$@"
            ;;
        help|--help|-h|"")
            cat <<EOF
Usage: jerboa wasm <command> [args...]

Commands:
  build --backend gc [--target core|node|browser] [-O0|-O1|-O2] [--debug-names] [--export name | --exports a,b] [--emit-ir path] [--emit-analysis path] [--emit-size-report path] [--emit-import-manifest path] [--unsafe-js-eval] [--compiler compiler.wasm [--source-manifest sources.json]] program.ss -o program.wasm
  run [--backend gc] module.wasm [export [expected [args...]]]
  inspect module.wasm
  validate module.wasm ...
  bench [--quick] [--engine node[,chromium,firefox,safari]] [--allow-missing-engine] benchmarks/wasm-gc/manifest.json [report.json]
  package [-o output-dir] [--compiler compiler.wasm --self-hosting-evidence evidence.json] [--version version]
  build-compiler --source compiler.ss [--source-manifest compiler-sources.json] -o stage1.wasm [--manifest stage1.manifest.json] [--flattened-source flattened.ss] [--source-audit-report audit.json]
  self-host-check --compiler stage1.wasm --compiler-source compiler.ss [--compiler-source-manifest compiler-sources.json] (--fixture fixture.ss [--fixture-source-manifest fixture-sources.json] [--fixture-target core|node|browser] [--fixture-O0|--fixture-O1|--fixture-O2] [--fixture-debug-names] [--fixture-export name] ... | --fixture-manifest fixtures.json) -o self-hosting-evidence.json
EOF
            ;;
        *)
            echo "Error: unknown wasm command '$sub'" >&2
            exit 1
            ;;
    esac
}

cmd_version() {
    echo "jerboa $VERSION"
    echo "home: $JERBOA_HOME"
    "$SCHEME" --version 2>&1 || true
}

# --- Main dispatch ---

case "${1:-}" in
    ""|repl)
        cmd_repl
        ;;
    run)
        shift
        cmd_run "$@"
        ;;
    exec)
        shift
        cmd_exec "$@"
        ;;
    eval)
        shift
        cmd_eval "$@"
        ;;
    test)
        shift
        cmd_test "$@"
        ;;
    wasm)
        shift
        cmd_wasm "$@"
        ;;
    build)
        cmd_build
        ;;
    expand)
        shift
        cmd_expand "$@"
        ;;
    install)
        shift
        cmd_install "$@"
        ;;
    uninstall)
        shift
        cmd_uninstall "$@"
        ;;
    update)
        shift
        cmd_update "$@"
        ;;
    list)
        cmd_list
        ;;
    pkg|jpkg)
        shift
        exec "$SCHEME" --libdirs "$LIBDIRS" --script "$JERBOA_HOME/tools/jpkg-main.ss" "$@"
        ;;
    version|--version|-v)
        cmd_version
        ;;
    help|--help|-h)
        usage
        ;;
    *)
        echo "Error: unknown command '$1'" >&2
        echo "" >&2
        usage >&2
        exit 1
        ;;
esac
