#!/usr/bin/env bash
#
# Bivious one-command installer and updater.
#
# Usage:
#   curl -fsS https://install.bivious.net | sudo bash
#
# Or locally:
#   sudo ./scripts/install.sh
#
# Supported hosts:
#   Ubuntu 26.04.x LTS (amd64, arm64) — requires Docker
#   Debian 13.x (amd64, arm64) — requires Docker
#
# Environment overrides:
#   BIVIOUS_MANIFEST_URL  — override manifest URL
#   BIVIOUS_RELEASE_BASE  — override release download base URL
#   BIVIOUS_VERSION       — force a specific version instead of latest
#

set -euo pipefail

# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------

SCRIPT_NAME="$(basename "$0")"
readonly SCRIPT_NAME
readonly MANIFEST_URL="${BIVIOUS_MANIFEST_URL:-https://install.bivious.net/manifest.json}"
readonly RELEASE_BASE_URL="${BIVIOUS_RELEASE_BASE:-https://install.bivious.net/releases}"
readonly BIVIOUS_PKG="bivious"
readonly BIVIOUS_SERVICE="bivious-manager.service"

readonly SUPPORTED_UBUNTU_VERSIONS=("26.04")
readonly SUPPORTED_DEBIAN_VERSIONS=("13")

readonly SUPPORTED_PYTHON_MAJOR=3
readonly SUPPORTED_PYTHON_MIN=9
readonly SUPPORTED_PYTHON_MAX=14

readonly SERVICE_WAIT_TIMEOUT=30

readonly DOCKER_SERVICE="docker.service"
readonly DOCKER_WAIT_TIMEOUT=45

# Populated by preflight checks — used in install_or_update.
DEB_ARCH=""
OS_ID=""
OS_VERSION=""

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

die() {
  printf 'error: %s\n' "$1" >&2
  exit 1
}

info() {
  printf ':: %s\n' "$1"
}

# ---------------------------------------------------------------------------
# Cleanup
# ---------------------------------------------------------------------------

WORK_DIR=""

cleanup() {
  if [ -n "$WORK_DIR" ] && [ -d "$WORK_DIR" ]; then
    rm -rf "$WORK_DIR"
  fi
}

trap cleanup EXIT

# ---------------------------------------------------------------------------
# Preflight: root
# ---------------------------------------------------------------------------

check_root() {
  if [ "$(id -u)" -ne 0 ]; then
    printf 'error: this script must be run as root (use: sudo %s)\n' "$SCRIPT_NAME" >&2
    return 1
  fi
}

# ---------------------------------------------------------------------------
# Preflight: operating system
# ---------------------------------------------------------------------------

check_os() {
  if [ ! -f /etc/os-release ]; then
    printf 'error: cannot determine OS — /etc/os-release not found\n' >&2
    return 1
  fi

  # shellcheck disable=SC1091
  . /etc/os-release

  local id="${ID:-}"
  local version_id="${VERSION_ID:-}"
  case "$id" in
    ubuntu)
      local match=0
      for v in "${SUPPORTED_UBUNTU_VERSIONS[@]}"; do
        if [ "$version_id" = "$v" ] || [[ "$version_id" == "$v".* ]]; then
          match=1
          break
        fi
      done
      if [ "$match" -eq 0 ]; then
        printf 'error: unsupported Ubuntu version: %s (supported: %s.x)\n' \
          "$version_id" "${SUPPORTED_UBUNTU_VERSIONS[*]}" >&2
        return 1
      fi
      OS_ID="ubuntu"
      OS_VERSION="$version_id"
      ;;
    debian)
      local match=0
      for v in "${SUPPORTED_DEBIAN_VERSIONS[@]}"; do
        if [ "$version_id" = "$v" ] || [[ "$version_id" == "$v".* ]]; then
          match=1
          break
        fi
      done
      if [ "$match" -eq 0 ]; then
        printf 'error: unsupported Debian version: %s (supported: %s.x)\n' \
          "$version_id" "${SUPPORTED_DEBIAN_VERSIONS[*]}" >&2
        return 1
      fi
      OS_ID="debian"
      OS_VERSION="$version_id"
      ;;
    *)
      printf 'error: unsupported OS: %s (supported: Ubuntu %s.x, Debian %s.x)\n' \
        "${id:-unknown}" "${SUPPORTED_UBUNTU_VERSIONS[*]}" "${SUPPORTED_DEBIAN_VERSIONS[*]}" >&2
      return 1
      ;;
  esac

  info "detected ${OS_ID} ${OS_VERSION}"
}

