#!/usr/bin/env bash
set -Eeuo pipefail

HARBOR_REGISTRY="${HARBOR_REGISTRY:-harbor.apptrix.app}"
HARBOR_PROJECT="${HARBOR_PROJECT:-qanode}"
HARBOR_URL="${HARBOR_URL:-https://${HARBOR_REGISTRY}}"
DEFAULT_INSTALL_DIR="${QANODE_INSTALL_DIR:-$(pwd)}"
DEFAULT_FRONTEND_PORT="${QANODE_FRONTEND_PORT:-3000}"
DEFAULT_WORKER_CONCURRENCY="${QANODE_WORKER_CONCURRENCY:-7}"
DEFAULT_WORKER_REPLICAS="${QANODE_WORKER_REPLICAS:-1}"
DRY_RUN=false
START_STACK=true

for arg in "$@"; do
  case "$arg" in
    --dry-run)
      DRY_RUN=true
      START_STACK=false
      ;;
    --no-start)
      START_STACK=false
      ;;
    --help|-h)
      cat <<'EOF'
QANode installer

Usage:
  sudo bash qanode-install.sh [--dry-run] [--no-start]

Options:
  --dry-run   Generate files, but do not pull or start containers.
  --no-start  Generate files, but do not pull or start containers.
EOF
      exit 0
      ;;
    *)
      echo "Unknown option: $arg" >&2
      exit 1
      ;;
  esac
done

if [[ ! -r /dev/tty ]]; then
  echo "This installer needs an interactive terminal. Download it first and run: sudo bash qanode-install.sh" >&2
  exit 1
fi

TTY=/dev/tty

log() {
  printf '\n[QANode] %s\n' "$*" > "$TTY"
}

warn() {
  printf '\n[QANode][WARN] %s\n' "$*" > "$TTY"
}

die() {
  printf '\n[QANode][ERROR] %s\n' "$*" >&2
  exit 1
}

ask() {
  local label="$1"
  local default_value="${2:-}"
  local answer
  if [[ -n "$default_value" ]]; then
    printf '%s [%s]: ' "$label" "$default_value" > "$TTY"
  else
    printf '%s: ' "$label" > "$TTY"
  fi
  IFS= read -r answer < "$TTY"
  if [[ -z "$answer" ]]; then
    printf '%s' "$default_value"
  else
    printf '%s' "$answer"
  fi
}

ask_required() {
  local label="$1"
  local answer
  while true; do
    answer="$(ask "$label" "")"
    if [[ -n "${answer// }" ]]; then
      printf '%s' "$answer"
      return
    fi
    warn "Value required."
  done
}

ask_secret() {
  local label="$1"
  local answer
  while true; do
    printf '%s: ' "$label" > "$TTY"
    IFS= read -rs answer < "$TTY"
    printf '\n' > "$TTY"
    if [[ -n "$answer" ]]; then
      printf '%s' "$answer"
      return
    fi
    warn "Value required."
  done
}

has_space_or_control() {
  local value="$1"
  [[ "$value" =~ [[:space:]] || "$value" =~ [[:cntrl:]] ]]
}

validate_config_token() {
  local label="$1"
  local value="$2"
  if has_space_or_control "$value"; then
    warn "${label} cannot contain spaces or control characters."
    return 1
  fi
  if [[ ! "$value" =~ ^[A-Za-z0-9._:@/+=,-]+$ ]]; then
    warn "${label} contains unsupported characters."
    return 1
  fi
  return 0
}

ask_yes_no() {
  local label="$1"
  local default_value="${2:-y}"
  local answer normalized
  while true; do
    answer="$(ask "$label" "$default_value")"
    normalized="$(printf '%s' "$answer" | tr '[:upper:]' '[:lower:]')"
    case "$normalized" in
      y|yes|s|sim) return 0 ;;
      n|no|nao) return 1 ;;
      *) warn "Answer y or n." ;;
    esac
  done
}

need_command() {
  command -v "$1" >/dev/null 2>&1 || die "Required command not found: $1"
}

