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

usage() {
  cat <<USAGE
Usage:
  sudo bash configure-minikube-nft.sh [options]

Configures:
  - ip custom_nat table (DNAT + MASQUERADE + optional OUTPUT)
  - ip filter FORWARD rule to allow inbound to minikube NodePort
  - ip filter DOCKER-USER rule to allow the same traffic
  - Saves ruleset to /etc/nftables.conf

Options:
  --listen-port PORT         External port to listen on, default: 8443
  --namespace NAME           Ingress namespace, default: ingress-nginx
  --service NAME             Ingress service, default: ingress-nginx-controller
  --enable-local-output      Also add OUTPUT DNAT so local VM access to VM_IP:PORT works
  --log-file PATH            Log file path, default: /tmp/configure-minikube-nft.log
  -h, --help                 Show help
USAGE
}

require_root() {
  if [[ ${EUID} -ne 0 ]]; then
    echo "Run as root (sudo)." >&2
    exit 1
  fi
}

need_cmd() {
  command -v "$1" >/dev/null 2>&1 || {
    echo "Missing required command: $1" >&2
    exit 1
  }
}

LISTEN_PORT="8443"
NAMESPACE="ingress-nginx"
SERVICE="ingress-nginx-controller"
ENABLE_LOCAL_OUTPUT="false"
LOG_FILE="/tmp/configure-minikube-nft.log"

while [[ $# -gt 0 ]]; do
  case "$1" in
    --listen-port)         LISTEN_PORT="$2";         shift 2 ;;
    --namespace)           NAMESPACE="$2";           shift 2 ;;
    --service)             SERVICE="$2";             shift 2 ;;
    --enable-local-output) ENABLE_LOCAL_OUTPUT="true"; shift ;;
    --log-file)            LOG_FILE="$2";            shift 2 ;;
    -h|--help)             usage; exit 0 ;;
    *) echo "Unknown argument: $1" >&2; usage; exit 1 ;;
  esac
done

require_root

mkdir -p "$(dirname "$LOG_FILE")"
touch "$LOG_FILE"
exec > >(tee -a "$LOG_FILE") 2>&1

on_error() {
  local exit_code=$?
  local line_no=${BASH_LINENO[0]:-unknown}
  echo
  echo "ERROR: command failed with exit code ${exit_code} at line ${line_no}." >&2
  echo "See log: $LOG_FILE" >&2
  exit "$exit_code"
}
trap on_error ERR

need_cmd nft
need_cmd ip
need_cmd kubectl
need_cmd minikube
need_cmd python3
need_cmd awk
need_cmd grep
need_cmd sed
need_cmd getent
need_cmd cut
need_cmd sudo

log() {
  echo "[$(date '+%F %T')] $*"
}

run() {
  log "+ $*"
  "$@"
}

ORIG_USER="${SUDO_USER:-${USER}}"
ORIG_HOME="$(getent passwd "$ORIG_USER" | cut -d: -f6)"

if [[ -z "$ORIG_HOME" || ! -d "$ORIG_HOME" ]]; then
  echo "Could not determine home directory for original user: $ORIG_USER" >&2
  exit 1
fi

log "Original user detected: $ORIG_USER"
log "Original home: $ORIG_HOME"

run_as_orig_user() {
  sudo -u "$ORIG_USER" env \
    HOME="$ORIG_HOME" \
    KUBECONFIG="${KUBECONFIG:-$ORIG_HOME/.kube/config}" \
    MINIKUBE_HOME="${MINIKUBE_HOME:-$ORIG_HOME/.minikube}" \
    "$@"
}

append_rule_if_missing() {
  local family="$1" table="$2" chain="$3" rule="$4"
  if nft list chain "$family" "$table" "$chain" 2>/dev/null | grep -qF -- "$rule"; then
    log "Rule already present in $family $table $chain: $rule"
  else
    run nft add rule "$family" "$table" "$chain" $rule
  fi
}

insert_rule_if_missing() {
  local family="$1" table="$2" chain="$3" rule="$4"
  if nft list chain "$family" "$table" "$chain" 2>/dev/null | grep -qF -- "$rule"; then
    log "Rule already present in $family $table $chain: $rule"
  else
    run nft insert rule "$family" "$table" "$chain" $rule
  fi
}

ensure_chain() {
  local family="$1" table="$2" chain="$3" type_clause="$4"
  if nft list chain "$family" "$table" "$chain" >/dev/null 2>&1; then
    log "Chain already exists: $family $table $chain"
  else
    run nft add chain "$family" "$table" "$chain" "$type_clause"
  fi
}

log "Starting minikube nftables configuration"
log "Log file: $LOG_FILE"
log "Parameters: listen_port=$LISTEN_PORT namespace=$NAMESPACE service=$SERVICE enable_local_output=$ENABLE_LOCAL_OUTPUT"

