#
# Just configuration.
#
# https://just.systems/man/en
#

# Compiler targets.
compiler-targets := "aarch64-apple-darwin aarch64-unknown-linux-gnu x86_64-unknown-linux-gnu"

# Default to the first Python binary on `PATH`.
python-binary := "python"

# MSC version.
package-version := `uv version --short`

# List recipes.
help:
    just --list

# Prepare the toolchain.
prepare-toolchain:
    # Add compiler targets.
    rustup target add {{compiler-targets}}
    # Prepare the virtual environment.
    if [[ -z "${CI:-}" ]]; then \
        uv sync --all-extras --python {{python-binary}} && uv run --python {{python-binary}} maturin develop --release; \
    else \
        uv sync --all-extras --locked --python {{python-binary}}; \
    fi

# Start the Python REPL.
start-repl: prepare-toolchain
    # Start the Python REPL.
    uv run python

# Start the MSC Explorer backend.
start-explorer: prepare-toolchain
    uv run python -m multistorageclient.commands.cli.main explorer

# Run static analysis (format, lint, type check).
analyze: prepare-toolchain
    # Remove analysis artifacts.
    rm -rf .reports/ruff.json
    # Format.
    if [[ -z "${CI:-}" ]]; then ruff format; else ruff format --check; fi
    # Lint.
    if [[ -z "${CI:-}" ]]; then ruff check --fix; else ruff check --output-file .reports/ruff.json --output-format gitlab; fi
    # Type check.
    uv run pyright

# Stop storage systems.
stop-storage-systems:
    # Stop storage systems.
    #
    # Azurite's process commands are `node` instead of `azurite`. Find by port instead.
    for PID in $(lsof -i :10000-10002 -c aisnode -c fake-gcs-server -c versitygw -t); do kill -s KILL $PID; done
    # Remove sandbox directories.
    -rm -rf .{aistore,azurite,fake-gcs-server,versitygw}/sandbox

# Start storage systems.
start-storage-systems: stop-storage-systems
    # Create sandbox directories.
    mkdir --parents .{aistore,azurite,fake-gcs-server,versitygw}/sandbox
    # Set up AIStore cluster (config generation).
    .aistore/prepare_sandbox.sh

    # Ports used by storage systems:
    # - AIStore         -> 51080 (proxy), 51081 (target)
    # - Azurite         -> 10000-10002
    # - fake-gcs-server -> 4443
    # - versitygw       -> 7070

    # Start an AIStore proxy.
    cd .aistore/sandbox/0 && aisnode -config="config/ais.json" -local_config="config/ais_local.json" -role=proxy -ntargets=1 > log/stdout.log 2>&1 &
    # Wait for the proxy to be ready before starting the target.
    timeout 30s bash -c 'until curl -sf "http://localhost:51080/v1/daemon?what=snode" >/dev/null 2>&1; do sleep 0.5; done' || { echo "ERROR: AIStore proxy failed to start"; exit 1; }
    # Start an AIStore target.
    cd .aistore/sandbox/1 && aisnode -config="config/ais.json" -local_config="config/ais_local.json" -role=target > log/stdout.log 2>&1 &
    # Wait for the target to join the cluster.
    timeout 30s bash -c 'until [ $(curl -s "http://localhost:51080/v1/cluster?what=smap" 2>/dev/null | grep -o "\"daemon_type\":\"target\"" | wc -l) -ge 1 ]; do sleep 0.5; done' || { echo "ERROR: AIStore target failed to join"; exit 1; }

    # Start Azurite.
    cd .azurite/sandbox && azurite --disableTelemetry --inMemoryPersistence --silent --skipApiVersionCheck &

    # Start fake-gcs-server.
    cd .fake-gcs-server/sandbox && TZ="UTC" fake-gcs-server -backend memory -log-level error -scheme http &

    # Start versitygw.
    cd .versitygw/sandbox && versitygw --access root-access-key --health /health --iam-dir . --quiet --secret root-secret-key posix . &

    # Wait for the AIStore cluster to be operational by creating a bucket and deleting it.
    timeout 60s bash -c 'until curl -sf -X POST "http://localhost:51080/v1/buckets/msc-readiness-check?provider=ais" -H "Content-Type: application/json" -d "{\"action\":\"create-bck\"}" >/dev/null 2>&1; do sleep 1; done' || { echo "ERROR: AIStore cluster not operational"; exit 1; }
    curl -sf -X DELETE "http://localhost:51080/v1/buckets/msc-readiness-check?provider=ais" -H "Content-Type: application/json" -d '{"action":"destroy-bck"}' >/dev/null 2>&1 || true

    # Wait for Azurite.
    timeout 30s bash -c "until netcat --zero localhost 10000; do sleep 1; done"

    # Wait for fake-gcs-server.
    timeout 30s bash -c "until curl --fail --output /dev/null --silent http://localhost:4443/_internal/healthcheck; do sleep 1; done"

    # Wait for versitygw.
    timeout 30s bash -c "until curl --fail --output /dev/null --silent http://localhost:7070/health; do sleep 1; done"
    # Create a user (for credentials refresh tests. Root user is hidden from credentials APIs).
    versitygw admin --access root-access-key --endpoint-url http://localhost:7070 --secret root-secret-key create-user --access access-key --role admin --secret secret-key

