#!/usr/bin/env bash
# Core Bank (wallet-core core-server) installer — https://corebank.gedata.mn
#
#   curl -fsSL https://corebank.gedata.mn/install.sh | bash -s -- --license CBL1.…
#
# Installs PostgreSQL 17 + core-server with Docker Compose into /opt/corebank. Re-running it upgrades
# to the latest release (or --version) and/or replaces the license (--license); secrets, data and
# settings in /opt/corebank/.env are kept. The license key is verified at corebank.gedata.mn (download
# access) and again, offline, by core-server itself (without it the API is read-only: 402 license_required).
#
# Options:
#   --license KEY          license key (or COREBANK_LICENSE; prompted on a terminal when missing)
#   --dir DIR              install directory (default /opt/corebank)
#   --version X.Y.Z        core-server release (default: latest)
#   --bank-code NNNN       CORE_BANK_CODE, one of the license's bank codes (default: its first)
#   --account-format 12|10 account number layout for a NEW installation (default 12)
#   --port N               API port on this host (default 8090; bound to 127.0.0.1 unless --bind)
#   --bind ADDR            API bind address (default 127.0.0.1; use a private interface, never 0.0.0.0 on the internet)
#   --install-docker       install Docker Engine with get.docker.com when it is missing
#   -y, --yes              do not ask for confirmation
#   -h, --help
set -euo pipefail

main() {
  BASE="${COREBANK_URL:-https://corebank.gedata.mn}"
  DIR=/opt/corebank
  LICENSE="${COREBANK_LICENSE:-}"
  VERSION=latest
  BANK_CODE=""
  ACCOUNT_FORMAT=""
  PORT=8090
  BIND=127.0.0.1
  INSTALL_DOCKER=0
  YES=0
  while [ $# -gt 0 ]; do
    case "$1" in
      --license) LICENSE="${2:-}"; shift 2 ;;
      --license=*) LICENSE="${1#*=}"; shift ;;
      --dir) DIR="${2:-}"; shift 2 ;;
      --version) VERSION="${2:-}"; shift 2 ;;
      --bank-code) BANK_CODE="${2:-}"; shift 2 ;;
      --account-format) ACCOUNT_FORMAT="${2:-}"; shift 2 ;;
      --port) PORT="${2:-}"; shift 2 ;;
      --bind) BIND="${2:-}"; shift 2 ;;
      --install-docker) INSTALL_DOCKER=1; shift ;;
      -y|--yes) YES=1; shift ;;
      -h|--help) usage; exit 0 ;;
      *) die "unknown option: $1 (see --help)" ;;
    esac
  done

  banner
  preflight
  read_license
  activate
  confirm
  fetch_image
  write_config
  start
  summary
}

usage() {
  cat <<'USAGE'
Core Bank installer — https://corebank.gedata.mn
  curl -fsSL https://corebank.gedata.mn/install.sh | bash -s -- --license CBL1.…

  --license KEY          license key (or COREBANK_LICENSE; prompted on a terminal when missing)
  --dir DIR              install directory (default /opt/corebank)
  --version X.Y.Z        core-server release (default: latest)
  --bank-code NNNN       CORE_BANK_CODE, one of the license's bank codes (default: its first)
  --account-format 12|10 account number layout for a new installation (default 12)
  --port N               API port on this host (default 8090)
  --bind ADDR            API bind address (default 127.0.0.1; a private interface, never the internet)
  --install-docker       install Docker Engine with get.docker.com when it is missing
  -y, --yes              do not ask for confirmation
Re-run to upgrade or to replace the license; data and secrets in DIR/.env are kept.
USAGE
}

if [ -t 1 ]; then B=$'\033[1m'; G=$'\033[32m'; Y=$'\033[33m'; R=$'\033[31m'; D=$'\033[2m'; N=$'\033[0m'; else B='' G='' Y='' R='' D='' N=''; fi
info() { printf '%s==>%s %s\n' "$G" "$N" "$*"; }
warn() { printf '%s!!%s  %s\n' "$Y" "$N" "$*" >&2; }
die() { printf '%serror:%s %s\n' "$R" "$N" "$*" >&2; exit 1; }
tty_ok() { [ -r /dev/tty ] && [ -w /dev/tty ] && { : </dev/tty; } 2>/dev/null; }