log "Detecting VM source IP"
VM_IP=$(ip -4 route get 1.1.1.1 | awk '{for(i=1;i<=NF;i++) if($i=="src") {print $(i+1); exit}}')
log "VM_IP=$VM_IP"

log "Checking minikube status as original user"
run_as_orig_user minikube status

log "Detecting minikube IP as original user"
MINIKUBE_IP=$(run_as_orig_user minikube ip)
if [[ -z "$MINIKUBE_IP" ]]; then
  echo "Could not determine minikube IP" >&2
  exit 1
fi
log "MINIKUBE_IP=$MINIKUBE_IP"

log "Looking up HTTPS NodePort for $NAMESPACE/$SERVICE as original user"
MINIKUBE_NODEPORT=$(run_as_orig_user kubectl -n "$NAMESPACE" get svc "$SERVICE" -o jsonpath='{.spec.ports[?(@.port==443)].nodePort}')
if [[ -z "$MINIKUBE_NODEPORT" ]]; then
  echo "Could not determine HTTPS NodePort from service $NAMESPACE/$SERVICE" >&2
  exit 1
fi
log "MINIKUBE_NODEPORT=$MINIKUBE_NODEPORT"

log "Detecting bridge / egress interface to reach minikube IP"
BRIDGE=$(ip route get "$MINIKUBE_IP" 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="dev") {print $(i+1); exit}}')
if [[ -z "$BRIDGE" ]]; then
  echo "Could not determine bridge interface for $MINIKUBE_IP" >&2
  exit 1
fi
log "BRIDGE=$BRIDGE"

MINIKUBE_SUBNET=$(python3 - <<PY
import ipaddress
ip = ipaddress.ip_address("$MINIKUBE_IP")
net = ipaddress.ip_network(f"{ip}/24", strict=False)
print(net)
PY
)
log "MINIKUBE_SUBNET=$MINIKUBE_SUBNET"

log "Ensuring ip custom_nat table exists"
if nft list table ip custom_nat >/dev/null 2>&1; then
  log "Table already exists: ip custom_nat"
else
  run nft add table ip custom_nat
fi

ensure_chain ip custom_nat prerouting '{ type nat hook prerouting priority dstnat; policy accept; }'
ensure_chain ip custom_nat postrouting '{ type nat hook postrouting priority srcnat; policy accept; }'

append_rule_if_missing ip custom_nat prerouting \
  "ip daddr $VM_IP tcp dport $LISTEN_PORT dnat to $MINIKUBE_IP:$MINIKUBE_NODEPORT"
append_rule_if_missing ip custom_nat postrouting \
  "ip saddr $MINIKUBE_SUBNET masquerade"

if [[ "$ENABLE_LOCAL_OUTPUT" == "true" ]]; then
  ensure_chain ip custom_nat output '{ type nat hook output priority -100; policy accept; }'
  append_rule_if_missing ip custom_nat output \
    "ip daddr $VM_IP tcp dport $LISTEN_PORT dnat to $MINIKUBE_IP:$MINIKUBE_NODEPORT"
fi

log "Ensuring DOCKER-USER chain exists in ip filter"
if nft list chain ip filter DOCKER-USER >/dev/null 2>&1; then
  log "Chain already exists: ip filter DOCKER-USER"
else
  echo "DOCKER-USER chain was not found. Docker may not have created it yet; start Docker/minikube first." >&2
  exit 1
fi

# Allow new inbound flows to minikube NodePort through FORWARD
log "Inserting FORWARD accept rule for inbound connections to minikube NodePort"
insert_rule_if_missing ip filter FORWARD \
  "oifname \"$BRIDGE\" ip daddr $MINIKUBE_IP tcp dport $MINIKUBE_NODEPORT ct state new counter accept"

# Allow same traffic in DOCKER-USER (before Docker's own rules)
log "Adding DOCKER-USER accept rule for minikube NodePort"
append_rule_if_missing ip filter DOCKER-USER \
  "oifname \"$BRIDGE\" ip daddr $MINIKUBE_IP tcp dport $MINIKUBE_NODEPORT counter accept"

if command -v systemctl >/dev/null 2>&1; then
  log "Saving current nftables ruleset to /etc/nftables.conf"
  nft list ruleset > /etc/nftables.conf
  run systemctl enable nftables || true
  run systemctl start nftables || true
fi

log "Configuration complete"
log "Relevant checks:"
echo "  sudo nft list table ip custom_nat"
echo "  sudo nft list chain ip filter FORWARD"
echo "  sudo nft list chain ip filter DOCKER-USER"
echo "  curl -vk https://$VM_IP:$LISTEN_PORT/"
echo "  tail -n 100 $LOG_FILE"
