#!/usr/bin/env bash
# ----------------------------------------------------------------------------
# AliasVault dev app starter.
#
# Use this to start AliasVault apps in dev mode with a preconfigured port
# setup: every service runs from source on a single, consistent set of ports,
# so the apps always know where to find each other.
#
# Usage:
#   ./scripts/dev.sh                 # interactive menu (pick an app)
#   ./scripts/dev.sh <app>           # start one app: api | client | admin | smtp | taskrunner | ext | mobile
#   ./scripts/dev.sh db-start        # start (only) the dev database
#   ./scripts/dev.sh db-stop         # stop & remove this instance's dev database
#   ./scripts/dev.sh ports           # print the resolved port map and exit
# ----------------------------------------------------------------------------
set -euo pipefail

# --- Resolve repo paths --------------------------------------------------------
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
cd "$ROOT_DIR"

# --- Colors (no-op if not a TTY) ----------------------------------------------
if [ -t 1 ]; then
  BOLD=$'\033[1m'; DIM=$'\033[2m'; GREEN=$'\033[32m'; CYAN=$'\033[36m'
  YELLOW=$'\033[33m'; RED=$'\033[31m'; NC=$'\033[0m'
else
  BOLD=""; DIM=""; GREEN=""; CYAN=""; YELLOW=""; RED=""; NC=""
fi
info()  { printf "%s==>%s %s\n" "$CYAN" "$NC" "$1"; }
warn()  { printf "%s!  %s%s\n" "$YELLOW" "$1" "$NC"; }
die()   { printf "%sError:%s %s\n" "$RED" "$NC" "$1" >&2; exit 1; }

LOCAL_ENV="$ROOT_DIR/dev.env"

# --- Config defaults -----------------------------------------------------------
DEFAULT_BASE_PORT=5100 # first port of the block; every service counts up from here
DEFAULT_PORT_STRIDE=10 # how far apart consecutive instances are spaced

# Write the dev.env template, with AV_INSTANCE pinned to the given value.
seed_local_env() {
  cat > "$LOCAL_ENV" <<ENV
# ----------------------------------------------------------------------------
# AliasVault dev instance config — generated by scripts/dev.sh, gitignored.
#
# AV_INSTANCE picks which block of ports this machine uses. Bump it if the
# default ports are already taken on your machine (another stack, another dev,
# something else on those ports). Each step moves every port up by ${DEFAULT_PORT_STRIDE}:
#
#     AV_INSTANCE=0  ->  ${DEFAULT_BASE_PORT}..$((DEFAULT_BASE_PORT + 8))   (default)
#     AV_INSTANCE=1  ->  $((DEFAULT_BASE_PORT + DEFAULT_PORT_STRIDE))..$((DEFAULT_BASE_PORT + DEFAULT_PORT_STRIDE + 8))
#     AV_INSTANCE=2  ->  $((DEFAULT_BASE_PORT + 2 * DEFAULT_PORT_STRIDE))..$((DEFAULT_BASE_PORT + 2 * DEFAULT_PORT_STRIDE + 8))
#     ...
# ----------------------------------------------------------------------------
AV_INSTANCE=$1

AV_BASE_PORT=$DEFAULT_BASE_PORT
AV_PORT_STRIDE=$DEFAULT_PORT_STRIDE

# UseDebugEncryptionKey for the Blazor client. 'true' uses a fixed debug
# encryption key so local logins skip the slow key derivation, speeds up dev.
# Note: this can conflict with some things like password verification steps.
# Switch to 'false' to use the actual user unlock flow compatible with production.
AV_USE_DEBUG_ENCRYPTION_KEY=true
ENV
}

# --- Load port config ----------------------------------------------------------
# First run: create a dev.env set to instance 0.
if [ ! -f "$LOCAL_ENV" ]; then
  seed_local_env 0
  info "Created dev.env (instance 0). Bump AV_INSTANCE there if the default ports are in use."
fi

# shellcheck disable=SC1091
set -a
. "$LOCAL_ENV"
set +a