banner() {
  cat <<'ART'

     ┌─────────────────────────────┐
     │  ▟█▙   C O R E   B A N K    │
     │  ▜█▛   self-hosted ledger   │
     └─────────────────────────────┘
ART
  printf '%s     corebank.gedata.mn%s\n\n' "$D" "$N"
}

preflight() {
  [ "$(uname -s)" = Linux ] || die "core-server installs on Linux (this is $(uname -s)). On macOS/Windows use a Linux VM or server."
  case "$(uname -m)" in
    x86_64|amd64) ARCH=amd64 ;;
    aarch64|arm64) ARCH=arm64 ;;
    *) die "unsupported CPU architecture $(uname -m) (amd64 and arm64 are supported)" ;;
  esac
  case "$PORT" in ''|*[!0-9]*) die "--port must be a number" ;; esac
  case "$ACCOUNT_FORMAT" in ''|10|12) ;; *) die "--account-format must be 12 or 10" ;; esac
  case "$VERSION" in latest|[0-9]*.[0-9]*.[0-9]*) ;; *) die "--version must look like 0.5.0" ;; esac
  for c in curl tar gzip; do command -v "$c" >/dev/null || die "$c is required"; done
  if command -v sha256sum >/dev/null; then SHA="sha256sum"; elif command -v shasum >/dev/null; then SHA="shasum -a 256"; else die "sha256sum is required"; fi

  SUDO=""
  if [ "$(id -u)" -ne 0 ]; then
    command -v sudo >/dev/null || die "run as root or install sudo"
    SUDO="sudo"
  fi

  if ! command -v docker >/dev/null; then
    if [ "$INSTALL_DOCKER" -eq 1 ] || ask "Docker is not installed. Install Docker Engine now (get.docker.com)?"; then
      info "installing Docker Engine"
      curl -fsSL https://get.docker.com | $SUDO sh
    else
      die "Docker is required: https://docs.docker.com/engine/install/ (or re-run with --install-docker)"
    fi
  fi
  # Compose must read the root-owned 0600 .env, so a non-root user goes through sudo even in the docker group.
  DOCKER="$SUDO docker"
  $DOCKER info >/dev/null 2>&1 || die "the Docker daemon is not running (systemctl start docker)"
  $DOCKER compose version >/dev/null 2>&1 || die "Docker Compose v2 is required (docker compose version)"
  info "Linux/$ARCH, $($DOCKER --version | head -1)"
}

ask() {
  [ "$YES" -eq 1 ] && return 0
  tty_ok || return 1
  local a
  printf '%s?%s %s [y/N] ' "$B" "$N" "$1" >/dev/tty
  read -r a </dev/tty || return 1
  case "$a" in y|Y|yes|YES) return 0 ;; esac
  return 1
}

read_license() {
  if [ -z "$LICENSE" ] && $SUDO test -f "$DIR/.env"; then
    LICENSE="$($SUDO sed -n 's/^CORE_LICENSE=//p' "$DIR/.env" | tail -1)"
    [ -n "$LICENSE" ] && info "using the license already in $DIR/.env"
  fi
  if [ -z "$LICENSE" ]; then
    tty_ok || die "no license key: pass --license CBL1.… (get one at $BASE)"
    printf '%s?%s License key (CBL1.…): ' "$B" "$N" >/dev/tty
    IFS= read -r LICENSE </dev/tty || true
    printf '\n' >/dev/tty
  fi
  LICENSE="$(printf '%s' "$LICENSE" | tr -d ' \t\r\n')"
  case "$LICENSE" in CBL1.*.*) ;; *) die "that does not look like a license key (it starts with CBL1.)" ;; esac
  case "$LICENSE" in *[!A-Za-z0-9._-]*) die "the license key contains unexpected characters" ;; esac
}