install_packages() {
  if (( "$#" == 0 )); then
    return 0
  fi

  if command -v apt-get >/dev/null 2>&1; then
    export DEBIAN_FRONTEND=noninteractive
    apt-get update
    apt-get install -y "$@"
    return
  fi
  if command -v dnf >/dev/null 2>&1; then
    dnf install -y "$@"
    return
  fi
  if command -v yum >/dev/null 2>&1; then
    yum install -y "$@"
    return
  fi
  if command -v apk >/dev/null 2>&1; then
    apk add --no-cache "$@"
    return
  fi

  return 1
}

ensure_command() {
  local command_name="$1"
  local package_name="${2:-$1}"
  if command -v "$command_name" >/dev/null 2>&1; then
    return 0
  fi

  log "Installing missing dependency: ${package_name}"
  if install_packages "$package_name" && command -v "$command_name" >/dev/null 2>&1; then
    return 0
  fi

  die "Could not install required dependency: ${package_name}"
}

check_prerequisites() {
  log "Checking prerequisites..."
  need_command docker
  docker compose version >/dev/null 2>&1 || die "Docker Compose plugin is required. Expected command: docker compose version"
  docker info >/dev/null 2>&1 || die "Docker is not available for this user. Run with sudo or adjust Docker permissions."

  ensure_command curl curl
  ensure_command openssl openssl
}

generate_secret() {
  openssl rand -hex 16
}

detect_server_ip() {
  local ip=""
  if command -v ip >/dev/null 2>&1; then
    ip="$(ip route get 1.1.1.1 2>/dev/null | awk '{for (i=1;i<=NF;i++) if ($i=="src") {print $(i+1); exit}}' || true)"
  fi
  if [[ -z "$ip" ]] && command -v hostname >/dev/null 2>&1; then
    ip="$(hostname -I 2>/dev/null | awk '{print $1}' || true)"
  fi
  printf '%s' "${ip:-127.0.0.1}"
}

is_port_in_use() {
  local port="$1"
  if command -v ss >/dev/null 2>&1; then
    ss -ltn 2>/dev/null | awk '{print $4}' | grep -Eq "[:.]${port}$"
    return $?
  fi
  if command -v lsof >/dev/null 2>&1; then
    lsof -iTCP:"$port" -sTCP:LISTEN >/dev/null 2>&1
    return $?
  fi
  return 1
}

ask_frontend_port() {
  local port
  while true; do
    port="$(ask "Frontend external port" "$DEFAULT_FRONTEND_PORT")"
    if [[ ! "$port" =~ ^[0-9]+$ ]] || (( port < 1 || port > 65535 )); then
      warn "Invalid port."
      continue
    fi
    if is_port_in_use "$port"; then
      warn "Port $port is already in use."
      continue
    fi
    printf '%s' "$port"
    return
  done
}

