#!/usr/bin/env bash
# Install the Oracle Instant Client used by Dosi on supported Linux and macOS
# runtimes. The package must match the architecture of the Dosi process (or of
# Python when dosi-engine is imported there), which can differ from the host.

set -euo pipefail

log() {
  printf '%s\n' "$*" >&2
}

die() {
  log "error: $*"
  exit 1
}

usage() {
  cat <<'EOF'
Install the Oracle Instant Client required for Dosi Oracle queries.

Usage:
  install-oracle-instant-client.sh
  install-oracle-instant-client.sh --detect-arch
  install-oracle-instant-client.sh --print-env
  install-oracle-instant-client.sh --print-package

Environment overrides:
  DOSI_RUNTIME  Dosi or Python executable whose architecture must be matched
  DOSI_ARCH     Explicit x86_64 or arm64 selection for an ambiguous runtime

After installation, evaluate --print-env in the shell that will start Dosi:
  eval "$(bash install-oracle-instant-client.sh --print-env)"

See https://dosi.datus.ai/connectors/oracle/ for supported platforms.
EOF
}

normalize_arch() {
  local value
  value=$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]')
  case "$value" in
    x86_64|x86-64|amd64) printf '%s\n' x86_64 ;;
    arm64|aarch64) printf '%s\n' arm64 ;;
    *) return 1 ;;
  esac
}

resolve_runtime() {
  if [[ -n ${DOSI_RUNTIME:-} ]]; then
    command -v "$DOSI_RUNTIME" 2>/dev/null || die "DOSI_RUNTIME is not executable: $DOSI_RUNTIME"
  elif command -v dosi >/dev/null 2>&1; then
    command -v dosi
  elif command -v python3 >/dev/null 2>&1; then
    command -v python3
  fi
}

detect_runtime_arch() {
  local arch runtime python_arch description has_x86=0 has_arm=0

  if [[ -n ${DOSI_ARCH:-} ]]; then
    arch=$(normalize_arch "$DOSI_ARCH") \
      || die "unsupported DOSI_ARCH '$DOSI_ARCH' (expected x86_64 or arm64)"
    printf '%s\n' "$arch"
    return
  fi

  runtime=$(resolve_runtime)
  if [[ -n $runtime ]]; then
    # A universal2 Python reports both slices through `file`, but this executes
    # the interpreter and reports the slice the current invocation really uses.
    if python_arch=$("$runtime" -c 'import platform; print(platform.machine())' 2>/dev/null) \
      && arch=$(normalize_arch "$python_arch" 2>/dev/null); then
      printf '%s\n' "$arch"
      return
    fi

    command -v file >/dev/null 2>&1 \
      || die "the 'file' command is required to inspect $runtime"
    description=$(file -L "$runtime")
  else
    description=$(uname -m)
  fi

  [[ $description == *x86_64* || $description == *x86-64* ]] && has_x86=1
  [[ $description == *arm64* || $description == *aarch64* ]] && has_arm=1
  if [[ $has_x86 -eq 1 && $has_arm -eq 1 ]]; then
    die "runtime has both x86_64 and arm64 slices; set DOSI_ARCH to the slice Dosi will run"
  elif [[ $has_x86 -eq 1 ]]; then
    printf '%s\n' x86_64
  elif [[ $has_arm -eq 1 ]]; then
    printf '%s\n' arm64
  else
    die "unsupported runtime architecture: $description"
  fi
}

detect_os() {
  printf '%s\n' "${_DOSI_INSTALLER_TEST_OS:-$(uname -s)}"
}

