#!/usr/bin/env bash
# Mint the four accounts the mesh runs as, and write their keys to mesh/.env.
#
#   ./mesh/provision.sh
#
# Idempotent, and it does NOT rotate a key it does not have to. A zk_ key is
# stored only as a sha256 hash and cannot be read back, so the naive version
# minted a fresh key every run -- which silently invalidates a key you may be
# using elsewhere, and made re-provisioning unsafe on any real account. Instead
# it hashes the key already in mesh/.env and compares: matches are kept as-is,
# and only an account with no key, or one whose key we have lost, gets a new
# one. Re-running on an unchanged mesh is a no-op.
#
# Why this reaches into the API container instead of calling the REST API:
# minting a key needs either a session for that user or admin rights, and the
# dev admin password is not in the repo (init.sql carries only its bcrypt hash).
# So it calls the same provisioning helpers the admin endpoint calls --
# provision_user() and assign_api_key() from dashboard.services.provisioning --
# rather than routing around them. The accounts it makes are the same shape an
# admin-created or Google-SSO account would be.
set -euo pipefail

MESH_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
MARKETPLACE_DIR="${MARKETPLACE_DIR:-$(cd "$MESH_DIR/../../zak-marketplace" 2>/dev/null && pwd || true)}"
API_URL="${API_URL:-http://localhost:8000}"
ENV_FILE="$MESH_DIR/.env"

# The account behind node4. Overridable, but it defaults to a mesh-local address
# rather than your real one: provisioning rotates the API key of whatever
# account it lands on, and pointing that at the account you sign into the
# dashboard with would invalidate a key you may be using elsewhere.
OWNER_EMAIL="${MESH_OWNER_EMAIL:-mine@mesh.local}"
OWNER_NAME="${MESH_OWNER_NAME:-Me (node4)}"
OWNER_CREDITS="${MESH_OWNER_CREDITS:-500}"
PEER_CREDITS="${MESH_PEER_CREDITS:-25}"

if [ -z "$MARKETPLACE_DIR" ] || [ ! -f "$MARKETPLACE_DIR/compose.yaml" ]; then
  echo "error: cannot find zak-marketplace (looked beside the zc checkout)." >&2
  echo "       set MARKETPLACE_DIR=/path/to/zak-marketplace" >&2
  exit 1
fi

if ! curl -fsS --max-time 5 "$API_URL/health" >/dev/null 2>&1 &&
   ! curl -fsS --max-time 5 "$API_URL/docs" >/dev/null 2>&1; then
  echo "error: no marketplace API at $API_URL." >&2
  echo "       start it first:  (cd $MARKETPLACE_DIR && docker compose up -d api)" >&2
  exit 1
fi

echo "==> provisioning 4 accounts via $MARKETPLACE_DIR"

# -T: no TTY, so stdout stays a clean pipe rather than gaining CR line endings
# that would end up inside the key values written to .env.
# Hand the keys we already hold to the container so it can recognise them and
# leave them alone. Sent as values, never written anywhere by the snippet.
EXISTING_ARGS=""
if [ -f "$ENV_FILE" ]; then
  for n in 1 2 3 4; do
    v=$(grep "^MESH_KEY_NODE${n}=" "$ENV_FILE" 2>/dev/null | cut -d= -f2-)
    [ -n "$v" ] && EXISTING_ARGS="$EXISTING_ARGS -e MESH_EXISTING_NODE${n}=$v"
  done
fi