# ---------------------------------------------------------------------------
# Preflight: architecture
# ---------------------------------------------------------------------------

check_arch() {
  local raw_arch=""

  if command -v dpkg >/dev/null 2>&1; then
    raw_arch="$(dpkg --print-architecture)"
  elif command -v uname >/dev/null 2>&1; then
    raw_arch="$(uname -m)"
  else
    printf 'error: cannot determine architecture — neither dpkg nor uname available\n' >&2
    return 1
  fi

  case "$raw_arch" in
    amd64|x86_64)
      DEB_ARCH="amd64"
      ;;
    arm64|aarch64)
      DEB_ARCH="arm64"
      ;;
    *)
      printf 'error: unsupported architecture: %s (supported: amd64/x86_64, arm64/aarch64)\n' \
        "$raw_arch" >&2
      return 1
      ;;
  esac

  info "detected architecture: ${DEB_ARCH}"
}

# ---------------------------------------------------------------------------
# Preflight: /dev/net/tun
# ---------------------------------------------------------------------------

check_tun() {
  if [ ! -c /dev/net/tun ]; then
    printf 'error: /dev/net/tun is not a character device — TUN support is required\n' >&2
    return 1
  fi
}

# ---------------------------------------------------------------------------
# Preflight: openssl
# ---------------------------------------------------------------------------

check_openssl() {
  if [ ! -x /usr/bin/openssl ]; then
    printf 'error: /usr/bin/openssl not found or not executable — install openssl\n' >&2
    return 1
  fi
}

# ---------------------------------------------------------------------------
# Preflight: python3
# ---------------------------------------------------------------------------

check_python() {
  if [ ! -x /usr/bin/python3 ]; then
    printf 'error: /usr/bin/python3 not found or not executable — install python3\n' >&2
    return 1
  fi

  local py_version
  py_version="$(/usr/bin/python3 -c "
import sys
print(f'{sys.version_info.major}.{sys.version_info.minor}')
")"

  local py_major py_minor
  py_major="${py_version%%.*}"
  py_minor="${py_version##*.}"

  if [ "$py_major" -ne "$SUPPORTED_PYTHON_MAJOR" ]; then
    printf 'error: Python %s is not supported (requires %d.%d–%d.%d)\n' \
      "$py_version" "$SUPPORTED_PYTHON_MAJOR" "$SUPPORTED_PYTHON_MIN" \
      "$SUPPORTED_PYTHON_MAJOR" "$SUPPORTED_PYTHON_MAX" >&2
    return 1
  fi

  if [ "$py_minor" -lt "$SUPPORTED_PYTHON_MIN" ] || [ "$py_minor" -gt "$SUPPORTED_PYTHON_MAX" ]; then
    printf 'error: Python %s is not supported (requires %d.%d–%d.%d)\n' \
      "$py_version" "$SUPPORTED_PYTHON_MAJOR" "$SUPPORTED_PYTHON_MIN" \
      "$SUPPORTED_PYTHON_MAJOR" "$SUPPORTED_PYTHON_MAX" >&2
    return 1
  fi

  info "detected Python ${py_version}"
}

# ---------------------------------------------------------------------------
# Preflight: systemd
# ---------------------------------------------------------------------------

check_systemd() {
  if [ ! -d /run/systemd/system ]; then
    printf 'error: systemd not detected (/run/systemd/system missing)\n' >&2
    return 1
  fi
}

# ---------------------------------------------------------------------------
# Preflight: package management tools
# ---------------------------------------------------------------------------

check_package_manager() {
  local missing=""

  if ! command -v dpkg >/dev/null 2>&1; then
    missing="${missing} dpkg"
  fi

  if ! command -v apt-get >/dev/null 2>&1; then
    missing="${missing} apt-get"
  fi

  if [ -n "$missing" ]; then
    printf 'error: missing required tools:%s\n' "$missing" >&2
    return 1
  fi
}

# ---------------------------------------------------------------------------
# Preflight: docker
# ---------------------------------------------------------------------------

check_docker() {
  if command -v docker >/dev/null 2>&1; then
    local docker_version
    docker_version="$(docker --version 2>/dev/null)" || docker_version="Docker (version unknown)"
    info "detected Docker: ${docker_version}"
    return 0
  fi

  info "Docker not found — will install"
  return 0
}

# ---------------------------------------------------------------------------
# Preflight: run all checks
# ---------------------------------------------------------------------------

