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

# One-click Tinyproxy installer/configurator for Linux servers.
# Default mode is Tailscale-safe: listen on the server's Tailscale IPv4
# address and allow clients from the Tailscale CGNAT range.

PORT="3128"
LISTEN_ADDRESS="auto"
ALLOW_LIST=()
AUTH_PAIR=""
OPEN_FIREWALL="1"
UNSAFE_PUBLIC_LISTEN="0"
CONFIG_PATH="/etc/tinyproxy/tinyproxy.conf"
SERVICE_NAME="tinyproxy"
BACKUP_PATH=""
TAILSCALE_IP=""
TAILSCALE_IFACE=""
WARNINGS=()
FIREWALL_NOTES=()
CHECK_NOTES=()

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

warn() {
  WARNINGS+=("$*")
  printf '[WARN] %s\n' "$*" >&2
}

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

have() {
  command -v "$1" >/dev/null 2>&1
}

usage() {
  cat <<'EOF'
Usage:
  sudo bash install_tinyproxy.sh [options]

Common examples:
  # Recommended for Tailscale: auto-detect Tailscale IP, port 3128,
  # allow the whole tailnet range.
  sudo bash install_tinyproxy.sh

  # Stricter: only allow one Synology/Tailscale client.
  sudo bash install_tinyproxy.sh --allow 100.92.18.7

  # Custom port.
  sudo bash install_tinyproxy.sh --port 7890 --allow 100.92.18.7

  # Add Basic Auth.
  sudo bash install_tinyproxy.sh --allow 100.92.18.7 --auth user:strong_password

  # Listen on a specific private/VPN IP.
  sudo bash install_tinyproxy.sh --listen-address 10.0.0.5 --allow 10.0.0.0/24

Options:
  --port PORT                 Proxy port, default: 3128
  --listen-address IP         Address to listen on. Default: auto
                               auto prefers Tailscale IPv4, otherwise 127.0.0.1
  --allow IP_OR_CIDR          Client IP/CIDR allowed to use the proxy. Repeatable.
                               Default with Tailscale: 100.64.0.0/10
  --auth USER:PASS            Enable Tinyproxy BasicAuth
  --no-firewall               Do not add ufw/firewalld allow rules
  --unsafe-public-listen      Allow listening on 0.0.0.0 or a public IPv4
                               Use only with strict firewall + auth
  -h, --help                  Show this help

Synology DSM fields after install:
  Address: the listen address printed at the end
  Port:    the proxy port printed at the end
EOF
}

add_allow_value() {
  local raw="$1"
  local part
  IFS=',' read -r -a parts <<<"$raw"
  for part in "${parts[@]}"; do
    part="${part//[[:space:]]/}"
    [ -n "$part" ] && ALLOW_LIST+=("$part")
  done
}

parse_args() {
  while [ "$#" -gt 0 ]; do
    case "$1" in
      --port)
        [ "$#" -ge 2 ] || die "--port requires a value"
        PORT="$2"
        shift 2
        ;;
      --listen-address|--listen)
        [ "$#" -ge 2 ] || die "--listen-address requires a value"
        LISTEN_ADDRESS="$2"
        shift 2
        ;;
      --allow)
        [ "$#" -ge 2 ] || die "--allow requires a value"
        add_allow_value "$2"
        shift 2
        ;;
      --auth)
        [ "$#" -ge 2 ] || die "--auth requires USER:PASS"
        AUTH_PAIR="$2"
        shift 2
        ;;
      --no-firewall)
        OPEN_FIREWALL="0"
        shift
        ;;
      --unsafe-public-listen)
        UNSAFE_PUBLIC_LISTEN="1"
        shift
        ;;
      -h|--help)
        usage
        exit 0
        ;;
      *)
        die "Unknown option: $1"
        ;;
    esac
  done
}

