#!/usr/bin/env bash
# Roll the currently checked-out code out to every database on the box.
#
# This is what Jenkins calls. It is idempotent and safe to re-run.
#
#   deploy.sh                 update masterdb + nacef_tpl + every tenant
#   deploy.sh --no-backup     skip the pg_dump (faster, use only for hotfixes)
#   deploy.sh --db acme       update one database and stop
#
# Odoo cannot apply a schema update while the server is serving the same
# database, so this takes the web containers down for the duration. Postgres
# stays up.
set -Eeuo pipefail

DEPLOY_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$DEPLOY_DIR"
set -a; . ./.env; set +a

BACKUP=1
ONLY_DB=""
while [[ $# -gt 0 ]]; do
  case "$1" in
    --no-backup) BACKUP=0; shift ;;
    --db)        ONLY_DB="$2"; shift 2 ;;
    *) echo "unknown argument: $1" >&2; exit 2 ;;
  esac
done

DC="docker compose -f $DEPLOY_DIR/docker-compose.yml"
STAMP="$(date +%Y%m%d-%H%M%S)"

log() { printf '\n\033[1;34m==> %s\033[0m\n' "$*"; }

psql_q() {  # psql_q <db> <sql>  -> bare rows
  # </dev/null matters: `docker compose exec` reads stdin even with -T, and
  # when called inside a loop it will happily drain the loop's own input.
  $DC exec -T db psql -U "$DB_USER" -d "$1" -tAc "$2" </dev/null
}

# ---------------------------------------------------------------- discovery
# Everything that is an Odoo database, template included. Order matters:
# nacef_tpl goes first so a newly provisioned tenant is never cloned from a
# stale template.
list_databases() {
  psql_q postgres "
    SELECT datname FROM pg_database
     WHERE datistemplate = false
       AND datname NOT IN ('postgres')
     ORDER BY (datname <> 'nacef_tpl'), (datname <> 'masterdb'), datname;"
}

# The nacef modules actually installed in a given database. Updating a module
# that was never installed there would install it, which is not what a deploy
# should do.
installed_modules() {
  local db="$1"
  psql_q "$db" "
    SELECT string_agg(name, ',' ORDER BY name)
      FROM ir_module_module
     WHERE state = 'installed' AND name LIKE 'nacef%';" 2>/dev/null || true
}

is_odoo_db() {
  [[ "$(psql_q "$1" "SELECT to_regclass('public.ir_module_module') IS NOT NULL;" 2>/dev/null || echo f)" == "t" ]]
}

# Resolve the list once, up front, into an array. Streaming it into a
# `while read` loop does not work here: every docker command in the body
# inherits the loop's stdin and swallows the remaining database names, so
# only the first one is ever processed.
mapfile -t DATABASES < <(list_databases)
[[ ${#DATABASES[@]} -gt 0 ]] || { echo "No databases found."; exit 1; }
echo "Databases: ${DATABASES[*]}"

# ------------------------------------------------------------------ backup
if [[ $BACKUP -eq 1 ]]; then
  log "Backing up to $BACKUP_DIR/$STAMP"
  # Create the directory on the host, not with `exec db mkdir`: the container
  # runs as root, and a root-owned directory here stops the calling user
  # (Jenkins) from writing its own metadata alongside the dumps.
  mkdir -p "$BACKUP_DIR/$STAMP"
  for db in "${DATABASES[@]}"; do
    [[ -z "$db" ]] && continue
    is_odoo_db "$db" || { echo "  skip $db (not an Odoo database)"; continue; }
    echo "  pg_dump $db"
    $DC exec -T db pg_dump -U "$DB_USER" -Fc -f "/backups/$STAMP/$db.dump" "$db" </dev/null
  done
  # Filestore lives in a named volume; snapshot it alongside the dumps.
  docker run --rm -v nacef_filestore:/fs -v "$BACKUP_DIR/$STAMP":/out alpine \
    tar czf /out/filestore.tar.gz -C /fs .
  echo "$STAMP" > "$BACKUP_DIR/LAST_GOOD"
fi

# ------------------------------------------------------------ stop web tier
log "Stopping web containers"
$DC stop odoo odoo-cp

restore_service() { log "Restarting web containers"; $DC up -d odoo odoo-cp; }
trap restore_service EXIT

# --------------------------------------------------------------- update DBs
update_db() {
  local db="$1" mods
  if ! is_odoo_db "$db"; then
    echo "  skip $db (not an Odoo database)"; return 0
  fi
  mods="$(installed_modules "$db")"
  if [[ -z "$mods" ]]; then
    echo "  skip $db (no nacef modules installed)"; return 0
  fi
  log "Updating $db: $mods"
  $DC run --rm --no-deps -T odoo \
    odoo -c /etc/odoo/odoo.conf -d "$db" -u "$mods" --stop-after-init </dev/null
}

if [[ -n "$ONLY_DB" ]]; then
  update_db "$ONLY_DB"
else
  for db in "${DATABASES[@]}"; do
    [[ -z "$db" ]] && continue
    update_db "$db"
  done
fi

# ------------------------------------------------------------------ restart
trap - EXIT
restore_service

# -------------------------------------------------------------------- smoke
log "Smoke check"
for i in $(seq 1 30); do
  cp_code=$(curl -s -o /dev/null -w '%{http_code}' -H 'Host: admin.kaissflow.com' http://127.0.0.1:8070/web/login || true)
  [[ "$cp_code" == "200" ]] && break
  sleep 3
done
[[ "$cp_code" == "200" ]] || { echo "control plane did not come back (HTTP $cp_code)"; exit 1; }
echo "  control plane OK"

# Only meaningful once at least one tenant exists.
first_tenant=$(psql_q postgres "
  SELECT datname FROM pg_database
   WHERE datistemplate = false
     AND datname NOT IN ('postgres','masterdb','nacef_tpl')
   ORDER BY datname LIMIT 1;")
if [[ -n "$first_tenant" ]]; then
  t_code=$(curl -s -o /dev/null -w '%{http_code}' \
    -H "Host: ${first_tenant}.kaissflow.com" http://127.0.0.1:8069/web/login || true)
  [[ "$t_code" == "200" ]] || { echo "tenant $first_tenant did not come back (HTTP $t_code)"; exit 1; }
  echo "  tenant $first_tenant OK"
fi

log "Deploy complete (backup: ${STAMP})"