# Precedence: dev.env > built-in defaults.
AV_INSTANCE="${AV_INSTANCE:-0}"
AV_BASE_PORT="${AV_BASE_PORT:-$DEFAULT_BASE_PORT}"
AV_PORT_STRIDE="${AV_PORT_STRIDE:-$DEFAULT_PORT_STRIDE}"
# Default true for pre-existing dev.env files that predate this setting.
AV_USE_DEBUG_ENCRYPTION_KEY="${AV_USE_DEBUG_ENCRYPTION_KEY:-true}"
case "$AV_INSTANCE" in (*[!0-9]*|"") die "AV_INSTANCE must be a non-negative integer (got '$AV_INSTANCE').";; esac
case "$AV_USE_DEBUG_ENCRYPTION_KEY" in true|false) ;; (*) die "AV_USE_DEBUG_ENCRYPTION_KEY must be 'true' or 'false' (got '$AV_USE_DEBUG_ENCRYPTION_KEY').";; esac

# --- Resolve effective ports ---------------------------------------------------
SERVICE_COUNT=10   # block width (offsets 0..9); DB sits last so it ends in 9
case "$AV_PORT_STRIDE" in (*[!0-9]*|"") die "AV_PORT_STRIDE must be a positive integer (got '$AV_PORT_STRIDE').";; esac
[ "$AV_PORT_STRIDE" -ge "$SERVICE_COUNT" ] || die "AV_PORT_STRIDE ($AV_PORT_STRIDE) must be >= $SERVICE_COUNT so instances don't overlap."
case "$AV_BASE_PORT" in (*[!0-9]*|"") die "AV_BASE_PORT must be a positive integer (got '$AV_BASE_PORT').";; esac

BLOCK_BASE=$(( AV_BASE_PORT + AV_INSTANCE * AV_PORT_STRIDE ))
# Dev only serves HTTP, so each web app reserves a single port.
API_HTTP=$((    BLOCK_BASE + 0 ))
CLIENT_HTTP=$(( BLOCK_BASE + 1 ))
ADMIN_HTTP=$((  BLOCK_BASE + 2 ))
EXT_PORT=$((    BLOCK_BASE + 3 ))
EXPO_PORT=$((   BLOCK_BASE + 4 ))
# DB sits at the last offset so its port always ends in 9 (e.g. 5109) — easy to
# remember. Offsets 5..8 are left free as spare slots inside the block.
DB_PORT=$((     BLOCK_BASE + 9 ))

# Always one dev DB container, named 'aliasvault-dev'. AV_INSTANCE only shifts
# the published port (above), not the container identity — so install.sh and any
# other tooling can find it by a single, stable project name.
DB_PROJECT="aliasvault-dev"
DB_COMPOSE="$ROOT_DIR/dockerfiles/docker-compose.dev.yml"
CONN_STR="Host=localhost;Port=$DB_PORT;Database=aliasvault;Username=aliasvault;Password=password;Maximum Pool Size=20;Minimum Pool Size=1"

# --- Helpers -------------------------------------------------------------------
RULE="────────────────────────────────────────────────────────────"

print_ports() {
  printf "%sAliasVault dev — instance %s%s (base %s, stride %s → block %s..%s)\n" \
    "$BOLD" "$AV_INSTANCE" "$NC" "$AV_BASE_PORT" "$AV_PORT_STRIDE" "$BLOCK_BASE" "$((BLOCK_BASE + SERVICE_COUNT - 1))"
  printf "  %-14s http://localhost:%s\n" "API"         "$API_HTTP"
  printf "  %-14s http://localhost:%s\n" "Client"      "$CLIENT_HTTP"
  printf "  %-14s http://localhost:%s\n" "Admin"       "$ADMIN_HTTP"
  printf "  %-14s localhost:%s\n"      "Postgres DB"    "$DB_PORT"
  printf "  %-14s :%s\n"                             "Browser ext" "$EXT_PORT"
  printf "  %-14s :%s\n"                             "Mobile"      "$EXPO_PORT"
}

# Print a clear, scannable header before a service takes over the terminal, so
# it's obvious at a glance which service it is and which ports it's on.
banner() {
  local title="$1"; shift
  printf "\n%s%s%s\n" "$CYAN" "$RULE" "$NC"
  printf "%s  ▶ %s%s   %sinstance %s%s\n" "$BOLD" "$title" "$NC" "$DIM" "$AV_INSTANCE" "$NC"
  local line
  for line in "$@"; do
    printf "      %s\n" "$line"
  done
  printf "%s%s%s\n\n" "$CYAN" "$RULE" "$NC"
}