run_preflight() {
  info "running preflight checks..."
  local failures=0

  for check in check_root check_os check_arch check_tun check_openssl \
               check_python check_systemd check_package_manager check_docker; do
    if ! "$check"; then
      failures=$((failures + 1))
    fi
  done

  if [ "$failures" -gt 0 ]; then
    die "${failures} preflight check(s) failed — fix the issues above and retry"
  fi

  info "all preflight checks passed"
}

# ---------------------------------------------------------------------------
# Version management
# ---------------------------------------------------------------------------

get_installed_version() {
  dpkg-query -W -f '${Version}' "$BIVIOUS_PKG" 2>/dev/null || echo ""
}

# Compare two Debian version strings.
# Returns 0 if $1 > $2, 1 otherwise.
version_gt() {
  dpkg --compare-versions "$1" gt "$2"
}

fetch_latest_version() {
  local manifest_content=""

  if command -v curl >/dev/null 2>&1; then
    manifest_content="$(curl -fsSL --connect-timeout 10 --max-time 30 "$MANIFEST_URL" 2>&1)" || \
      die "failed to fetch version manifest from ${MANIFEST_URL} — check network connectivity"
  elif command -v wget >/dev/null 2>&1; then
    manifest_content="$(wget -qO- --timeout=30 "$MANIFEST_URL" 2>&1)" || \
      die "failed to fetch version manifest from ${MANIFEST_URL} — check network connectivity"
  else
    die "neither curl nor wget available — cannot fetch version manifest"
  fi

  if [ -z "$manifest_content" ]; then
    die "failed to fetch version manifest from ${MANIFEST_URL}"
  fi

  # Parse "latest" field from JSON without jq.
  # Anchored to line start; version value restricted to valid Debian version chars.
  local version
  version="$(printf '%s' "$manifest_content" | \
    sed -n 's/^[[:space:]]*"latest"[[:space:]]*:[[:space:]]*"\([0-9][0-9a-zA-Z.+~-]*\)".*/\1/p' | \
    head -n1)"

  if [ -z "$version" ]; then
    die "could not parse latest version from manifest"
  fi

  echo "$version"
}

# ---------------------------------------------------------------------------
# Download and checksum verification
# ---------------------------------------------------------------------------

download_file() {
  local url="$1"
  local dest="$2"

  if command -v curl >/dev/null 2>&1; then
    curl -fsSL --connect-timeout 10 --max-time 120 -o "$dest" "$url" || \
      die "failed to download ${url}"
  elif command -v wget >/dev/null 2>&1; then
    wget -q --timeout=120 -O "$dest" "$url" || \
      die "failed to download ${url}"
  else
    die "neither curl nor wget available"
  fi
}

verify_checksum() {
  local file_path="$1"
  local checksum_url="$2"

  local checksum_content=""
  download_file "$checksum_url" "${file_path}.sha256"
  checksum_content="$(cat "${file_path}.sha256")"

  # The .sha256 file contains the hex hash, possibly followed by whitespace and
  # a filename reference.  Extract the first 64-hex-char token.
  local expected_hash
  expected_hash="$(printf '%s' "$checksum_content" | tr -d '[:space:]' | cut -c1-64)"

  if [ "${#expected_hash}" -ne 64 ]; then
    die "invalid SHA-256 checksum format in ${checksum_url}"
  fi

  local actual_hash
  actual_hash="$(openssl dgst -sha256 "$file_path" | awk '{print $NF}')"

  if [ "$expected_hash" != "$actual_hash" ]; then
    die "SHA-256 checksum mismatch for $(basename "$file_path"):
  expected: ${expected_hash}
  actual:   ${actual_hash}"
  fi

  info "checksum verified: $(basename "$file_path")"
  rm -f "${file_path}.sha256"
}

# ---------------------------------------------------------------------------
# Install or update
# ---------------------------------------------------------------------------

