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

INSTALL_DIR="${VPN_PROXY_INSTALL_DIR:-$HOME/.vpn-proxy-client}"
REPO_URL="${VPN_PROXY_REPO_URL:-https://git.dokops.ru/dokril/vpn-proxy.git}"
BRANCH="${VPN_PROXY_BRANCH:-master}"
COMPOSE_FILE="docker-compose.client.yml"
DEFAULT_PROXY_PORT="8080"
REQUESTED_PROXY_PORT="${VPN_PROXY_CLIENT_PORT:-}"
REQUESTED_UI_PORT="${VPN_PROXY_CLIENT_UI_PORT:-${CLIENT_UI_PORT:-}}"
CLIENT_CONTAINER_NAME="vpn-proxy-client"

log() {
  printf '[vpn-proxy-client] %s\n' "$*"
}

die() {
  printf '[vpn-proxy-client] error: %s\n' "$*" >&2
  exit 1
}

need() {
  command -v "$1" >/dev/null 2>&1 || die "$1 is required"
}

is_valid_port() {
  case "$1" in
    ''|*[!0-9]*) return 1 ;;
  esac
  [ "$1" -ge 1024 ] && [ "$1" -le 65535 ]
}

ask_proxy_port() {
  local value=""
  if [ -n "$REQUESTED_PROXY_PORT" ]; then
    if ! is_valid_port "$REQUESTED_PROXY_PORT"; then
      die "VPN_PROXY_CLIENT_PORT must be a port from 1024 to 65535"
    fi
    printf '%s\n' "$REQUESTED_PROXY_PORT"
    return 0
  fi

  if [ -r /dev/tty ]; then
    while true; do
      printf 'Proxy port for local apps [%s]: ' "$DEFAULT_PROXY_PORT" >/dev/tty
      IFS= read -r value </dev/tty || value=""
      value="${value:-$DEFAULT_PROXY_PORT}"
      if is_valid_port "$value"; then
        printf '%s\n' "$value"
        return 0
      fi
      printf 'Enter a port from 1024 to 65535.\n' >/dev/tty
    done
  fi

  if ! is_valid_port "$DEFAULT_PROXY_PORT"; then
    die "VPN_PROXY_CLIENT_PORT must be a port from 1024 to 65535"
  fi
  printf '%s\n' "$DEFAULT_PROXY_PORT"
}

published_port_conflicts() {
  local port="$1"
  local line

  while IFS= read -r line; do
    [ -n "$line" ] || continue
    case "$line" in
      "${CLIENT_CONTAINER_NAME}"$'\t'*) ;;
      *) printf '%s\n' "$line" ;;
    esac
  done < <(docker ps --filter "publish=${port}" --format '{{.Names}}	{{.Ports}}')
}

proxy_port_conflicts() {
  published_port_conflicts "$1"
}

assert_proxy_port_available() {
  local port="$1"
  local conflicts

  conflicts="$(proxy_port_conflicts "$port")"
  if [ -z "$conflicts" ]; then
    return 0
  fi

  printf '[vpn-proxy-client] proxy port %s is already used:\n%s\n' \
    "$port" "$conflicts" >&2
  die "choose another proxy port with VPN_PROXY_CLIENT_PORT=<port> or stop the conflicting container"
}

assert_single_port_available() {
  local label="$1"
  local port="$2"
  local conflicts

  conflicts="$(published_port_conflicts "$port")"
  if [ -z "$conflicts" ]; then
    return 0
  fi

  printf '[vpn-proxy-client] %s port %s is already used:\n%s\n' \
    "$label" "$port" "$conflicts" >&2
  die "choose another ${label} port or stop the conflicting container"
}

first_free_port() {
  local start="$1"
  local port

  for port in $(seq "$start" 65535); do
    if [ -z "$(published_port_conflicts "$port")" ]; then
      printf '%s\n' "$port"
      return 0
    fi
  done
  return 1
}

choose_ui_port() {
  local value="$1"
  local suggested

  if ! is_valid_port "$value"; then
    die "CLIENT_UI_PORT must be a port from 1024 to 65535"
  fi

  if [ -z "$(published_port_conflicts "$value")" ]; then
    printf '%s\n' "$value"
    return 0
  fi

  if [ -n "$REQUESTED_UI_PORT" ] || [ ! -r /dev/tty ]; then
    assert_single_port_available "UI" "$value"
  fi

  suggested="$(first_free_port "$((value + 1))" || true)"
  suggested="${suggested:-3457}"
  while true; do
    printf 'UI port %s is busy. Choose UI port [%s]: ' "$value" "$suggested" >/dev/tty
    IFS= read -r value </dev/tty || value=""
    value="${value:-$suggested}"
    if is_valid_port "$value" && [ -z "$(published_port_conflicts "$value")" ]; then
      printf '%s\n' "$value"
      return 0
    fi
    printf 'Enter a free port from 1024 to 65535.\n' >/dev/tty
  done
}

