# raisfast common commands
#
# Usage: just <recipe>
# Help:  just --list

set dotenv-load

# ── Database Backend Configuration ────────────────────────────────
#
# Override defaults via environment variables:
#   RAISFAST_DB         — "sqlite" | "postgres" | "mysql"
#   RAISFAST_DB_URL     — connection string for dev DB
#   RAISFAST_TEST_DB_URL — connection string for test DB
#
# Or just edit the default below.
#
# Switch backend:  RAISFAST_DB=mysql just test-all

db        := env_var_or_default("RAISFAST_DB", "sqlite")

# Derive connection strings from `db` if not explicitly set.
default_db_url := if db == "sqlite" { "sqlite:storage/db/raisfast.db?mode=rwc" } \
                  else if db == "mysql" { "mysql://root:root@localhost:3306/raisfast" } \
                  else { "postgres://postgres:postgres@localhost:5432/raisfast" }

default_test_db_url := if db == "sqlite" { "sqlite::memory:" } \
                       else if db == "mysql" { "mysql://root:root@localhost:3306/raisfast_test" } \
                       else { "postgres://postgres:postgres@localhost:5432/raisfast_test" }

db_url        := env_var_or_default("RAISFAST_DB_URL", default_db_url)
test_db_url   := env_var_or_default("RAISFAST_TEST_DB_URL", default_test_db_url)

# Per-backend sqlx offline cache directory (avoids cross-contamination).
# `cargo sqlx prepare` writes to .sqlx/, so we keep per-backend copies in
# .sqlx-{db}/ and symlink .sqlx → .sqlx-{db} before each compile.
sqlx_cache := ".sqlx-" + db

# Schema file path for the active database backend.
schema_file := if db == "sqlite" { "migrations/sqlite/schema.sqlite.sql" } else if db == "postgres" { "migrations/postgres/schema.postgres.sql" } else { "migrations/mysql/schema.mysql.sql" }

# Feature flags derived from backend.
features     := "db-" + db + " plugin-js plugin-rhai search-tantivy payment-all tunnel mcp cron-system integration-stream integration-imap kb-qdrant"
features_csv := "db-" + db + ",plugin-js,plugin-rhai,search-tantivy,payment-all,tunnel,mcp,cron-system,integration-stream,integration-imap,kb-qdrant"

# Tests run in parallel on SQLite; PG/MySQL share a single test DB so they run
# serially to avoid DDL/catalog races during concurrent schema apply.
test_threads := if db == "sqlite" { "" } else { "-- --test-threads=1" }

# ── Default ───────────────────────────────────────────────────────

default:
    @just --list

# ── sqlx cache symlink helper ─────────────────────────────────────

# Link .sqlx → .sqlx-{db} so sqlx macros read the correct backend cache.
# Called automatically before every compile/test recipe.
_link-cache:
    #!/usr/bin/env bash
    set -euo pipefail
    CACHE="{{sqlx_cache}}"
    if [ -d "$CACHE" ]; then
        rm -rf .sqlx
        ln -s "$CACHE" .sqlx
    elif [ -d .sqlx ] && [ ! -L .sqlx ]; then
        echo ">> WARNING: .sqlx exists but $CACHE does not. Run: just db-prepare"
    fi

# ── Build ─────────────────────────────────────────────────────────

# Check compilation
check *FLAGS: _link-cache
    SQLX_OFFLINE=true DATABASE_URL={{db_url}} cargo check --no-default-features --features "{{features}}" {{FLAGS}}

# Build release binary
build *FLAGS: _link-cache
    SQLX_OFFLINE=true DATABASE_URL={{db_url}} cargo build --release --no-default-features --features "{{features}}" {{FLAGS}}

# Build release binary with Admin UI (auto-detects frontend/admin)
build-full *FLAGS: _link-cache
    #!/usr/bin/env bash
    set -euo pipefail
    if [ -d "frontend/admin" ]; then
        echo ">> Building Admin UI from source..."
        cd frontend/admin && npm ci
        npm run build
        cd ../..
        rm -rf adminui
        cp -r frontend/admin/dist adminui
        echo ">> Admin UI built and copied to adminui/"
    else
        echo ">> frontend/admin not found, using existing adminui/ as-is"
    fi
    SQLX_OFFLINE=true DATABASE_URL={{db_url}} cargo build --release --no-default-features --features "{{features}}" {{FLAGS}}