normalize_url() {
  local raw="$1"
  raw="${raw%/}"
  if [[ "$raw" != http://* && "$raw" != https://* ]]; then
    raw="http://${raw}"
  fi
  printf '%s' "$raw"
}

validate_app_url() {
  local url="$1"
  local port=""
  if has_space_or_control "$url"; then
    warn "APP_URL cannot contain spaces or control characters."
    return 1
  fi
  if [[ "$url" =~ :([0-9]+)$ ]]; then
    port="${BASH_REMATCH[1]}"
    if (( port < 1 || port > 65535 )); then
      warn "APP_URL port is invalid."
      return 1
    fi
  fi
  if [[ "$url" =~ ^https?://([A-Za-z0-9.-]+|\[[0-9A-Fa-f:.]+\])(:[0-9]+)?$ ]]; then
    return 0
  fi
  warn "APP_URL must be only the public origin, for example: http://192.168.0.10:3000 or https://qanode.example.com"
  return 1
}

ask_app_url() {
  local suggested_url="$1"
  local app_url_input normalized
  while true; do
    app_url_input="$(ask "APP_URL (IP:port or DNS used by users)" "$suggested_url")"
    normalized="$(normalize_url "$app_url_input")"
    if validate_app_url "$normalized"; then
      printf '%s' "$normalized"
      return
    fi
  done
}

ask_license_key() {
  local license_key
  while true; do
    license_key="$(ask_required "Enterprise license key")"
    if validate_config_token "Enterprise license key" "$license_key"; then
      printf '%s' "$license_key"
      return
    fi
  done
}

harbor_api() {
  local path="$1"
  curl -fsS --connect-timeout 15 --retry 1 \
    -u "${HARBOR_USER}:${HARBOR_PASSWORD}" \
    "${HARBOR_URL}${path}"
}

registry_api() {
  local path="$1"
  curl -fsS --connect-timeout 15 --retry 1 \
    -u "${HARBOR_USER}:${HARBOR_PASSWORD}" \
    "${HARBOR_URL}${path}"
}

docker_login_loop() {
  log "Harbor authentication"
  while true; do
    HARBOR_USER="$(ask_required "Harbor login")"
    HARBOR_PASSWORD="$(ask_secret "Harbor password")"

    if printf '%s' "$HARBOR_PASSWORD" | docker login "$HARBOR_REGISTRY" -u "$HARBOR_USER" --password-stdin >/dev/null 2>&1; then
      log "Harbor login OK."
      export HARBOR_USER HARBOR_PASSWORD
      return
    fi

    warn "Invalid Harbor login or password. Try again."
  done
}

extract_semver_tags() {
  grep -oE '"[0-9]+\.[0-9]+\.[0-9]+"' \
    | tr -d '"' \
    | sort -u
}

fetch_repo_tags() {
  local repo="$1"
  local output_file="$2"
  local response_file
  response_file="$(mktemp)"

  if registry_api "/v2/${HARBOR_PROJECT}/${repo}/tags/list" > "$response_file" 2>/dev/null; then
    extract_semver_tags < "$response_file" > "$output_file" || true
    rm -f "$response_file"
    [[ -s "$output_file" ]] && return 0
  fi

  if harbor_api "/api/v2.0/projects/${HARBOR_PROJECT}/repositories/${repo}/artifacts?page_size=100" > "$response_file" 2>/dev/null; then
    extract_semver_tags < "$response_file" > "$output_file" || true
    rm -f "$response_file"
    [[ -s "$output_file" ]] && return 0
  fi

  rm -f "$response_file"
  return 1
}

discover_latest_version() {
  local tmpdir api_tags worker_tags frontend_tags common_tags latest
  tmpdir="$(mktemp -d)"
  api_tags="${tmpdir}/api.tags"
  worker_tags="${tmpdir}/worker.tags"
  frontend_tags="${tmpdir}/frontend.tags"

  if ! fetch_repo_tags "qanode-api" "$api_tags"; then
    rm -rf "$tmpdir"
    return 1
  fi
  if ! fetch_repo_tags "qanode-worker" "$worker_tags"; then
    rm -rf "$tmpdir"
    return 1
  fi
  if ! fetch_repo_tags "qanode-frontend" "$frontend_tags"; then
    rm -rf "$tmpdir"
    return 1
  fi

  common_tags="$(comm -12 "$api_tags" "$worker_tags" | comm -12 - "$frontend_tags" || true)"
  latest="$(printf '%s\n' "$common_tags" | sed '/^$/d' | sort -V | tail -n 1)"
  rm -rf "$tmpdir"

  [[ -n "$latest" ]] || return 1
  printf '%s' "$latest"
}

tag_exists_in_harbor() {
  local repo="$1"
  local version="$2"
  harbor_api "/api/v2.0/projects/${HARBOR_PROJECT}/repositories/${repo}/artifacts/${version}" >/dev/null 2>&1
}

tag_exists_with_docker() {
  local repo="$1"
  local version="$2"
  docker manifest inspect "${HARBOR_REGISTRY}/${HARBOR_PROJECT}/${repo}:${version}" >/dev/null 2>&1
}

validate_version() {
  local version="$1"
  local repo
  for repo in qanode-api qanode-worker qanode-frontend; do
    if tag_exists_with_docker "$repo" "$version"; then
      continue
    fi
    if tag_exists_in_harbor "$repo" "$version"; then
      continue
    fi
    warn "Image not found: ${HARBOR_REGISTRY}/${HARBOR_PROJECT}/${repo}:${version}"
    return 1
  done
  return 0
}

ask_version() {
  local latest version
  log "Discovering latest QANode version in Harbor..."
  if latest="$(discover_latest_version)"; then
    log "Latest common version found: ${latest}"
  else
    warn "Could not discover latest version automatically. You can type it manually."
    latest=""
  fi

  while true; do
    version="$(ask "QANode version" "$latest")"
    if [[ -z "$version" ]]; then
      warn "Version required."
      continue
    fi
    if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
      warn "Use semantic version format, for example: 0.8.4"
      continue
    fi
    if validate_version "$version"; then
      printf '%s' "$version"
      return
    fi
    warn "Version ${version} is not available for all required images. Try again."
  done
}

prepare_install_dir() {
  INSTALL_DIR="$(ask "Install directory" "$DEFAULT_INSTALL_DIR")"
  [[ -n "$INSTALL_DIR" ]] || INSTALL_DIR="$DEFAULT_INSTALL_DIR"
  if has_space_or_control "$INSTALL_DIR"; then
    die "Install directory cannot contain spaces or control characters."
  fi
  case "$INSTALL_DIR" in
    /*) ;;
    *) INSTALL_DIR="$(pwd)/${INSTALL_DIR}" ;;
  esac

  if [[ -e "${INSTALL_DIR}/docker-compose.yml" || -e "${INSTALL_DIR}/.env" ]]; then
    warn "Existing QANode files found in ${INSTALL_DIR}."
    if ! ask_yes_no "Backup existing files and continue?" "n"; then
      die "Installation cancelled."
    fi
    local backup_dir="${INSTALL_DIR}/backup-$(date +%Y%m%d-%H%M%S)"
    mkdir -p "$backup_dir"
    [[ -e "${INSTALL_DIR}/docker-compose.yml" ]] && cp "${INSTALL_DIR}/docker-compose.yml" "$backup_dir/"
    [[ -e "${INSTALL_DIR}/.env" ]] && cp "${INSTALL_DIR}/.env" "$backup_dir/env.backup"
    log "Backup saved at ${backup_dir}"
  fi

  mkdir -p "$INSTALL_DIR"
}

write_env_file() {
  local env_file="${INSTALL_DIR}/.env"
  cat > "$env_file" <<EOF
# Generated by QANode installer on $(date -Iseconds)
QANODE_VERSION=${QANODE_VERSION}
HARBOR_REGISTRY=${HARBOR_REGISTRY}
HARBOR_PROJECT=${HARBOR_PROJECT}

# External access
FRONTEND_PORT=${FRONTEND_PORT}
APP_URL=${APP_URL}

# PostgreSQL
POSTGRES_USER=qanode
POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
POSTGRES_DB=qanode

# Security
QANODE_CRED_KEY=${QANODE_CRED_KEY}
JWT_SECRET=${JWT_SECRET}

# License
LICENSE_KEY=${LICENSE_KEY}

# Queue Worker
EXECUTIONS_MODE=queue
REDIS_HOST=redis
REDIS_PORT=6379
WORKER_QUEUES=executions,load-tests
WORKER_CONCURRENCY=${WORKER_CONCURRENCY}
WORKER_REPLICAS=${WORKER_REPLICAS}

# Storage
STORAGE_PROVIDER=s3
S3_ENDPOINT=http://seaweedfs:8333
S3_ACCESS_KEY=admin
S3_SECRET_KEY=${S3_SECRET_KEY}
S3_BUCKET=qanode

# Bootstrap
QANODE_RUN_BOOTSTRAP=true
EOF
  chmod 600 "$env_file"
}

write_compose_file() {
  local compose_file="${INSTALL_DIR}/docker-compose.yml"
  cat > "$compose_file" <<EOF
services:
  postgres:
    image: postgres:16
    environment:
      POSTGRES_USER: \${POSTGRES_USER}
      POSTGRES_PASSWORD: \${POSTGRES_PASSWORD}
      POSTGRES_DB: \${POSTGRES_DB}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U qanode"]
      interval: 5s
      timeout: 5s
      retries: 10

  redis:
    image: redis:7-alpine
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 10

  seaweedfs:
    image: chrislusf/seaweedfs:4.37
    entrypoint: ["/bin/sh", "-c"]
    command:
      - |
        mkdir -p /etc/seaweedfs
        cat > /etc/seaweedfs/s3.json <<'SEAWEEDFS_S3_CONFIG'
        {"identities":[{"name":"admin","credentials":[{"accessKey":"\${S3_ACCESS_KEY}","secretKey":"\${S3_SECRET_KEY}"}],"actions":["Admin","Read","Write"]}]}
        SEAWEEDFS_S3_CONFIG
        exec weed server -s3 -s3.config=/etc/seaweedfs/s3.json -dir=/data
    volumes:
      - seaweedfs_data:/data

  api:
    image: \${HARBOR_REGISTRY}/\${HARBOR_PROJECT}/qanode-api:\${QANODE_VERSION}
    environment:
      DATABASE_URL: postgresql://\${POSTGRES_USER}:\${POSTGRES_PASSWORD}@postgres:5432/\${POSTGRES_DB}
      EXECUTIONS_MODE: queue
      REDIS_HOST: redis
      REDIS_PORT: \${REDIS_PORT}
      STORAGE_PROVIDER: \${STORAGE_PROVIDER}
      S3_ENDPOINT: \${S3_ENDPOINT}
      S3_ACCESS_KEY: \${S3_ACCESS_KEY}
      S3_SECRET_KEY: \${S3_SECRET_KEY}
      S3_BUCKET: \${S3_BUCKET}
      QANODE_CRED_KEY: \${QANODE_CRED_KEY}
      JWT_SECRET: \${JWT_SECRET}
      LICENSE_KEY: \${LICENSE_KEY}
      APP_URL: \${APP_URL}
      QANODE_RUN_BOOTSTRAP: \${QANODE_RUN_BOOTSTRAP}
    extra_hosts:
      - "host.docker.internal:host-gateway"
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
      seaweedfs:
        condition: service_started
    healthcheck:
      test: ["CMD-SHELL", "curl -fsS http://localhost:3001/api/health >/dev/null || exit 1"]
      interval: 10s
      timeout: 5s
      retries: 30

  worker:
    image: \${HARBOR_REGISTRY}/\${HARBOR_PROJECT}/qanode-worker:\${QANODE_VERSION}
    environment:
      DATABASE_URL: postgresql://\${POSTGRES_USER}:\${POSTGRES_PASSWORD}@postgres:5432/\${POSTGRES_DB}
      EXECUTIONS_MODE: queue
      REDIS_HOST: redis
      REDIS_PORT: \${REDIS_PORT}
      WORKER_QUEUES: \${WORKER_QUEUES}
      WORKER_CONCURRENCY: \${WORKER_CONCURRENCY}
      STORAGE_PROVIDER: \${STORAGE_PROVIDER}
      S3_ENDPOINT: \${S3_ENDPOINT}
      S3_ACCESS_KEY: \${S3_ACCESS_KEY}
      S3_SECRET_KEY: \${S3_SECRET_KEY}
      S3_BUCKET: \${S3_BUCKET}
      QANODE_CRED_KEY: \${QANODE_CRED_KEY}
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
      seaweedfs:
        condition: service_started
    deploy:
      replicas: \${WORKER_REPLICAS}

  frontend:
    image: \${HARBOR_REGISTRY}/\${HARBOR_PROJECT}/qanode-frontend:\${QANODE_VERSION}
    ports:
      - "\${FRONTEND_PORT}:80"
    depends_on:
      api:
        condition: service_healthy

volumes:
  postgres_data:
  redis_data:
  seaweedfs_data:
EOF
  chmod 644 "$compose_file"
}

wait_for_health() {
  local url="http://127.0.0.1:${FRONTEND_PORT}/api/health"
  local attempts=90
  local i

  log "Waiting for QANode healthcheck at ${url}..."
  for ((i=1; i<=attempts; i++)); do
    if curl -fsS "$url" >/dev/null 2>&1; then
      log "Healthcheck OK."
      return 0
    fi
    sleep 2
  done

  warn "Healthcheck did not respond in time."
  return 1
}

start_stack() {
  cd "$INSTALL_DIR"
  log "Pulling Docker images..."
  docker compose pull

  log "Starting QANode..."
  docker compose up -d

  if ! wait_for_health; then
    warn "QANode containers were started, but healthcheck failed."
    docker compose ps
    warn "Useful logs:"
    echo "  cd ${INSTALL_DIR}" > "$TTY"
    echo "  docker compose logs --tail=120 api" > "$TTY"
    echo "  docker compose logs --tail=120 worker" > "$TTY"
    return 1
  fi

  docker compose ps
}

main() {
  check_prerequisites
  docker_login_loop

  QANODE_VERSION="$(ask_version)"
  FRONTEND_PORT="$(ask_frontend_port)"

  local detected_ip suggested_url
  detected_ip="$(detect_server_ip)"
  suggested_url="http://${detected_ip}:${FRONTEND_PORT}"
  APP_URL="$(ask_app_url "$suggested_url")"

  LICENSE_KEY="$(ask_license_key)"
  WORKER_CONCURRENCY="$(ask "Worker concurrency" "$DEFAULT_WORKER_CONCURRENCY")"
  if [[ ! "$WORKER_CONCURRENCY" =~ ^[0-9]+$ ]] || (( WORKER_CONCURRENCY < 1 )); then
    die "Worker concurrency must be a positive number."
  fi
  WORKER_REPLICAS="$(ask "Worker replicas" "$DEFAULT_WORKER_REPLICAS")"
  if [[ ! "$WORKER_REPLICAS" =~ ^[0-9]+$ ]] || (( WORKER_REPLICAS < 1 )); then
    die "Worker replicas must be a positive number."
  fi

  prepare_install_dir

  POSTGRES_PASSWORD="$(generate_secret)"
  QANODE_CRED_KEY="$(generate_secret)"
  JWT_SECRET="$(generate_secret)"
  S3_SECRET_KEY="$(generate_secret)"

  export QANODE_VERSION FRONTEND_PORT APP_URL LICENSE_KEY WORKER_CONCURRENCY WORKER_REPLICAS
  export POSTGRES_PASSWORD QANODE_CRED_KEY JWT_SECRET S3_SECRET_KEY INSTALL_DIR

  write_env_file
  write_compose_file

  log "Files generated:"
  echo "  ${INSTALL_DIR}/.env" > "$TTY"
  echo "  ${INSTALL_DIR}/docker-compose.yml" > "$TTY"

  if [[ "$DRY_RUN" == true || "$START_STACK" != true ]]; then
    warn "Dry run/no-start mode enabled. Containers were not started."
  else
    start_stack
  fi

  log "QANode installation summary"
  cat > "$TTY" <<EOF
  Directory: ${INSTALL_DIR}
  Version:   ${QANODE_VERSION}
  Workers:   ${WORKER_REPLICAS} replica(s), concurrency ${WORKER_CONCURRENCY}
  Access:    ${APP_URL}

Useful commands:
  cd ${INSTALL_DIR}
  docker compose ps
  docker compose logs -f api
  docker compose logs -f worker
  docker compose pull && docker compose up -d
EOF
}

main "$@"