select_package() {
  local os=$1 arch=$2
  case "$os:$arch" in
    Linux:x86_64)
      ARCHIVE=instantclient-basic-linux.x64-23.26.3.0.0.zip
      URL="https://download.oracle.com/otn_software/linux/instantclient/2326300/$ARCHIVE"
      SHA256=fce485361332927f8328ea4b68d98968c6b0ea124e62470de8c9d0cef880130e
      CLIENT_DIR=/opt/oracle/instantclient_23_26
      ;;
    Linux:arm64)
      ARCHIVE=instantclient-basic-linux.arm64-23.26.3.0.0.zip
      URL="https://download.oracle.com/otn_software/linux/instantclient/2326300/$ARCHIVE"
      SHA256=74b0744d7bf1be28f60c3d90a135e5d9aaca1946bab0adcc0c8db5fbc2cc9758
      CLIENT_DIR=/opt/oracle/instantclient_23_26
      ;;
    Darwin:arm64)
      ARCHIVE=instantclient-basic-macos.arm64-23.26.2.0.0.dmg
      URL="https://download.oracle.com/otn_software/mac/instantclient/2326200/$ARCHIVE"
      SHA256=c3c4fce37a557192322717c1a8422fe098fc829a41f382f57a288b24ad6ba11e
      CLIENT_DIR="$HOME/Downloads/instantclient_23_26"
      ;;
    Darwin:x86_64)
      ARCHIVE=instantclient-basic-macos.x64-19.16.0.0.0dbru.dmg
      URL="https://download.oracle.com/otn_software/mac/instantclient/1916000/$ARCHIVE"
      SHA256=725eeb7c00041e926b57c3db508ea7439a1935a37f480c0ac6d46eddc4205331
      CLIENT_DIR="$HOME/Downloads/instantclient_19_16"
      ;;
    *) die "unsupported platform: $os $arch" ;;
  esac
}

run_as_root() {
  if [[ $(id -u) -eq 0 ]]; then
    "$@"
  elif command -v sudo >/dev/null 2>&1; then
    sudo "$@"
  else
    die "root privileges are required, but sudo is unavailable"
  fi
}

install_linux_prerequisites() {
  local libaio_package
  if command -v apt-get >/dev/null 2>&1; then
    run_as_root apt-get update
    if apt-cache show libaio1t64 >/dev/null 2>&1; then
      libaio_package=libaio1t64
    else
      libaio_package=libaio1
    fi
    run_as_root apt-get install -y curl file unzip "$libaio_package"
  elif command -v dnf >/dev/null 2>&1; then
    run_as_root dnf install -y curl file unzip libaio
  else
    die "expected apt-get or dnf on Linux"
  fi
}

repair_libaio_t64_name() {
  local library link
  ldd "$CLIENT_DIR/libclntsh.so" | grep -q 'libaio.so.1 => not found' || return 0
  command -v dpkg-query >/dev/null 2>&1 \
    || die "libaio.so.1 is missing and dpkg-query is unavailable"
  library=$(dpkg-query -L libaio1t64 2>/dev/null \
    | awk '/\/libaio\.so\.1t64$/ { print; exit }')
  [[ -n $library ]] \
    || die "libaio1t64 is installed but libaio.so.1t64 was not found"
  link="$(dirname "$library")/libaio.so.1"
  run_as_root ln -sfn "$(basename "$library")" "$link"
  run_as_root ldconfig
}

verify_linux_client() {
  local dependencies
  [[ -e $CLIENT_DIR/libclntsh.so ]] \
    || die "Oracle client library was not installed at $CLIENT_DIR"
  dependencies=$(ldd "$CLIENT_DIR/libclntsh.so")
  if printf '%s\n' "$dependencies" | grep -q 'not found'; then
    printf '%s\n' "$dependencies" >&2
    die "Oracle Instant Client has a missing native dependency"
  fi
}

install_linux() (
  local temporary
  temporary=$(mktemp -d)
  trap 'rm -rf "$temporary"' EXIT

  if [[ ! -e $CLIENT_DIR/libclntsh.so ]]; then
    log "Downloading $ARCHIVE"
    curl -fsSL "$URL" -o "$temporary/$ARCHIVE"
    printf '%s  %s\n' "$SHA256" "$temporary/$ARCHIVE" | sha256sum -c -
    run_as_root mkdir -p /opt/oracle
    run_as_root unzip -oq "$temporary/$ARCHIVE" -d /opt/oracle
  fi

  printf '%s\n' "$CLIENT_DIR" \
    | run_as_root tee /etc/ld.so.conf.d/oracle-instantclient.conf >/dev/null
  run_as_root ldconfig
  repair_libaio_t64_name
  verify_linux_client
  log "Oracle Instant Client is ready: $CLIENT_DIR"
  log "Run a real 'dosi query --execute' to verify the database connection."
)