# Build the visitor widget (frontend/widget) into dist/widget.js.
# The backend serves it at /widget/widget.js (WIDGET_DIR default ./frontend/widget/dist).
widget-build:
    cd frontend/widget && npm run build

# ── Code Quality ──────────────────────────────────────────────────

# Format code
fmt:
    cargo fmt

# Check formatting
fmt-check:
    cargo fmt --check

# Lint (including tests)
lint: _link-cache
    SQLX_OFFLINE=true DATABASE_URL={{db_url}} cargo clippy --tests --no-default-features --features "{{features}}" -- -D warnings

# Full quality check (fmt + lint)
qa: fmt-check lint

# ── Tests ─────────────────────────────────────────────────────────

# Run all raisfast tests (excludes bore-cli e2e tests that need a bore server)
# ID_ENCODING=false keeps tests hermetic: `set dotenv-load` exports the dev
# server's .env (ID_ENCODING=true) into recipes, which breaks tests that pass
# plain-digit ids through `parse_id`.
test *FLAGS: _link-cache
    SQLX_OFFLINE=true ID_ENCODING=false DATABASE_URL={{db_url}} RAISFAST_TEST_DB_URL={{test_db_url}} cargo test -p raisfast --no-default-features --features "{{features}}" {{FLAGS}} {{test_threads}}

# Run unit tests only
test-unit: _link-cache
    SQLX_OFFLINE=true ID_ENCODING=false DATABASE_URL={{db_url}} RAISFAST_TEST_DB_URL={{test_db_url}} cargo test -p raisfast --lib --no-default-features --features "{{features}}" {{test_threads}}

# Run integration tests only
test-integration: _link-cache
    SQLX_OFFLINE=true ID_ENCODING=false DATABASE_URL={{db_url}} RAISFAST_TEST_DB_URL={{test_db_url}} cargo test -p raisfast --test api --no-default-features --features "{{features}}" {{test_threads}}

# Run the chat plugin's JS unit tests (services logic against the sdk mock,
# no kernel/DB). Tests live in extensions/plugins/chat/test/ (dev source only,
# never packaged into the .rafapp bundle).
test-chat-plugin:
    npm test --prefix extensions/plugins/chat

# ── Database ──────────────────────────────────────────────────────

# Create database (if needed) and load schema
db-init:
    #!/usr/bin/env bash
    set -euo pipefail
    DB="{{db}}"
    DB_URL="{{db_url}}"
    SCHEMA="{{schema_file}}"
    if [ "$DB" = "sqlite" ]; then
        mkdir -p storage/db
        sqlite3 "$(echo "$DB_URL" | sed 's/sqlite://;s/?.*//')" < "$SCHEMA"
    elif [ "$DB" = "postgres" ]; then
        psql "postgresql://postgres:postgres@localhost:5432/postgres" -tc "SELECT 1 FROM pg_database WHERE datname = 'raisfast'" | grep -q 1 || \
            psql "postgresql://postgres:postgres@localhost:5432/postgres" -c "CREATE DATABASE raisfast"
        psql "$DB_URL" -f "$SCHEMA"
    else
        mysql -u root -proot -e "CREATE DATABASE IF NOT EXISTS raisfast"
        mysql -u root -proot raisfast < "$SCHEMA"
    fi
    echo ">> Database initialized ($DB)"

# Recreate database (dangerous: deletes existing data)
db-reset:
    #!/usr/bin/env bash
    set -euo pipefail
    DB="{{db}}"
    DB_URL="{{db_url}}"
    if [ "$DB" = "sqlite" ]; then
        rm -f "$(echo "$DB_URL" | sed 's/sqlite://;s/?.*//')"
    elif [ "$DB" = "postgres" ]; then
        psql "postgresql://postgres:postgres@localhost:5432/postgres" -c "DROP DATABASE IF EXISTS raisfast"
    else
        mysql -u root -proot -e "DROP DATABASE IF EXISTS raisfast"
    fi
    just db-init