require() { command -v "$1" >/dev/null 2>&1 || die "'$1' is required but not installed."; }

# Generate the Blazor client's dev config.
write_client_dev_settings() {
  local target="$ROOT_DIR/apps/server/AliasVault.Client/wwwroot/appsettings.Development.json"
  cat > "$target" <<JSON
{
    "ApiUrl": "http://localhost:$API_HTTP",
    "PrivateEmailDomains": ["example.tld", "example2.tld", "disabled.tld"],
    "HiddenPrivateEmailDomains": ["disabled.tld"],
    "SupportEmail": "support@example.tld",
    "PublicRegistrationEnabled": "true",
    "DeploymentMode": "dev",
    "UseDebugEncryptionKey": "$AV_USE_DEBUG_ENCRYPTION_KEY",
    "CryptographyOverrideType": "Argon2Id",
    "CryptographyOverrideSettings": "{\"DegreeOfParallelism\":1,\"MemorySize\":1024,\"Iterations\":1}"
}
JSON
  info "Wrote client dev config → ApiUrl http://localhost:$API_HTTP (UseDebugEncryptionKey=$AV_USE_DEBUG_ENCRYPTION_KEY)"
}

db_running() {
  [ -n "$(AV_DB_PORT="$DB_PORT" docker compose -f "$DB_COMPOSE" -p "$DB_PROJECT" ps --status running -q 2>/dev/null)" ]
}

start_db() {
  require docker
  banner "Database — $DB_PROJECT" "Postgres   localhost:$DB_PORT"
  # --wait blocks until the container is healthy, so callers (and CI) can assume
  # the database is ready to accept connections once this returns.
  AV_DB_PORT="$DB_PORT" \
    docker compose -f "$DB_COMPOSE" -p "$DB_PROJECT" up -d --build --wait --wait-timeout 60
  info "Database ready: $CONN_STR"
}

stop_db() {
  require docker
  info "Stopping dev database '$DB_PROJECT' ..."
  AV_DB_PORT="$DB_PORT" \
    docker compose -f "$DB_COMPOSE" -p "$DB_PROJECT" down
}

# Toggle: stop the dev database if it's running, otherwise start it.
toggle_db() {
  require docker
  if db_running; then
    stop_db
  else
    start_db
  fi
}

# Run a .NET app with ports + DB driven by env/CLI flags.
# Binds to 0.0.0.0 so the service is reachable from other devices on the LAN
# (e.g. a phone running the mobile app), not just from localhost.
run_dotnet() {
  local proj="$1" http="$2"
  require dotnet
  banner "$proj" \
    "HTTP     http://localhost:$http" \
    "Database localhost:$DB_PORT"
  ASPNETCORE_ENVIRONMENT="Development" \
  ConnectionStrings__AliasServerDbContext="$CONN_STR" \
    dotnet watch --project "$ROOT_DIR/apps/server/$proj" --no-launch-profile \
      --urls "http://0.0.0.0:$http"
}

# Dev-only secrets. These are static fake values that are used only in dev.
DEV_JWT_KEY="${JWT_KEY:-12345678901234567890123456789012}"
DEV_DATA_PROTECTION_CERT_PASS="${DATA_PROTECTION_CERT_PASS:-Development}"
DEV_ADMIN_PASSWORD_HASH="${ADMIN_PASSWORD_HASH:-AQAAAAIAAYagAAAAEKWfKfa2gh9Z72vjAlnNP1xlME7FsunRznzyrfqFte40FToufRwa3kX8wwDwnEXZag==}"
DEV_ADMIN_PASSWORD_GENERATED="${ADMIN_PASSWORD_GENERATED:-2030-01-01T00:00:00Z}"
DEV_PRIVATE_EMAIL_DOMAINS="${PRIVATE_EMAIL_DOMAINS:-example.tld,example2.tld,aliasvault.net,disabled.tld}"
DEV_HIDDEN_PRIVATE_EMAIL_DOMAINS="${HIDDEN_PRIVATE_EMAIL_DOMAINS:-disabled.tld}"