mac_library_matches() {
  local description
  [[ -e $CLIENT_DIR/libclntsh.dylib ]] || return 1
  description=$(file -L "$CLIENT_DIR/libclntsh.dylib")
  case "$ARCH" in
    x86_64) [[ $description == *x86_64* ]] ;;
    arm64) [[ $description == *arm64* || $description == *aarch64* ]] ;;
  esac
}

install_macos() (
  local temporary volume=
  if mac_library_matches; then
    log "Oracle Instant Client is already installed: $CLIENT_DIR"
    return
  fi

  temporary=$(mktemp -d)
  # Invoked indirectly by the EXIT trap below.
  # shellcheck disable=SC2329
  cleanup_macos() {
    [[ -z $volume ]] || hdiutil detach "$volume" >/dev/null 2>&1 || true
    rm -rf "$temporary"
  }
  trap cleanup_macos EXIT

  log "Downloading $ARCHIVE"
  curl -fsSL "$URL" -o "$temporary/$ARCHIVE"
  printf '%s  %s\n' "$SHA256" "$temporary/$ARCHIVE" | shasum -a 256 -c -
  volume=$(hdiutil attach -nobrowse "$temporary/$ARCHIVE" \
    | awk '/\/Volumes\// { print substr($0, index($0, "/Volumes/")); exit }')
  [[ -n $volume ]] || die "could not determine the mounted Instant Client volume"
  (cd "$volume" && sh ./install_ic.sh)
  hdiutil detach "$volume" >/dev/null
  volume=

  mac_library_matches \
    || die "installed Oracle client at $CLIENT_DIR does not match $ARCH"
  log "Oracle Instant Client is ready: $CLIENT_DIR"
  log "Evaluate this script's --print-env output before starting Dosi."
)

print_package() {
  printf 'os=%s\narch=%s\narchive=%s\nurl=%s\nsha256=%s\nclient_dir=%s\n' \
    "$OS" "$ARCH" "$ARCHIVE" "$URL" "$SHA256" "$CLIENT_DIR"
}

print_environment() {
  if [[ $OS == Darwin ]]; then
    # The package directories contain no shell metacharacters other than a
    # possible space in HOME, which double quotes preserve.
    printf '%s\n' \
      "export DYLD_LIBRARY_PATH=\"$CLIENT_DIR\${DYLD_LIBRARY_PATH:+:\$DYLD_LIBRARY_PATH}\""
  fi
}

main() {
  local mode=install
  if [[ $# -gt 1 ]]; then
    usage >&2
    exit 2
  elif [[ $# -eq 1 ]]; then
    case "$1" in
      --detect-arch) mode=detect ;;
      --print-env) mode=print_env ;;
      --print-package) mode=package ;;
      -h|--help) usage; return ;;
      *) usage >&2; exit 2 ;;
    esac
  fi

  OS=$(detect_os)
  if [[ $mode == install && $OS == Linux ]]; then
    # Minimal Linux images may not have `file`; install it before inspecting
    # the Dosi binary rather than silently falling back to the host CPU.
    install_linux_prerequisites
  fi
  ARCH=$(detect_runtime_arch)
  if [[ $mode == detect ]]; then
    printf '%s\n' "$ARCH"
    return
  fi
  select_package "$OS" "$ARCH"

  case "$mode" in
    print_env) print_environment ;;
    package) print_package ;;
    install)
      case "$OS" in
        Linux) install_linux ;;
        Darwin) install_macos ;;
        *) die "unsupported operating system: $OS" ;;
      esac
      ;;
  esac
}

main "$@"