# Run CLI migrations
db-migrate:
    DATABASE_URL={{db_url}} cargo run --no-default-features --features "{{features}}" -- db migrate

# Salvage a corrupted SQLite database (SQLITE_CORRUPT) via .recover —
# keeps a timestamped backup, then integrity-checks + VACUUMs the result.
# STOP the dev server first. Damaged rows/objects may be dropped; follow
# up with `just db-init` to restore missing schema objects.
# Usage: just db-recover (sqlite backend only)
db-recover:
    #!/usr/bin/env bash
    set -euo pipefail
    DB="{{db}}"
    if [ "$DB" != "sqlite" ]; then
        echo ">> db-recover only applies to the sqlite backend (current: $DB)" >&2
        exit 1
    fi
    DB_FILE="$(echo '{{db_url}}' | sed 's/sqlite://;s/?.*//')"
    if [ ! -f "$DB_FILE" ]; then
        echo ">> database file not found: $DB_FILE" >&2
        exit 1
    fi
    if command -v lsof >/dev/null 2>&1; then
        if [ -n "$(lsof -t "$DB_FILE" 2>/dev/null)" ]; then
            echo ">> a process still holds $DB_FILE open — stop the dev server first:" >&2
            lsof "$DB_FILE" >&2 || true
            exit 1
        fi
    elif [ -f "$DB_FILE-wal" ]; then
        echo ">> cannot verify (lsof missing) and a -wal file exists — stop the dev server first." >&2
        exit 1
    fi
    STAMP="$(date +%Y%m%d%H%M%S)"
    echo ">> recovering $DB_FILE ..."
    sqlite3 "$DB_FILE" ".recover" | sqlite3 "${DB_FILE}.recovered"
    mv "$DB_FILE" "${DB_FILE}.corrupt.${STAMP}.bak"
    rm -f "$DB_FILE-shm"
    mv "${DB_FILE}.recovered" "$DB_FILE"
    sqlite3 "$DB_FILE" "VACUUM;"
    echo ">> integrity check:"
    sqlite3 "$DB_FILE" "PRAGMA integrity_check;"
    echo ">> done. corrupt backup: ${DB_FILE}.corrupt.${STAMP}.bak"
    echo ">> if schema objects are missing, run: just db-init"

# Backup database
db-backup:
    DATABASE_URL={{db_url}} cargo run --no-default-features --features "{{features}}" -- db backup ./backups

# Generate sqlx offline query metadata for production AND test code.
# cargo sqlx prepare writes to .sqlx/, then we move it to .sqlx-{db}/.
db-prepare:
    #!/usr/bin/env bash
    set -euo pipefail
    DB="{{db}}"
    TEST_URL="{{test_db_url}}"
    SCHEMA="{{schema_file}}"
    CACHE_DIR="{{sqlx_cache}}"
    rm -rf .sqlx
    rm -rf "$CACHE_DIR"
    if [ "$DB" = "sqlite" ]; then
        TMPDB="$(mktemp -d)/raisfast_prepare.db"
        sqlite3 "$TMPDB" < "$SCHEMA"
        SQLX_OFFLINE=false DATABASE_URL="sqlite:$TMPDB" cargo sqlx prepare --workspace -- --tests --no-default-features --features "{{features}}"
        rm -rf "$(dirname "$TMPDB")"
    else
        SQLX_OFFLINE=false DATABASE_URL="$TEST_URL" cargo sqlx prepare --workspace -- --tests --no-default-features --features "{{features}}"
    fi
    mv .sqlx "$CACHE_DIR"
    ln -sf "$CACHE_DIR" .sqlx
    echo ">> sqlx cache written to $CACHE_DIR ($(ls "$CACHE_DIR" | wc -l | tr -d ' ') queries)"