start_api() {
  JWT_KEY="$DEV_JWT_KEY" \
  DATA_PROTECTION_CERT_PASS="$DEV_DATA_PROTECTION_CERT_PASS" \
  PUBLIC_REGISTRATION_ENABLED="true" \
  PRIVATE_EMAIL_DOMAINS="$DEV_PRIVATE_EMAIL_DOMAINS" \
  HIDDEN_PRIVATE_EMAIL_DOMAINS="$DEV_HIDDEN_PRIVATE_EMAIL_DOMAINS" \
  IP_LOGGING_ENABLED="true" \
  MAX_UPLOAD_SIZE_MB="100" \
    run_dotnet "AliasVault.Api" "$API_HTTP"
}
start_admin() {
  ADMIN_PASSWORD_HASH="$DEV_ADMIN_PASSWORD_HASH" \
  ADMIN_PASSWORD_GENERATED="$DEV_ADMIN_PASSWORD_GENERATED" \
  DATA_PROTECTION_CERT_PASS="$DEV_DATA_PROTECTION_CERT_PASS" \
  IP_LOGGING_ENABLED="true" \
    run_dotnet "AliasVault.Admin" "$ADMIN_HTTP"
}
start_client() {
  write_client_dev_settings
  run_dotnet "AliasVault.Client" "$CLIENT_HTTP"
}

# Background workers (no HTTP port).
run_worker() {
  local proj="$1"
  require dotnet
  banner "$proj (worker)" "Database  localhost:$DB_PORT"
  ConnectionStrings__AliasServerDbContext="$CONN_STR" \
    dotnet watch --project "$ROOT_DIR/apps/server/$proj"
}
start_smtp()       { run_worker "Services/AliasVault.SmtpService"; }
start_taskrunner() { run_worker "Services/AliasVault.TaskRunner"; }

start_ext() {
  require npm
  banner "Browser extension (Chrome dev)" \
    "Dev server  http://localhost:$EXT_PORT" \
    "API         http://localhost:$API_HTTP"
  cd "$ROOT_DIR/apps/browser-extension"
  npm run dev:chrome -- --port "$EXT_PORT"
}

start_mobile() {
  require npx
  banner "Mobile app (Expo)" \
    "Metro  http://localhost:$EXPO_PORT" \
    "API    http://localhost:$API_HTTP"
  cd "$ROOT_DIR/apps/mobile-app"
  EXPO_PUBLIC_API_URL="http://localhost:$API_HTTP" \
    npx expo start --port "$EXPO_PORT"
}

# --- Interactive menu ----------------------------------------------------------
menu() {
  print_ports
  printf "\n%sWhich app do you want to start?%s\n" "$BOLD" "$NC"
  # Quick live check so the DB entry shows the action it'll take + current state.
  local db_label
  if db_running; then
    db_label="Stop dev DB  ${GREEN}(running)${NC}"
  else
    db_label="Start dev DB ${DIM}(stopped)${NC}"
  fi
  local options=(
    "api:API"
    "client:Client"
    "admin:Admin"
    "ext:Browser extension"
    "mobile:Mobile (Expo)"
    "db:$db_label"
  )
  local i=1
  for o in "${options[@]}"; do
    printf "  %s%2d%s) %s\n" "$CYAN" "$i" "$NC" "${o#*:}"; i=$((i+1))
  done
  printf "%sSelect [1-%d]:%s " "$BOLD" "${#options[@]}" "$NC"
  local choice; read -r choice
  case "$choice" in (*[!0-9]*|"") die "Invalid selection.";; esac
  [ "$choice" -ge 1 ] && [ "$choice" -le "${#options[@]}" ] || die "Selection out of range."
  local picked="${options[$((choice-1))]}"
  dispatch "${picked%%:*}"
}

# --- Dispatch ------------------------------------------------------------------
dispatch() {
  case "${1:-}" in
    api)        start_api ;;
    client)     start_client ;;
    admin)      start_admin ;;
    smtp)       start_smtp ;;
    taskrunner) start_taskrunner ;;
    ext)        start_ext ;;
    mobile)     start_mobile ;;
    db)         toggle_db ;;
    db-start)   start_db ;;
    db-stop)    stop_db ;;
    ports)      print_ports ;;
    quit|q)     exit 0 ;;
    "")         menu ;;
    -h|--help|help) sed -n '2,25p' "$0" | sed 's/^# \{0,1\}//' ;;
    *)          die "Unknown command '$1'.
  start: api/client/admin/smtp/taskrunner/ext/mobile
  db:    db (toggle) / db-start / db-stop" ;;
  esac
}

dispatch "${1:-}"
