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

ACTION="${1:-prepare}"
NETWORK='learnitive-os-egress'
DEFAULT_SUBNET='172.30.240.0/24'
ALTERNATE_SUBNET='172.30.241.0/24'
CHAIN='LEARNITIVE-OS-EGRESS'
OWNER="${LEARNITIVE_DEPLOYMENT:-www.learnitive.com}"
ALLOW_WWW_PROMOTION="${LEARNITIVE_ALLOW_WWW_PROMOTION:-0}"
CONFIG_ROOT="${LEARNITIVE_CONFIG_ROOT:-/etc/learnitive}"
APP_ROOT="${LEARNITIVE_APP_ROOT:-/var/www/learnitive.com/public_html}"
MARKER_FILE="$CONFIG_ROOT/.deployment"
RULE_COMMENT='learnitive-os'
PROXY_PORT="${LEARNITIVE_PACKAGES_GIT_PROXY_PORT:-9892}"
DOCKER_BIN="${LEARNITIVE_DOCKER_BIN:-/usr/bin/docker}"
IPTABLES_BIN="${LEARNITIVE_IPTABLES_BIN:-/usr/sbin/iptables}"

fail() { printf '[Learnitive console egress] ERROR: %s\n' "$*" >&2; exit 1; }

subnet_is_recognized() {
  case "${1:-}" in
    "$DEFAULT_SUBNET"|"$ALTERNATE_SUBNET") return 0 ;;
    *) return 1 ;;
  esac
}

network_exists() { "$DOCKER_BIN" network inspect "$NETWORK" >/dev/null 2>&1; }
network_label() {
  "$DOCKER_BIN" network inspect --format '{{index .Labels "learnitive.deployment"}}' "$NETWORK" 2>/dev/null || true
}
committed_www_marker_accepts_legacy_owner() {
  local observed="${1:-}"
  [[ "$OWNER" == 'www.learnitive.com' ]] || return 1
  [[ "$observed" == 'edu.learnitive.com' || "$observed" == 'os.learnitive.com' ]] || return 1
  [[ -f "$MARKER_FILE" && ! -L "$MARKER_FILE" ]] || return 1
  [[ "$(stat -c %U:%G "$MARKER_FILE" 2>/dev/null || true)" == 'root:root' ]] || return 1
  grep -Fxq 'domain=www.learnitive.com' "$MARKER_FILE" || return 1
  grep -Fxq "root=$APP_ROOT" "$MARKER_FILE" || return 1
  grep -Fxq "network_owner_compat=$observed" "$MARKER_FILE"
}

network_owned() {
  local observed
  observed="$(network_label)"
  [[ "$observed" == "$OWNER" ]] && return 0
  if committed_www_marker_accepts_legacy_owner "$observed"; then return 0; fi
  [[ "$OWNER" == 'www.learnitive.com' && "$ALLOW_WWW_PROMOTION" == '1' ]] || return 1
  [[ "$observed" == 'edu.learnitive.com' || "$observed" == 'os.learnitive.com' ]]
}
network_subnet() {
  "$DOCKER_BIN" network inspect --format '{{(index .IPAM.Config 0).Subnet}}' "$NETWORK" 2>/dev/null || true
}
network_signature_values() {
  local driver scope subnet icc internal ingress
  driver="$("$DOCKER_BIN" network inspect --format '{{.Driver}}' "$NETWORK" 2>/dev/null || true)"
  scope="$("$DOCKER_BIN" network inspect --format '{{.Scope}}' "$NETWORK" 2>/dev/null || true)"
  subnet="$(network_subnet)"
  icc="$("$DOCKER_BIN" network inspect --format '{{index .Options "com.docker.network.bridge.enable_icc"}}' "$NETWORK" 2>/dev/null || true)"
  internal="$("$DOCKER_BIN" network inspect --format '{{.Internal}}' "$NETWORK" 2>/dev/null || true)"
  ingress="$("$DOCKER_BIN" network inspect --format '{{.Ingress}}' "$NETWORK" 2>/dev/null || true)"
  printf '%s|%s|%s|%s|%s|%s\n' "$driver" "$scope" "$subnet" "$icc" "$internal" "$ingress"
}
network_signature_matches() {
  local driver scope subnet icc internal ingress
  IFS='|' read -r driver scope subnet icc internal ingress < <(network_signature_values)
  subnet_is_recognized "$subnet" || return 1
  [[ "$driver" == 'bridge' && "$scope" == 'local' && "$icc" == 'false' && "$internal" == 'false' && "$ingress" == 'false' ]]
}
create_owned_network() {
  local subnet="${1:-$DEFAULT_SUBNET}"
  subnet_is_recognized "$subnet" || fail "Refusing to create $NETWORK with unrecognized subnet $subnet."
  "$DOCKER_BIN" network create \
    --driver bridge \
    --subnet "$subnet" \
    --opt com.docker.network.bridge.enable_icc=false \
    --label "learnitive.deployment=$OWNER" \
    "$NETWORK" >/dev/null
}
normalize_firewall_rule() {
  # iptables -S may add protocol match modules automatically and may quote
  # comment values. Normalize only those representation differences. Foreign
  # comments are deliberately left intact so they can never be adopted.
  sed -E \
    -e "s/[[:space:]]+-m comment --comment \"?$RULE_COMMENT\"?//g" \
    -e 's/[[:space:]]+-m (tcp|udp|icmp)//g' \
    -e 's/[[:space:]]+--reject-with icmp-port-unreachable//g' \
    -e 's/[[:space:]]+/ /g' \
    -e 's/[[:space:]]+$//'
}