# Recreate the test database (SQLite uses :memory:, no setup needed)
test-db-init:
    #!/usr/bin/env bash
    set -euo pipefail
    DB="{{db}}"
    TEST_URL="{{test_db_url}}"
    SCHEMA="{{schema_file}}"
    if [ "$DB" = "sqlite" ]; then
        echo "SQLite uses :memory: — no test DB setup needed"
    elif [ "$DB" = "postgres" ]; then
        psql "postgresql://postgres:postgres@localhost:5432/postgres" -c "DROP DATABASE IF EXISTS raisfast_test"
        psql "postgresql://postgres:postgres@localhost:5432/postgres" -c "CREATE DATABASE raisfast_test"
        psql "$TEST_URL" -f "$SCHEMA"
    else
        mysql -u root -proot -e "DROP DATABASE IF EXISTS raisfast_test"
        mysql -u root -proot -e "CREATE DATABASE raisfast_test"
        mysql -u root -proot raisfast_test < "$SCHEMA"
    fi

# ── Content Types ─────────────────────────────────────────────────

# Generate TypeScript types from content-type TOML files.
#   just ct-types                           # all CTs → stdout
#   just ct-types chat_conversation         # one CT → stdout
#   just ct-types chat_conversation -o frontend/chat/src/types/chat.ts
# CT dir override: CONTENT_TYPE_DIR (default ./extensions/content_types).
# App CTs are materialized as {singular}.toml — use the flat singular name (e.g. "chat_conversation"), not "group/singular".
ct-types *ARGS: _link-cache
    SQLX_OFFLINE=true DATABASE_URL={{db_url}} cargo run --no-default-features --features "{{features}}" -- ct types {{ARGS}}

# Generate TypeScript types from plugin `[[routes]]` declarations.
#   just plugin-types                        # all plugins → stdout
#   just plugin-types chat                   # one plugin → stdout
#   just plugin-types chat -o frontend/chat/src/types/routes.ts
# Plugin dir override: PLUGIN_DIR (default ./extensions/plugins).
# Routes without [[routes.input]]/[[routes.output]] appear with unknown payloads —
# declare params/fields in the manifest to opt into full typing.
plugin-types *ARGS: _link-cache
    SQLX_OFFLINE=true DATABASE_URL={{db_url}} cargo run --no-default-features --features "{{features}}" -- plugin types {{ARGS}}

# ── Run ───────────────────────────────────────────────────────────

# Install cargo-watch (auto-recompile on file change)
install-watch:
    cargo install cargo-watch

# Start development server (online mode: validates SQL against live DB at compile time)
dev:
    SQLX_OFFLINE=false DATABASE_URL={{db_url}} cargo run --no-default-features --features "{{features}}"

# Start development server with auto-reload on code change.
# -w crates:   only watch source code (skip frontend/, .sqlx-*/, etc.)
# --delay 1s:  debounce rapid saves into one rebuild
# --no-restart: don't kill/restart if the binary exits on its own
dev-watch:
    SQLX_OFFLINE=false DATABASE_URL={{db_url}} RUST_LOG=info cargo watch -w crates --delay 1s --no-restart -x "run --no-default-features --features {{features_csv}}"

# ── Backend-Specific Checks ───────────────────────────────────────

# Check compilation with PostgreSQL
pg-check:
    RAISFAST_DB=postgres just check

# Check compilation with MySQL
mysql-check:
    RAISFAST_DB=mysql just check

# Check compilation with SQLite
sqlite-check:
    RAISFAST_DB=sqlite just check

# ── Full CI Pipeline ──────────────────────────────────────────────

# CI: fmt → lint → test (ensure all checks pass; includes chat plugin JS tests)
ci: fmt-check lint test test-chat-plugin

# One-shot: reset test DB → regenerate sqlx cache → run all tests
test-all: test-db-init db-prepare test

# ── Deploy ────────────────────────────────────────────────────────

fly_target := "x86_64-unknown-linux-musl"
fly_image := "raisfast-fly"

# Install cross (Rust cross-compilation tool)
install-cross:
    cargo install cross --git https://github.com/cross-rs/cross

# Cross-compile Linux binary for fly.io
build-cross:
    @echo "Cross-compiling for Linux via cross..."
    cross build --release --features "{{features}}" --target {{fly_target}}

# Deploy pre-built binary to fly.io (skip compilation)
deploy-fly:
    @echo "Building Docker image..."
    docker build --platform linux/amd64 -t {{fly_image}} -f deploy/fly/Dockerfile .
    @echo "Deploying to fly.io..."
    fly deploy --local-only -c deploy/fly/fly.toml --image {{fly_image}}