# Stop telemetry systems.
stop-telemetry-systems:
    # Stop telemetry systems.
    for PID in $(lsof -c grafana -c mimir -c tempo -t); do kill -s KILL $PID; done
    # Remove sandbox directories.
    -rm -rf .{grafana,mimir,tempo}/sandbox

# Start telemetry systems.
start-telemetry-systems: stop-telemetry-systems
    # Create sandbox directories.
    mkdir --parents .{grafana,mimir,tempo}/sandbox
    # Set up Grafana sandbox directory.
    #
    # Grafana expects some included data files to be present.
    mkdir --parents .grafana/sandbox/{data,logs,plugins}
    ln --symbolic $(dirname $(dirname $(command -v grafana)))/share/grafana/{conf,public} .grafana/sandbox
    # Set up Tempo sandbox directory.
    mkdir --parents .tempo/sandbox/{trace,wal}

    # Ports used by telemetry systems:
    # - Grafana -> 3000 (HTTP), Random (gRPC)
    # - Mimir   -> 7946 (Gossip), 8080 (HTTP), 9095 (gRPC)
    # - Tempo   -> 8081 (HTTP), 9096 (gRPC)

    # Start Grafana.
    cd .grafana/sandbox && grafana server --config ../grafana.ini &
    # Start Mimir.
    cd .mimir/sandbox && mimir -config.file ../mimir.yaml &
    # Start Tempo.
    cd .tempo/sandbox && tempo -config.file ../tempo.yaml &

    # Wait for Grafana.
    #
    # An error log about `stat /proc` failing is expected.
    timeout 30s bash -c "until curl --fail --output /dev/null --silent http://localhost:3000/api/health; do sleep 1; done"
    # Wait for Mimir.
    timeout 60s bash -c "until curl --fail --output /dev/null --silent http://localhost:8080/ready; do sleep 1; done"
    # Wait for Tempo.
    timeout 60s bash -c "until curl --fail --output /dev/null --silent http://localhost:8081/ready; do sleep 1; done"

# Run unit tests.
run-unit-tests: prepare-toolchain start-storage-systems && stop-storage-systems
    # Remove test artifacts.
    rm -rf .reports/unit
    # Run Rust unit tests.
    cd rust && cargo test
    # Run Python unit tests.
    uv run pytest --cov --cov-report term --cov-report html --cov-report xml --durations 10 --junit-xml .reports/unit/pytest.xml --numprocesses auto --timeout 120 --ignore tests/test_multistorageclient/unit/contrib/test_ray.py

# Run load tests. For dummy load generation when experimenting with telemetry.
run-load-tests: prepare-toolchain start-storage-systems && stop-storage-systems
    # Run load tests.
    uv run pytest --minutes 30 tests/test_multistorageclient/load/local.py