gateway_for_subnet() {
  case "${1:-}" in
    "$DEFAULT_SUBNET") printf '172.30.240.1\n' ;;
    "$ALTERNATE_SUBNET") printf '172.30.241.1\n' ;;
    *) fail "Unable to derive Packages + Git proxy gateway for subnet ${1:-<empty>}." ;;
  esac
}

expected_packages_git_chain_rules() {
  local subnet gateway cidr
  subnet="$(network_subnet)"
  gateway="$(gateway_for_subnet "$subnet")"
  printf '%s\n' "-A $CHAIN -d $gateway/32 -p tcp --dport $PROXY_PORT -j ACCEPT"
  for cidr in 10.0.0.0/8 100.64.0.0/10 127.0.0.0/8 169.254.0.0/16 172.16.0.0/12 192.168.0.0/16 224.0.0.0/4 240.0.0.0/4; do
    printf '%s\n' "-A $CHAIN -d $cidr -j REJECT"
  done
  printf '%s\n' "-A $CHAIN -j REJECT"
}

actual_chain_rules() {
  "$IPTABLES_BIN" -S "$CHAIN" 2>/dev/null | while IFS= read -r line; do
    [[ "$line" == -A\ * ]] || continue
    printf '%s\n' "$line" | normalize_firewall_rule
  done
}

chain_semantics_match_current_profile() {
  [[ "$(actual_chain_rules)" == "$(expected_packages_git_chain_rules)" ]]
}

chain_all_rules_currently_owned() {
  local line saw=0
  while IFS= read -r line; do
    [[ "$line" == -A\ * ]] || continue
    saw=1
    [[ "$line" == *"--comment $RULE_COMMENT"* || "$line" == *"--comment \"$RULE_COMMENT\""* ]] || return 1
  done < <("$IPTABLES_BIN" -S "$CHAIN")
  [[ "$saw" == 1 ]]
}

verify_existing_chain() {
  "$IPTABLES_BIN" -S "$CHAIN" >/dev/null 2>&1 || return 1
  chain_all_rules_currently_owned || fail "Existing $CHAIN contains unowned rules; refusing to adopt or rewrite it."
  chain_semantics_match_current_profile || fail "Existing owned $CHAIN differs from the expected Learnitive Packages + Git profile; refusing to rewrite it."
  return 0
}


verify_existing_hooks() {
  "$IPTABLES_BIN" -S DOCKER-USER >/dev/null 2>&1 || fail 'Docker DOCKER-USER firewall chain is unavailable.'
  local line normalized
  while IFS= read -r line; do
    [[ "$line" == -A\ DOCKER-USER*"-j $CHAIN"* ]] || continue
    [[ "$line" == *"--comment $RULE_COMMENT"* || "$line" == *"--comment \"$RULE_COMMENT\""* ]] || \
      fail "DOCKER-USER contains an unowned jump to $CHAIN; refusing to adopt or rewrite it. Observed: $line"
    normalized="$(printf '%s\n' "$line" | normalize_firewall_rule)"
    case "$normalized" in
      "-A DOCKER-USER -s $DEFAULT_SUBNET -j $CHAIN"|"-A DOCKER-USER -s $ALTERNATE_SUBNET -j $CHAIN") ;;
      *) fail "DOCKER-USER contains an owned jump to $CHAIN with an unexpected source; refusing to rewrite it. Observed: $line" ;;
    esac
  done < <("$IPTABLES_BIN" -S DOCKER-USER)
}


remove_owned_hooks() {
  local candidate
  for candidate in "$DEFAULT_SUBNET" "$ALTERNATE_SUBNET"; do
    while "$IPTABLES_BIN" -C DOCKER-USER -s "$candidate" -m comment --comment "$RULE_COMMENT" -j "$CHAIN" 2>/dev/null; do
      "$IPTABLES_BIN" -D DOCKER-USER -s "$candidate" -m comment --comment "$RULE_COMMENT" -j "$CHAIN"
    done
  done
}