install_or_update() {
  local installed_version
  installed_version="$(get_installed_version)"

  local target_version="${BIVIOUS_VERSION:-}"
  if [ -z "$target_version" ]; then
    info "resolving latest version..."
    target_version="$(fetch_latest_version)"
  fi

  info "target version: ${target_version}"

  # Determine action
  local action="install"
  if [ -n "$installed_version" ]; then
    if [ "$installed_version" = "$target_version" ]; then
      info "Bivious ${installed_version} is already installed and up to date — nothing to do"
      return 0
    fi

    if version_gt "$installed_version" "$target_version"; then
      die "installed version ${installed_version} is newer than available ${target_version} — refusing to downgrade"
    fi

    action="update"
    info "upgrading from ${installed_version} to ${target_version}"
  else
    info "no existing Bivious installation found — installing ${target_version}"
  fi

  # Prepare download directory
  WORK_DIR="$(mktemp -d)"

  local deb_filename="${BIVIOUS_PKG}_${target_version}_${DEB_ARCH}.deb"
  local deb_url="${RELEASE_BASE_URL}/${deb_filename}"
  local sha256_url="${deb_url}.sha256"
  local deb_path="${WORK_DIR}/${deb_filename}"

  # Download
  info "downloading ${deb_filename}..."
  download_file "$deb_url" "$deb_path"

  # Verify checksum
  info "verifying package integrity..."
  verify_checksum "$deb_path" "$sha256_url"

  # Install
  if [ "$action" = "update" ]; then
    info "stopping Bivious service..."
    systemctl stop "$BIVIOUS_SERVICE" 2>/dev/null || true
  fi

  info "installing package..."
  apt-get install -y --no-install-recommends --allow-downgrades "$deb_path" || \
    die "package installation failed"

  # Post-install service management
  info "reloading systemd daemon..."
  systemctl daemon-reload

  if [ "$action" = "install" ]; then
    info "enabling and starting Bivious service..."
    systemctl enable "$BIVIOUS_SERVICE"
    systemctl start "$BIVIOUS_SERVICE"
  else
    info "restarting Bivious service..."
    systemctl restart "$BIVIOUS_SERVICE"
  fi

  # Wait for service to become active
  info "waiting for Bivious service to become active (timeout: ${SERVICE_WAIT_TIMEOUT}s)..."
  local elapsed=0
  while [ "$elapsed" -lt "$SERVICE_WAIT_TIMEOUT" ]; do
    if systemctl is-active --quiet "$BIVIOUS_SERVICE"; then
      info "Bivious ${target_version} ${action} successful — service is active"
      return 0
    fi
    sleep 1
    elapsed=$((elapsed + 1))
  done

  die "Bivious service did not reach active state within ${SERVICE_WAIT_TIMEOUT}s — check: journalctl -u ${BIVIOUS_SERVICE}"
}

# ---------------------------------------------------------------------------
# Docker installation
# ---------------------------------------------------------------------------

install_docker() {
  info "installing Docker..."

  # Verify network is available before attempting package installation
  if command -v curl >/dev/null 2>&1; then
    curl -fsSL --connect-timeout 5 --max-time 5 "$MANIFEST_URL" >/dev/null 2>&1 || \
      die "no network connectivity — cannot install Docker"
  elif command -v wget >/dev/null 2>&1; then
    wget -q --spider --timeout=5 "$MANIFEST_URL" 2>/dev/null || \
      die "no network connectivity — cannot install Docker"
  fi

  info "refreshing package lists..."
  apt-get update || \
    die "apt-get update failed — Docker installation aborted"

  apt-get install -y docker.io || \
    die "Docker installation failed — Docker is mandatory for Bivious"

  info "enabling and starting Docker service..."
  if ! systemctl enable "$DOCKER_SERVICE" || ! systemctl start "$DOCKER_SERVICE"; then
    die "failed to start Docker service — check: systemctl status ${DOCKER_SERVICE}"
  fi

  # Brief pause — allows systemd to propagate service state before verification
  sleep 2

  info "waiting for Docker service to become active (timeout: ${DOCKER_WAIT_TIMEOUT}s)..."
  local elapsed=0
  while [ "$elapsed" -lt "$DOCKER_WAIT_TIMEOUT" ]; do
    if docker info >/dev/null 2>&1; then
      local docker_version
      docker_version="$(docker info --format '{{.ServerVersion}}' 2>/dev/null)" || \
        docker_version="$(docker --version 2>/dev/null)" || \
        docker_version="unknown"
      info "Docker installed and started successfully — version ${docker_version}"
      return 0
    fi
    sleep 1
    elapsed=$((elapsed + 1))
  done

  die "Docker service did not reach active state within ${DOCKER_WAIT_TIMEOUT}s — check: journalctl -u ${DOCKER_SERVICE}"
}

# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------

main() {
  info "Bivious installer"
  run_preflight

  # Install Docker if it wasn't found during preflight
  if ! command -v docker >/dev/null 2>&1; then
    install_docker
  fi

  install_or_update
  info "done"
}

main "$@"