validate_inputs() {
  [[ "$PORT" =~ ^[0-9]+$ ]] || die "Port must be a number"
  [ "$PORT" -ge 1 ] && [ "$PORT" -le 65535 ] || die "Port must be 1-65535"

  if [ -n "$AUTH_PAIR" ]; then
    [[ "$AUTH_PAIR" == *:* ]] || die "--auth must be USER:PASS"
    [[ "$AUTH_PAIR" != *[[:space:]]* ]] || die "--auth cannot contain spaces"
    [ -n "${AUTH_PAIR%%:*}" ] || die "--auth user cannot be empty"
    [ -n "${AUTH_PAIR#*:}" ] || die "--auth password cannot be empty"
  fi
}

ensure_root() {
  if [ "$(id -u)" -ne 0 ]; then
    have sudo || die "Please run as root, or install sudo first"
    exec sudo -E bash "$0" "$@"
  fi
}

show_help_without_root() {
  local arg
  for arg in "$@"; do
    case "$arg" in
      -h|--help)
        usage
        exit 0
        ;;
    esac
  done
}

install_packages() {
  log "Installing Tinyproxy and basic network tools..."

  if have apt-get; then
    export DEBIAN_FRONTEND=noninteractive
    apt-get update
    apt-get install -y tinyproxy curl iproute2 ca-certificates
  elif have dnf; then
    dnf install -y tinyproxy curl iproute ca-certificates
  elif have yum; then
    yum install -y tinyproxy curl iproute ca-certificates || {
      warn "yum could not install tinyproxy directly. If this is RHEL/CentOS, enable EPEL and rerun."
      return 1
    }
  elif have zypper; then
    zypper --non-interactive install tinyproxy curl iproute2 ca-certificates
  elif have apk; then
    apk add --no-cache tinyproxy curl iproute2 ca-certificates
  elif have pacman; then
    pacman -Sy --noconfirm tinyproxy curl iproute2 ca-certificates
  else
    die "Unsupported package manager. Install tinyproxy manually, then rerun this script."
  fi
}

detect_tailscale() {
  if have tailscale; then
    TAILSCALE_IP="$(tailscale ip -4 2>/dev/null | head -n 1 || true)"
  fi

  if have ip; then
    if ip link show tailscale0 >/dev/null 2>&1; then
      TAILSCALE_IFACE="tailscale0"
    else
      TAILSCALE_IFACE="$(ip -o link show 2>/dev/null | awk -F': ' '/tailscale/ {print $2; exit}' || true)"
    fi
  fi
}

