#!/usr/bin/env bash
# EXTRA_APPS: the apps a workspace grows after init — a new app added to the
# monorepo, served like web/admin/landing.
#
#   EXTRA_APPS="reports partner-portal"
#
# Each one is a workspace name from its own apps/<dir>/package.json, and gets a
# port and a start command named after it: reports -> REPORTS_PORT, REPORTS_CMD.
# Sourced, not executed.

# Service names the base compose file already owns.
EXTRA_APPS_RESERVED="backend queue scheduler web admin landing desktop
frontend-deps frontend-packages frontend-sync mobile-deps mobile-packages
mobile-client postgres mysql redis minio mailpit dashboard"

extra_app_names() {
  local name
  for name in ${EXTRA_APPS:-}; do
    [ -n "${name}" ] && printf '%s\n' "${name}"
  done
  return 0
}

is_extra_app() {
  local name
  while IFS= read -r name; do
    [ "${name}" = "$1" ] && return 0
  done < <(extra_app_names)
  return 1
}

# reports -> REPORTS
extra_app_key() {
  printf '%s' "$1" | tr '[:lower:]' '[:upper:]' | tr -c '[:alnum:]\n' '_'
}

# extra_app_var NAME SUFFIX [fallback]
extra_app_var() {
  local var
  var="$(extra_app_key "$1")_$2"
  if [ -n "${!var:-}" ]; then printf '%s' "${!var}"; else printf '%s' "${3-}"; fi
}

# Why a name cannot be used, or nothing when it is fine.
extra_app_name_problem() {
  local name="$1" existing
  if [ -z "${name}" ]; then
    printf 'is empty'
    return 0
  fi
  case "${name}" in
    *[!a-zA-Z0-9._-]*) printf 'has characters other than letters, digits, . _ or -'; return 0 ;;
    [!a-zA-Z0-9]*)     printf 'does not start with a letter or digit'; return 0 ;;
  esac
  for existing in ${EXTRA_APPS_RESERVED}; do
    if [ "${name}" = "${existing}" ]; then
      printf 'is already a service in the stack'
      return 0
    fi
  done
  return 0
}

# An extra app is whatever it already is: a package depending on expo or
# react-native is a Metro app and is served as one, anything else is a Vite app.
# Nothing in the config says so — the app's own package.json does.
extra_app_is_mobile() {
  local dir
  dir="$(frontend_app_dir "$1")"
  [ -n "${dir}" ] && [ -f "${dir}/package.json" ] || return 1
  node -pe "
    try {
      const p = JSON.parse(require('fs').readFileSync('${dir}/package.json','utf8'));
      const d = Object.keys({ ...p.dependencies, ...p.devDependencies });
      d.some((n) => n === 'expo' || n.startsWith('expo-')
        || n === 'react-native' || n.startsWith('react-native-')) ? 0 : 1;
    } catch (e) { 1 }
  " 2>/dev/null | grep -qx 0
}

# The next free port, counting from where apps of that kind start.
extra_app_default_port() {
  local base=5180 best port name mobile=false
  if [ -n "${1:-}" ] && extra_app_is_mobile "$1"; then
    base=8082
    mobile=true
  fi
  best="${base}"
  while IFS= read -r name; do
    # Its own port is not something to step over: this is the default for it.
    [ "${name}" = "${1:-}" ] && continue
    # Only apps of the same kind count: a Metro port says nothing about where
    # the next Vite app should start, and counting it pushes it out of range.
    if [ "${mobile}" = true ]; then
      extra_app_is_mobile "${name}" || continue
    else
      extra_app_is_mobile "${name}" && continue
    fi
    port="$(extra_app_var "${name}" PORT)"
    case "${port}" in
      ''|*[!0-9]*) continue ;;
    esac
    [ "${port}" -ge "${best}" ] && best=$((port + 1))
  done < <(extra_app_names)
  printf '%s' "${best}"
}

extra_app_port() {
  extra_app_var "$1" PORT "$(extra_app_default_port "$1")"
}