# Run Ray tests.
run-ray-tests: prepare-toolchain
    # Run Ray unit tests.
    uv run pytest --cov --cov-append --cov-report term --cov-report html --cov-report xml --timeout 120 tests/test_multistorageclient/unit/contrib/test_ray.py

# Create package archives.
package: prepare-toolchain
    # Clean Rust build artifacts to prevent stale object files.
    cd rust && cargo clean
    # Remove package archives.
    rm -rf dist
    # Create a source distribution.
    uv build --sdist
    # Create platform-specific wheels.
    #
    # Link Apple SDKs for cross-compilation, setup environment variables to correct wheel names.
    #
    # https://github.com/rust-cross/cargo-zigbuild#caveats
    # https://github.com/PyO3/maturin/discussions/2586#discussioncomment-13095109
    # https://github.com/PyO3/maturin/blob/d95faa64f2c9971820314d228da9a7e71d2e4b87/src/build_context.rs#L1160
    set -e; \
    for TARGET in {{compiler-targets}}; do \
        case "$TARGET" in \
            (aarch64-apple-darwin) \
                export MACOSX_DEPLOYMENT_TARGET=$APPLE_SDK_VERSION_AARCH64 \
                export SDKROOT=$APPLE_SDK_AARCH64 \
                ;; \
            (*) \
                unset MACOSX_DEPLOYMENT_TARGET \
                unset SDKROOT \
                ;; \
        esac; \
        PYTHON_VERSION=$(uv run {{python-binary}} -c "import sys; print(f'python{sys.version_info.major}.{sys.version_info.minor}')") && \
        echo "Building for target $TARGET with $PYTHON_VERSION" && \
        env --unset _PYTHON_HOST_PLATFORM uv run maturin build --interpreter $PYTHON_VERSION --out dist --release --target $TARGET --zig; \
    done

# Release build.
build: analyze run-unit-tests run-ray-tests
    if [[ -n "${CI:-}" ]]; then just package; fi