assert_ui_outside_proxy_range() {
  if [ "$UI_PORT" = "$PROXY_PORT" ]; then
    die "UI port ${UI_PORT} overlaps proxy port"
  fi
}

wait_for_client_ui() {
  local ui_port="${UI_PORT:-3456}"
  local ui_url="http://127.0.0.1:${ui_port}/api/state"
  local attempt

  for attempt in $(seq 1 30); do
    if curl --noproxy "*" -fsS "$ui_url" >/dev/null 2>&1; then
      return 0
    fi
    sleep 1
  done

  printf '\n[vpn-proxy-client] client did not become ready at %s\n' "$ui_url" >&2
  printf '[vpn-proxy-client] docker compose status:\n' >&2
  docker compose -f "$COMPOSE_FILE" ps >&2 || true
  printf '\n[vpn-proxy-client] recent service logs:\n' >&2
  docker compose -f "$COMPOSE_FILE" logs --tail=120 vpn-proxy-client >&2 || true
  die "client UI is not ready; see Docker status and logs above"
}

set_env_value() {
  local key="$1"
  local value="$2"
  local tmp
  tmp="$(mktemp)"

  if [ -f .env ] && grep -q "^${key}=" .env; then
    awk -v key="$key" -v value="$value" '
      BEGIN { prefix = key "=" }
      index($0, prefix) == 1 { print key "=" value; next }
      { print }
    ' .env > "$tmp"
  else
    [ -f .env ] && cat .env > "$tmp"
    printf '%s=%s\n' "$key" "$value" >> "$tmp"
  fi

  mv "$tmp" .env
}

get_env_value() {
  local key="$1"
  [ -f .env ] || return 0
  awk -v key="$key" '
    BEGIN { prefix = key "=" }
    index($0, prefix) == 1 { print substr($0, length(prefix) + 1); exit }
  ' .env
}

if [[ "$(uname -s)" != "Darwin" ]]; then
  die "this installer is intended for macOS"
fi

need git
need docker
need curl

docker compose version >/dev/null 2>&1 || die "Docker Compose plugin is required"
docker info >/dev/null 2>&1 || die "Docker Desktop is not running"

if [[ -d "$INSTALL_DIR/.git" ]]; then
  log "updating $INSTALL_DIR"
  git -C "$INSTALL_DIR" fetch origin "$BRANCH"
  git -C "$INSTALL_DIR" checkout "$BRANCH"
  git -C "$INSTALL_DIR" pull --ff-only origin "$BRANCH"
else
  log "cloning $REPO_URL#$BRANCH to $INSTALL_DIR"
  mkdir -p "$(dirname "$INSTALL_DIR")"
  git clone --branch "$BRANCH" "$REPO_URL" "$INSTALL_DIR"
fi

cd "$INSTALL_DIR"

if [[ ! -f .env && -f .env.example ]]; then
  cp .env.example .env
fi

PROXY_PORT="$(ask_proxy_port)"
assert_proxy_port_available "$PROXY_PORT"
PROXY_PORT_END="$PROXY_PORT"
UI_PORT="${REQUESTED_UI_PORT:-$(get_env_value CLIENT_UI_PORT)}"
UI_PORT="${UI_PORT:-3456}"
UI_PORT="$(choose_ui_port "$UI_PORT")"
assert_ui_outside_proxy_range

set_env_value APP_MODE client
set_env_value CLIENT_UI_PORT "$UI_PORT"
set_env_value CLIENT_PROXY_PORT "$PROXY_PORT"
set_env_value CLIENT_PROXY_PORT_START "$PROXY_PORT"
set_env_value CLIENT_PROXY_PORT_END "$PROXY_PORT_END"
set_env_value PROXY_PORT "$PROXY_PORT"

log "UI port: http://127.0.0.1:${UI_PORT}"
log "proxy port: 127.0.0.1:${PROXY_PORT}"

log "building and starting Docker client"
docker compose -f "$COMPOSE_FILE" up -d --build
wait_for_client_ui

cat <<EOF

VPN Proxy Client is running.

UI:
  http://127.0.0.1:${UI_PORT}

Proxy:
  HTTP/SOCKS5 127.0.0.1:${PROXY_PORT}
  This is the only Docker-published proxy port. Re-run the installer with VPN_PROXY_CLIENT_PORT=<port> to change it.

Useful commands:
  cd ~/.vpn-proxy-client
  docker compose -f docker-compose.client.yml logs -f
  docker compose -f docker-compose.client.yml restart
  docker compose -f docker-compose.client.yml down

Optional macOS system proxy example:
  networksetup -setwebproxy Wi-Fi 127.0.0.1 ${PROXY_PORT}
  networksetup -setsecurewebproxy Wi-Fi 127.0.0.1 ${PROXY_PORT}
  networksetup -setsocksfirewallproxy Wi-Fi 127.0.0.1 ${PROXY_PORT}

Disable later:
  networksetup -setwebproxystate Wi-Fi off
  networksetup -setsecurewebproxystate Wi-Fi off
  networksetup -setsocksfirewallproxystate Wi-Fi off

EOF