activate() {
  info "verifying the license with $BASE"
  local out code
  out="$(mktemp)"
  # The key goes through 0600 files, never argv (visible in ps).
  WORK="$(mktemp -d)"
  trap 'rm -rf "$WORK"' EXIT
  printf '%s' "$LICENSE" >"$WORK/license"
  printf 'header = "Authorization: License %s"\n' "$LICENSE" >"$WORK/curl-auth"
  code="$(curl -sS -o "$out" -w '%{http_code}' -H 'Accept: text/plain' -A "corebank-installer/1 ($ARCH)" \
    --data-urlencode "license@$WORK/license" --data-urlencode "arch=$ARCH" --data-urlencode "version=$VERSION" \
    "$BASE/api/v1/activate")" || { rm -f "$out"; die "cannot reach $BASE"; }
  LICENSE_ID='' LICENSEE='' PLAN='' BANK_CODES='' EXPIRES_AT='' REL_VERSION='' IMAGE='' DOWNLOAD_PATH='' SHA256='' SIZE='' ERROR='' MESSAGE=''
  local k v
  while IFS='=' read -r k v; do
    case "$k" in
      LICENSE_ID) LICENSE_ID="$v" ;; LICENSEE) LICENSEE="$v" ;; PLAN) PLAN="$v" ;; BANK_CODES) BANK_CODES="$v" ;;
      EXPIRES_AT) EXPIRES_AT="$v" ;; VERSION) REL_VERSION="$v" ;; IMAGE) IMAGE="$v" ;; DOWNLOAD_PATH) DOWNLOAD_PATH="$v" ;;
      SHA256) SHA256="$v" ;; SIZE) SIZE="$v" ;; ERROR) ERROR="$v" ;; MESSAGE) MESSAGE="$v" ;;
    esac
  done <"$out"
  rm -f "$out"
  [ "$code" = 200 ] || die "license rejected (${ERROR:-HTTP $code}): ${MESSAGE:-unexpected response}"
  case "$IMAGE" in ghcr.io/gerege-systems/core-server:[0-9]*) ;; *) die "unexpected image name from server: $IMAGE" ;; esac
  case "$DOWNLOAD_PATH" in /api/v1/releases/*) ;; *) die "unexpected download path from server" ;; esac
  case "$SHA256" in *[!0-9a-f]*|'') die "unexpected checksum from server" ;; esac

  if [ -z "$BANK_CODE" ]; then
    if $SUDO test -f "$DIR/.env"; then BANK_CODE="$($SUDO sed -n 's/^CORE_BANK_CODE=//p' "$DIR/.env" | tail -1)"; fi
    [ -n "$BANK_CODE" ] || BANK_CODE="${BANK_CODES%%,*}"
  fi
  case ",$BANK_CODES," in *",$BANK_CODE,"*) ;; *) die "bank code $BANK_CODE is not covered by this license (it covers $BANK_CODES)" ;; esac
  info "license $LICENSE_ID — ${B}$LICENSEE${N}, $PLAN, bank code(s) $BANK_CODES, valid until ${EXPIRES_AT%%T*}"
  [ "$PLAN" = production ] || warn "this is a $PLAN license — not for live customer money"
}

confirm() {
  if $SUDO test -f "$DIR/.env"; then
    MODE=upgrade
    printf '\n  Upgrade %s to core-server %s (data and secrets are kept)\n\n' "$DIR" "$REL_VERSION"
  else
    MODE=install
    printf '\n  Install core-server %s into %s\n  bank code %s, API on %s:%s, PostgreSQL 17 (Docker volume)\n\n' \
      "$REL_VERSION" "$DIR" "$BANK_CODE" "$BIND" "$PORT"
  fi
  if [ "$YES" -ne 1 ] && tty_ok; then
    ask "Continue?" || die "cancelled"
  fi
}

fetch_image() {
  if $DOCKER image inspect "$IMAGE" >/dev/null 2>&1; then
    info "$IMAGE is already loaded"
    return
  fi
  info "downloading core-server $REL_VERSION (linux/$ARCH, $((SIZE / 1048576)) MB)"
  curl -fL --retry 3 --progress-bar -K "$WORK/curl-auth" -o "$WORK/image.tar.gz" "$BASE$DOWNLOAD_PATH" \
    || die "download failed"
  printf '%s  %s\n' "$SHA256" "$WORK/image.tar.gz" | $SHA -c - >/dev/null 2>&1 || die "checksum mismatch — download corrupted, re-run the installer"
  info "loading the image"
  gzip -dc "$WORK/image.tar.gz" | $DOCKER load >/dev/null
  rm -f "$WORK/image.tar.gz"
  $DOCKER image inspect "$IMAGE" >/dev/null 2>&1 || die "the archive did not contain $IMAGE"
}

rand_hex() {
  if command -v openssl >/dev/null; then openssl rand -hex "$1"; else head -c "$1" /dev/urandom | od -An -tx1 | tr -d ' \n'; fi
}

# set_env KEY VALUE: replace or append KEY=VALUE in $DIR/.env (values here never contain | or newlines).
set_env() {
  if $SUDO grep -q "^$1=" "$DIR/.env"; then
    $SUDO sed -i "s|^$1=.*|$1=$2|" "$DIR/.env"
  else
    printf '%s=%s\n' "$1" "$2" | $SUDO tee -a "$DIR/.env" >/dev/null
  fi
}

write_config() {
  $SUDO install -d -m 0750 "$DIR"
  if [ "$MODE" = install ]; then
    APP_SECRET="csk_$(rand_hex 24)"
    ADMIN_SECRET="csk_$(rand_hex 24)"
    local name="${LICENSEE//[\"\$\`\\]/}"
    # shellcheck disable=SC2016
    $SUDO sh -c 'umask 077; cat > "$1"' sh "$DIR/.env" <<ENV
# Core Bank — generated by install.sh on $(date -u +%Y-%m-%dT%H:%M:%SZ). Keep this file secret (API keys, DB password).
# Change settings here, then: cd $DIR && docker compose up -d
POSTGRES_PASSWORD=$(rand_hex 24)
CORE_BANK_CODE=$BANK_CODE
CORE_BANK_NAME="$name"
CORE_ACCOUNT_NUMBER_FORMAT=${ACCOUNT_FORMAT:-12}
CORE_TRANSFER_FEE=0
CORE_SIGNUP_BONUS=0
# keyid:secret:scopes[:gl_allowlist] — ck_app for the host app backend, ck_admin for back office.
CORE_API_KEYS="ck_app:$APP_SECRET:superapp+reports:1000-*+1010+4100+4900+5000+5100+2400+5110,ck_admin:$ADMIN_SECRET:admin+reports"
CORE_WEBHOOK_URL=
CORE_WEBHOOK_SECRET=
CORE_BIND=$BIND
CORE_PORT=$PORT
CORE_SERVER_IMAGE=$IMAGE
CORE_LICENSE=$LICENSE
ENV
  else
    [ -z "$ACCOUNT_FORMAT" ] || warn "--account-format is ignored on upgrade (the layout of an installation never changes)"
    $SUDO cp -p "$DIR/.env" "$DIR/.env.bak"
    set_env CORE_SERVER_IMAGE "$IMAGE"
    set_env CORE_LICENSE "$LICENSE"
    BIND="$($SUDO sed -n 's/^CORE_BIND=//p' "$DIR/.env" | tail -1)"; BIND="${BIND:-127.0.0.1}"
    PORT="$($SUDO sed -n 's/^CORE_PORT=//p' "$DIR/.env" | tail -1)"; PORT="${PORT:-8090}"
  fi
  $SUDO sh -c 'cat > "$1"' sh "$DIR/compose.yaml" <<'YAML'
# Core Bank — written by install.sh (re-running the installer overwrites this file; settings live in .env).
name: corebank
services:
  postgres:
    image: postgres:17-alpine
    restart: unless-stopped
    environment:
      POSTGRES_USER: walletcore
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?}
      POSTGRES_DB: walletcore
    volumes: [pgdata:/var/lib/postgresql/data]
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U walletcore -d walletcore"]
      interval: 3s
      timeout: 3s
      retries: 30

  core:
    image: ${CORE_SERVER_IMAGE:?}
    restart: unless-stopped
    environment:
      CORE_DATABASE_URL: postgres://walletcore:${POSTGRES_PASSWORD}@postgres:5432/walletcore?sslmode=disable
      CORE_BANK_CODE: ${CORE_BANK_CODE:?}
      CORE_BANK_NAME: ${CORE_BANK_NAME:-}
      CORE_ACCOUNT_NUMBER_FORMAT: ${CORE_ACCOUNT_NUMBER_FORMAT:-12}
      CORE_TRANSFER_FEE: ${CORE_TRANSFER_FEE:-0}
      CORE_SIGNUP_BONUS: ${CORE_SIGNUP_BONUS:-0}
      CORE_API_KEYS: ${CORE_API_KEYS:?}
      CORE_WEBHOOK_URL: ${CORE_WEBHOOK_URL:-}
      CORE_WEBHOOK_SECRET: ${CORE_WEBHOOK_SECRET:-}
      CORE_LICENSE: ${CORE_LICENSE:?}
    ports:
      - "${CORE_BIND:-127.0.0.1}:${CORE_PORT:-8090}:8090"   # API (HMAC) — keep it on a private network
      - "127.0.0.1:${CORE_OPS_PORT:-9091}:9090"             # ops: /healthz /readyz /metrics
    depends_on:
      postgres: { condition: service_healthy }

volumes:
  pgdata:
YAML
}

start() {
  info "starting PostgreSQL + core-server"
  (cd "$DIR" && $DOCKER compose up -d --quiet-pull --remove-orphans)
  local _
  for _ in $(seq 1 60); do
    (cd "$DIR" && $DOCKER compose exec -T core /core-server healthcheck) >/dev/null 2>&1 && break
    sleep 2
  done
  (cd "$DIR" && $DOCKER compose exec -T core /core-server healthcheck) >/dev/null 2>&1 \
    || die "core-server did not become healthy: cd $DIR && docker compose logs core"
  LICENSE_STATUS="$(cd "$DIR" && $DOCKER compose exec -T core /core-server license 2>&1 | tail -1)" || true
}

summary() {
  printf '\n%s✔ Core Bank %s is running%s (%s)\n\n' "$G$B" "$REL_VERSION" "$N" "$MODE"
  printf '  API          http://%s:%s/v1/health\n' "$BIND" "$PORT"
  printf '  Ops          http://127.0.0.1:%s/readyz  (/metrics for Prometheus)\n' "9091"
  printf '  License      %s\n' "$LICENSE_STATUS"
  printf '  Config       %s/.env  (API keys, DB password — keep secret)\n' "$DIR"
  if [ "$MODE" = install ]; then
    printf '\n  API keys (HMAC — also in %s/.env, shown once here):\n' "$DIR"
    printf '    ck_app    %s   scopes superapp+reports\n' "$APP_SECRET"
    printf '    ck_admin  %s   scopes admin+reports\n' "$ADMIN_SECRET"
  fi
  cat <<EOF

  Next:
    curl -s http://$BIND:$PORT/v1/health
    cd $DIR && docker compose logs -f core      # logs
    cd $DIR && docker compose exec core /core-server audit   # ledger self-check
    Upgrade / renew license: re-run the installer (add --license NEW_KEY to replace it)
    Docs and SDKs (Go, Node, Java): $BASE/#sdk  ·  support: $BASE/#contact

EOF
}

main "$@"