# Run E2E tests.
run-e2e-tests: prepare-toolchain
    # Remove test artifacts.
    rm -rf .{aws,gcloud,oci,teleport}/sandbox .reports/e2e
    # Create sandbox directories.
    mkdir --parents .{aws,gcloud,oci,teleport}/sandbox .teleport/sandbox/tbot .teleport/sandbox/workload-identity-jwt/{aws,azure,gcp,oci}
    # Get Teleport credentials.
    if [[ -n "${GITHUB_ACTIONS:-}" ]]; then \
        tbot start --config .teleport/tbot.yaml --join-method github --token rg-multi-storage-client-beta-github-actions; \
    elif [[ -n "${GITLAB_CI:-}" ]]; then \
        tbot start --config .teleport/tbot.yaml --join-method gitlab --token rg-multi-storage-client-beta-gitlab-ci-cd; \
    else \
        (bao token renew || bao login -method oidc -path oidc role=namespace-reader) && \
        bao kv get -field id_bkp beta/teleport-bound-keypair > .teleport/sandbox/tbot/id_bkp && \
        tbot start --config .teleport/tbot.yaml --join-method bound_keypair --token rg-multi-storage-client-beta-bound-keypair; \
    fi
    # Get static credentials.
    AWS_CONFIG_FILE=.aws/config aws --profile aws secretsmanager get-secret-value --output text --query SecretString --secret-id multi-storage-client-beta > .aws/sandbox/multi-storage-client-beta.json
    # Create AWS credentials files.
    cat .aws/credentials.template | GCS_HMAC_ACCESS_KEY_ID=$(jq --raw-output ".GCS_HMAC_ACCESS_KEY_ID" .aws/sandbox/multi-storage-client-beta.json) GCS_HMAC_SECRET_ACCESS_KEY=$(jq --raw-output ".GCS_HMAC_SECRET_ACCESS_KEY" .aws/sandbox/multi-storage-client-beta.json) S8K_ACCESS_KEY_ID=$(jq --raw-output ".S8K_ACCESS_KEY_ID" .aws/sandbox/multi-storage-client-beta.json) S8K_SECRET_ACCESS_KEY=$(jq --raw-output ".S8K_SECRET_ACCESS_KEY" .aws/sandbox/multi-storage-client-beta.json) envsubst > .aws/sandbox/credentials
    jq --raw-output ".CLOUDFRONT_PRIVATE_KEY" .aws/sandbox/multi-storage-client-beta.json > .aws/sandbox/id_rsa
    # Create GCP credentials files.
    gcloud iam workload-identity-pools create-cred-config projects/328033609117/locations/global/workloadIdentityPools/multi-storage-client-beta/providers/multi-storage-client-beta --credential-source-file .teleport/sandbox/workload-identity-jwt/gcp/jwt_svid --credential-source-type text --output-file .gcloud/sandbox/credentials.json
    # Create OCI credentials files.
    openssl genrsa -out .oci/sandbox/id_rsa 4096
    curl \
        --data-urlencode "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \
        --data-urlencode "public_key=$(openssl rsa -in .oci/sandbox/id_rsa -outform PEM -pubout | sed '1d;$d' | tr --delete '\n')" \
        --data-urlencode "requested_token_type=urn:oci:token-type:oci-upst" \
        --data-urlencode "subject_token=$(cat .teleport/sandbox/workload-identity-jwt/oci/jwt_svid)" \
        --data-urlencode "subject_token_type=jwt" \
        --fail-with-body \
        --location \
        --user $(jq --raw-output ".OCI_TOKEN_EXCHANGE_CLIENT_ID" .aws/sandbox/multi-storage-client-beta.json):$(jq --raw-output ".OCI_TOKEN_EXCHANGE_CLIENT_SECRET" .aws/sandbox/multi-storage-client-beta.json) \
        https://idcs-1a8b8478607a4a329637387e744475be.identity.oraclecloud.com:443/oauth2/v1/token | jq --join-output ".token" > .oci/sandbox/session_token
    # Run E2E tests.
    AWS_CONFIG_FILE=.aws/config \
    AWS_SHARED_CREDENTIALS_FILE=.aws/sandbox/credentials \
    CLOUDFRONT_DOMAIN=$(jq --raw-output ".CLOUDFRONT_DOMAIN" .aws/sandbox/multi-storage-client-beta.json) \
    CLOUDFRONT_KEY_PAIR_ID=$(jq --raw-output ".CLOUDFRONT_KEY_PAIR_ID" .aws/sandbox/multi-storage-client-beta.json) \
    CLOUDFRONT_PRIVATE_KEY_PATH=.aws/sandbox/id_rsa \
    AZURE_FEDERATED_TOKEN_FILE=.teleport/sandbox/workload-identity-jwt/azure/jwt_svid \
    GOOGLE_APPLICATION_CREDENTIALS=.gcloud/sandbox/credentials.json \
    OCI_CONFIG_FILE=.oci/config \
    HF_TOKEN=$(jq --raw-output ".HF_TOKEN" .aws/sandbox/multi-storage-client-beta.json) \
    PANOPTES_CLIENT_ID=$(jq --raw-output ".PANOPTES_CLIENT_ID" .aws/sandbox/multi-storage-client-beta.json) \
    PANOPTES_CLIENT_CREDENTIAL=$(jq --raw-output ".PANOPTES_CLIENT_CREDENTIAL" .aws/sandbox/multi-storage-client-beta.json) \
    MSC_CONFIG=.msc/config.yaml \
    PATH=.rclone:${PATH} \
    uv run pytest --durations 10 --junit-xml .reports/e2e/pytest.xml --numprocesses auto tests/test_multistorageclient/e2e

# Run minimal verification without any optional dependencies.
run-minimal-verification:
    # Prepare the virtual environment without optional dependencies.
    if [[ -z "${CI:-}" ]]; then uv sync --python {{python-binary}}; else uv sync --locked --python {{python-binary}}; fi
    # Run minimal verification unit tests.
    uv run pytest tests/test_multistorageclient/unit/test_minimal.py