remove_owned_proxy_input_rules() {
  local candidate gateway
  for candidate in "$DEFAULT_SUBNET" "$ALTERNATE_SUBNET"; do
    gateway="$(gateway_for_subnet "$candidate")"
    while "$IPTABLES_BIN" -C INPUT -s "$candidate" -d "$gateway/32" -p tcp --dport "$PROXY_PORT" -m comment --comment "$RULE_COMMENT" -j ACCEPT 2>/dev/null; do
      "$IPTABLES_BIN" -D INPUT -s "$candidate" -d "$gateway/32" -p tcp --dport "$PROXY_PORT" -m comment --comment "$RULE_COMMENT" -j ACCEPT
    done
  done
}

ensure_proxy_input_rule() {
  local subnet="$1" gateway
  gateway="$(gateway_for_subnet "$subnet")"
  # Container -> bridge-gateway traffic terminates on the Docker host and goes
  # through INPUT, not DOCKER-USER. Hosts with UFW/default-drop INPUT otherwise
  # make the proxy appear healthy on 127.0.0.1 while every package container
  # hangs on the gateway connection. Allow only this Learnitive subnet, only its
  # own bridge gateway, and only the filtered proxy port.
  remove_owned_proxy_input_rules
  "$IPTABLES_BIN" -I INPUT 1 -s "$subnet" -d "$gateway/32" -p tcp --dport "$PROXY_PORT" -m comment --comment "$RULE_COMMENT" -j ACCEPT
}

check() {
  "$DOCKER_BIN" info >/dev/null 2>&1 || fail 'Docker is unavailable.'
  if ! network_exists; then
    printf '[Learnitive console egress] Dedicated Learnitive egress network is not present yet; it will be created during activation on %s.\n' "$DEFAULT_SUBNET"
    return 0
  fi
  if network_owned; then
    network_signature_matches || fail "Owned Docker network $NETWORK does not match an expected Learnitive egress signature ($(network_signature_description))."
  else
    fail "Docker network $NETWORK exists but is not owned by Learnitive deployment $OWNER; refusing to adopt, remove, or recreate it."
  fi
  if "$IPTABLES_BIN" -S "$CHAIN" >/dev/null 2>&1; then
    verify_existing_chain
  fi
  verify_existing_hooks
}

prepare() {
  "$DOCKER_BIN" info >/dev/null 2>&1 || fail 'Docker is unavailable.'
  if network_exists; then
    if network_owned; then
      network_signature_matches || fail "Owned Docker network $NETWORK does not match an expected Learnitive egress signature ($(network_signature_description))."
    else
      fail "Docker network $NETWORK already exists but is not owned by Learnitive deployment $OWNER; refusing to adopt, remove, or recreate it."
    fi
  else
    create_owned_network "$DEFAULT_SUBNET"
  fi

  local active_subnet
  active_subnet="$(network_subnet)"
  subnet_is_recognized "$active_subnet" || fail "Existing $NETWORK uses unexpected subnet $active_subnet."
  network_signature_matches || fail "Owned Docker network $NETWORK does not match an expected Learnitive egress signature after activation ($(network_signature_description))."

  if "$IPTABLES_BIN" -S "$CHAIN" >/dev/null 2>&1; then
    verify_existing_chain
    "$IPTABLES_BIN" -F "$CHAIN"
  else
    "$IPTABLES_BIN" -N "$CHAIN"
  fi

  local cidr proxy_gateway
  proxy_gateway="$(gateway_for_subnet "$active_subnet")"
  # Packages + Git containers can reach only the host-side filtered egress
  # proxy. Direct Internet, private-network, DNS, and ICMP traffic is rejected.
  "$IPTABLES_BIN" -A "$CHAIN" -d "$proxy_gateway/32" -p tcp --dport "$PROXY_PORT" -m comment --comment "$RULE_COMMENT" -j ACCEPT
  for cidr in 10.0.0.0/8 100.64.0.0/10 127.0.0.0/8 169.254.0.0/16 172.16.0.0/12 192.168.0.0/16 224.0.0.0/4 240.0.0.0/4; do
    "$IPTABLES_BIN" -A "$CHAIN" -d "$cidr" -m comment --comment "$RULE_COMMENT" -j REJECT
  done
  "$IPTABLES_BIN" -A "$CHAIN" -m comment --comment "$RULE_COMMENT" -j REJECT

  verify_existing_hooks
  remove_owned_hooks
  "$IPTABLES_BIN" -C DOCKER-USER -s "$active_subnet" -m comment --comment "$RULE_COMMENT" -j "$CHAIN" 2>/dev/null || \
    "$IPTABLES_BIN" -I DOCKER-USER 1 -s "$active_subnet" -m comment --comment "$RULE_COMMENT" -j "$CHAIN"
  ensure_proxy_input_rule "$active_subnet"
}

case "$ACTION" in
  check) check ;;
  prepare) prepare ;;
  *) fail 'Usage: console-egress-control.sh check|prepare' ;;
esac