# shellcheck disable=SC2086  # EXISTING_ARGS is a deliberate list of -e flags
OUTPUT=$(docker compose -f "$MARKETPLACE_DIR/compose.yaml" exec -T \
  $EXISTING_ARGS \
  -e MESH_OWNER_EMAIL="$OWNER_EMAIL" \
  -e MESH_OWNER_NAME="$OWNER_NAME" \
  -e MESH_OWNER_CREDITS="$OWNER_CREDITS" \
  -e MESH_PEER_CREDITS="$PEER_CREDITS" \
  api python3 - <<'PY'
import asyncio, hashlib, os
from sqlalchemy import select
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine

from dashboard.models.user import User
from dashboard.services.provisioning import assign_api_key, provision_user

OWNER_CREDITS = float(os.environ["MESH_OWNER_CREDITS"])
PEER_CREDITS = float(os.environ["MESH_PEER_CREDITS"])

# node4 last so a partial run still leaves the three providers consistent.
ACCOUNTS = [
    ("node1", "alice@mesh.local", "Alice (node1)", PEER_CREDITS),
    ("node2", "bob@mesh.local", "Bob (node2)", PEER_CREDITS),
    ("node3", "carol@mesh.local", "Carol (node3)", PEER_CREDITS),
    ("node4", os.environ["MESH_OWNER_EMAIL"], os.environ["MESH_OWNER_NAME"],
     OWNER_CREDITS),
]


async def main():
    engine = create_async_engine(os.environ["DATABASE_URL"])
    factory = async_sessionmaker(engine, expire_on_commit=False)
    rows = []
    async with factory() as db:
        for node, email, username, credits in ACCOUNTS:
            found = (await db.execute(
                select(User).where(User.email == email))).scalar_one_or_none()
            if found is None:
                user = await provision_user(
                    db, email=email, username=username,
                    initial_credits=credits, role="user",
                    # A real password, not the SSO placeholder: it makes these
                    # accounts signable-into in the dashboard UI, which is the
                    # only way to see the mesh from a provider's own screen.
                    password="mesh-dev-password",
                )
                state = "created"
            else:
                # Do NOT reset the balance. After a job has run, the balance is
                # the result this mesh exists to show; overwriting it on every
                # re-provision would erase the evidence.
                user, state = found, "reused"
            # Reuse the key we already hold for this node, if it is still the
            # one the account carries. Only mint when we have nothing usable --
            # rotating a live key is destructive and must not be a side effect
            # of re-running provisioning.
            held = os.environ.get(f"MESH_EXISTING_{node.upper()}", "")
            if held and user.api_key_hash == hashlib.sha256(
                    held.encode()).hexdigest():
                key, key_state = held, "key kept"
            else:
                key = assign_api_key(user)
                key_state = "key minted" if not user.api_key_hash else "key rotated"
            await db.flush()
            rows.append((node, email, user.zakuro_user_id, key,
                         f"{state}/{key_state}",
                         f"{float(user.credits_balance):.2f}"))
        await db.commit()
    await engine.dispose()
    for r in rows:
        print("\t".join(r))

asyncio.run(main())
PY
)

if [ -z "$OUTPUT" ]; then
  echo "error: provisioning produced no output; nothing written to .env" >&2
  exit 1
fi

# Written fresh each run rather than appended: a stale MESH_KEY_* left behind
# from an earlier run would be silently preferred by compose over the new one.
{
  echo "# Generated by mesh/provision.sh -- do not edit, do not commit."
  echo "# Each key is zk_{zakuro_user_id}_{hex}; the account it names is the"
  echo "# node's OWNER (who gets paid). Regenerate with ./mesh/provision.sh."
  echo
} > "$ENV_FILE"

printf '%-7s %-22s %-12s %-9s %s\n' NODE OWNER BILLING-ID CREDITS STATE
while IFS=$'\t' read -r node email zid key state credits; do
  printf '%-7s %-22s %-12s %-9s %s\n' "$node" "$email" "$zid" "$credits" "$state"
  echo "MESH_KEY_$(echo "$node" | tr '[:lower:]' '[:upper:]')=$key" >> "$ENV_FILE"
done <<< "$OUTPUT"

chmod 600 "$ENV_FILE"
echo
echo "==> wrote 4 keys to mesh/.env (mode 600)"
echo "    next:  docker compose -f mesh/compose.yaml up -d --build"