is_ipv4() {
  local ip="$1"
  [[ "$ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] || return 1
  local a b c d
  IFS=. read -r a b c d <<<"$ip"
  for octet in "$a" "$b" "$c" "$d"; do
    [ "$octet" -ge 0 ] 2>/dev/null && [ "$octet" -le 255 ] 2>/dev/null || return 1
  done
}

is_private_or_loopback_ipv4() {
  local ip="$1"
  is_ipv4 "$ip" || return 1
  local a b c d
  IFS=. read -r a b c d <<<"$ip"

  [ "$a" -eq 10 ] && return 0
  [ "$a" -eq 127 ] && return 0
  [ "$a" -eq 192 ] && [ "$b" -eq 168 ] && return 0
  [ "$a" -eq 172 ] && [ "$b" -ge 16 ] && [ "$b" -le 31 ] && return 0
  [ "$a" -eq 100 ] && [ "$b" -ge 64 ] && [ "$b" -le 127 ] && return 0

  return 1
}

choose_listen_address() {
  detect_tailscale

  if [ "$LISTEN_ADDRESS" = "auto" ]; then
    if [ -n "$TAILSCALE_IP" ]; then
      LISTEN_ADDRESS="$TAILSCALE_IP"
      log "Auto-selected Tailscale listen address: $LISTEN_ADDRESS"
    else
      LISTEN_ADDRESS="127.0.0.1"
      warn "No Tailscale IPv4 found. Listening on 127.0.0.1 only. Pass --listen-address <VPN/private IP> if needed."
    fi
  fi

  if [ "$LISTEN_ADDRESS" != "0.0.0.0" ] && ! is_ipv4 "$LISTEN_ADDRESS"; then
    die "Only IPv4 listen addresses are supported by this script: $LISTEN_ADDRESS"
  fi

  if [ "$LISTEN_ADDRESS" = "0.0.0.0" ] && [ "$UNSAFE_PUBLIC_LISTEN" != "1" ]; then
    die "Refusing to listen on 0.0.0.0 without --unsafe-public-listen"
  fi

  if [ "$LISTEN_ADDRESS" != "0.0.0.0" ] && ! is_private_or_loopback_ipv4 "$LISTEN_ADDRESS"; then
    [ "$UNSAFE_PUBLIC_LISTEN" = "1" ] || die "Refusing to listen on public IPv4 $LISTEN_ADDRESS without --unsafe-public-listen"
    warn "Listening on public IPv4 $LISTEN_ADDRESS. Use firewall rules and BasicAuth."
  fi

  if [ "${#ALLOW_LIST[@]}" -eq 0 ]; then
    ALLOW_LIST+=("127.0.0.1")
    if [ -n "$TAILSCALE_IP" ] || [ "$LISTEN_ADDRESS" = "0.0.0.0" ]; then
      ALLOW_LIST+=("100.64.0.0/10")
      log "No --allow provided. Defaulting to Tailscale client range: 100.64.0.0/10"
    else
      warn "No --allow provided. Only localhost is allowed; remote clients cannot use this proxy yet."
    fi
  else
    ALLOW_LIST=("127.0.0.1" "${ALLOW_LIST[@]}")
    if [ "$LISTEN_ADDRESS" != "0.0.0.0" ] && [ "$LISTEN_ADDRESS" != "127.0.0.1" ]; then
      ALLOW_LIST+=("$LISTEN_ADDRESS")
    fi
  fi
}

ensure_tinyproxy_user() {
  if ! getent group tinyproxy >/dev/null 2>&1; then
    if have groupadd; then
      groupadd --system tinyproxy
    elif have addgroup; then
      addgroup -S tinyproxy
    fi
  fi

  if ! id -u tinyproxy >/dev/null 2>&1; then
    if have useradd; then
      useradd --system --gid tinyproxy --home-dir /nonexistent --shell /usr/sbin/nologin tinyproxy 2>/dev/null \
        || useradd --system --gid tinyproxy --home-dir /nonexistent --shell /sbin/nologin tinyproxy
    elif have adduser; then
      adduser -S -D -H -s /sbin/nologin -G tinyproxy tinyproxy
    fi
  fi

  mkdir -p /var/log/tinyproxy /run/tinyproxy
  if id -u tinyproxy >/dev/null 2>&1; then
    chown tinyproxy:tinyproxy /var/log/tinyproxy /run/tinyproxy 2>/dev/null || true
  fi
}

write_config() {
  log "Writing Tinyproxy config: $CONFIG_PATH"
  mkdir -p "$(dirname "$CONFIG_PATH")"

  if [ -f "$CONFIG_PATH" ]; then
    BACKUP_PATH="${CONFIG_PATH}.bak.$(date +%Y%m%d%H%M%S)"
    cp -a "$CONFIG_PATH" "$BACKUP_PATH"
  fi

  {
    printf '# Managed by install_tinyproxy.sh on %s\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
    printf 'User tinyproxy\n'
    printf 'Group tinyproxy\n'
    printf 'Port %s\n' "$PORT"
    printf 'Listen %s\n' "$LISTEN_ADDRESS"
    printf 'Timeout 600\n'
    printf 'LogFile "/var/log/tinyproxy/tinyproxy.log"\n'
    printf 'LogLevel Info\n'
    printf 'PidFile "/run/tinyproxy/tinyproxy.pid"\n'
    printf 'MaxClients 100\n'
    printf 'MinSpareServers 5\n'
    printf 'MaxSpareServers 20\n'
    printf 'StartServers 10\n'
    printf 'ViaProxyName "tinyproxy"\n'
    printf 'DisableViaHeader Yes\n'
    printf 'ConnectPort 443\n'
    printf 'ConnectPort 563\n'
    if [ -n "$AUTH_PAIR" ]; then
      printf 'BasicAuth %s %s\n' "${AUTH_PAIR%%:*}" "${AUTH_PAIR#*:}"
    fi
    local allow
    for allow in "${ALLOW_LIST[@]}"; do
      printf 'Allow %s\n' "$allow"
    done
  } >"$CONFIG_PATH"
}

restart_service() {
  log "Starting Tinyproxy service..."

  if have systemctl && [ -d /run/systemd/system ]; then
    systemctl daemon-reload || true
    systemctl enable "$SERVICE_NAME" >/dev/null 2>&1 || true
    if ! systemctl restart "$SERVICE_NAME"; then
      journalctl -u "$SERVICE_NAME" -n 80 --no-pager || true
      die "Tinyproxy failed to start. See logs above."
    fi
  elif have rc-service; then
    rc-update add "$SERVICE_NAME" default >/dev/null 2>&1 || true
    rc-service "$SERVICE_NAME" restart || die "Tinyproxy failed to start via OpenRC"
  elif have service; then
    service "$SERVICE_NAME" restart || die "Tinyproxy failed to start via service"
  else
    die "Could not find systemctl, rc-service, or service to start Tinyproxy"
  fi
}

open_firewall_if_possible() {
  if [ "$OPEN_FIREWALL" != "1" ]; then
    FIREWALL_NOTES+=("Skipped by --no-firewall")
    return
  fi

  if have ufw && ufw status 2>/dev/null | grep -qi '^Status: active'; then
    if [ -n "$TAILSCALE_IFACE" ]; then
      ufw allow in on "$TAILSCALE_IFACE" to any port "$PORT" proto tcp comment "tinyproxy tailscale" >/dev/null
      FIREWALL_NOTES+=("ufw: allowed TCP $PORT on interface $TAILSCALE_IFACE")
    else
      local allow
      for allow in "${ALLOW_LIST[@]}"; do
        [ "$allow" = "127.0.0.1" ] && continue
        ufw allow from "$allow" to any port "$PORT" proto tcp comment "tinyproxy" >/dev/null || true
      done
      FIREWALL_NOTES+=("ufw: attempted source-based allow rules for TCP $PORT")
    fi
    return
  fi

  if have firewall-cmd && firewall-cmd --state >/dev/null 2>&1; then
    local allow
    for allow in "${ALLOW_LIST[@]}"; do
      [ "$allow" = "127.0.0.1" ] && continue
      firewall-cmd --permanent --add-rich-rule="rule family=\"ipv4\" source address=\"$allow\" port port=\"$PORT\" protocol=\"tcp\" accept" >/dev/null || true
    done
    firewall-cmd --reload >/dev/null || true
    FIREWALL_NOTES+=("firewalld: attempted rich-rule allow for TCP $PORT from allowed client ranges")
    return
  fi

  if have nft && nft list ruleset >/dev/null 2>&1; then
    FIREWALL_NOTES+=("nftables detected, but no persistent rule was added automatically")
    return
  fi

  if have iptables && iptables -S >/dev/null 2>&1; then
    FIREWALL_NOTES+=("iptables detected, but no persistent rule was added automatically")
    return
  fi

  FIREWALL_NOTES+=("No active ufw/firewalld detected; nothing to open locally")
}

check_listener() {
  log "Checking listener and proxy connectivity..."

  if have ss; then
    if ss -H -lnt 2>/dev/null | awk '{print $4}' | grep -Eq "(\[?${LISTEN_ADDRESS}\]?:${PORT}|0\.0\.0\.0:${PORT}|\*:${PORT})$"; then
      CHECK_NOTES+=("Listener: OK, TCP $PORT is listening")
    else
      CHECK_NOTES+=("Listener: NOT FOUND by ss for TCP $PORT")
    fi
  fi

  local proxy_url
  if [ -n "$AUTH_PAIR" ]; then
    proxy_url="http://${AUTH_PAIR}@${LISTEN_ADDRESS}:${PORT}"
  else
    proxy_url="http://${LISTEN_ADDRESS}:${PORT}"
  fi

  if have curl; then
    if curl -fsSIL --max-time 12 -x "$proxy_url" https://www.gstatic.com/generate_204 >/dev/null 2>&1; then
      CHECK_NOTES+=("Proxy test: OK via https://www.gstatic.com/generate_204")
    else
      CHECK_NOTES+=("Proxy test: FAILED from this server; check egress network, Allow rules, or auth")
    fi
  fi

  if [ "$LISTEN_ADDRESS" != "0.0.0.0" ] && is_private_or_loopback_ipv4 "$LISTEN_ADDRESS"; then
    CHECK_NOTES+=("Bind safety: listening on private/VPN/loopback IPv4 $LISTEN_ADDRESS")
  else
    CHECK_NOTES+=("Bind safety: public or wildcard bind; verify firewall/security group manually")
  fi
}

service_status_line() {
  if have systemctl && [ -d /run/systemd/system ]; then
    systemctl is-active "$SERVICE_NAME" 2>/dev/null || true
  elif have rc-service; then
    rc-service "$SERVICE_NAME" status 2>/dev/null | head -n 1 || true
  else
    printf 'unknown'
  fi
}

print_summary() {
  local auth_display="disabled"
  local proxy_for_test="http://${LISTEN_ADDRESS}:${PORT}"
  if [ -n "$AUTH_PAIR" ]; then
    auth_display="enabled (${AUTH_PAIR%%:*}:********)"
    proxy_for_test="http://${AUTH_PAIR%%:*}:PASSWORD@${LISTEN_ADDRESS}:${PORT}"
  fi

  printf '\n'
  printf '================ Tinyproxy setup complete ================\n'
  printf 'Service:       %s (%s)\n' "$SERVICE_NAME" "$(service_status_line)"
  printf 'Config:        %s\n' "$CONFIG_PATH"
  [ -n "$BACKUP_PATH" ] && printf 'Backup:        %s\n' "$BACKUP_PATH"
  printf 'Listen:        %s\n' "$LISTEN_ADDRESS"
  printf 'Port:          %s\n' "$PORT"
  printf 'BasicAuth:     %s\n' "$auth_display"
  printf 'Allowed:       %s\n' "${ALLOW_LIST[*]}"
  [ -n "$TAILSCALE_IP" ] && printf 'Tailscale IP:  %s\n' "$TAILSCALE_IP"
  [ -n "$TAILSCALE_IFACE" ] && printf 'Tailscale IF:  %s\n' "$TAILSCALE_IFACE"
  printf '\n'
  printf 'Synology DSM proxy fields:\n'
  printf '  Address: %s\n' "$LISTEN_ADDRESS"
  printf '  Port:    %s\n' "$PORT"
  printf '\n'
  printf 'Test from Synology SSH:\n'
  printf '  curl -I -x %s https://www.youtube.com\n' "$proxy_for_test"
  printf '\n'

  if [ "${#FIREWALL_NOTES[@]}" -gt 0 ]; then
    printf 'Firewall:\n'
    local note
    for note in "${FIREWALL_NOTES[@]}"; do
      printf '  - %s\n' "$note"
    done
  fi

  if [ "${#CHECK_NOTES[@]}" -gt 0 ]; then
    printf 'Checks:\n'
    local note
    for note in "${CHECK_NOTES[@]}"; do
      printf '  - %s\n' "$note"
    done
  fi

  if [ "${#WARNINGS[@]}" -gt 0 ]; then
    printf 'Warnings:\n'
    local note
    for note in "${WARNINGS[@]}"; do
      printf '  - %s\n' "$note"
    done
  fi

  printf '==========================================================\n'
}

main() {
  show_help_without_root "$@"
  ensure_root "$@"
  parse_args "$@"
  validate_inputs
  install_packages
  choose_listen_address
  ensure_tinyproxy_user
  write_config
  open_firewall_if_possible
  restart_service
  check_listener
  print_summary
}

main "$@"
