#!/usr/bin/env bash
# Unified local operator interface. Network upgrades retain daemon and config.
set -Eeuo pipefail
ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
TARGET=/usr/local/libexec/cf-pp-gateway
CONFIG=/etc/cf-pp-gateway
command_name=${1:-help}
shift || true
fail(){ echo "cfppctl: $*" >&2; exit 1; }
need_root(){ (( EUID == 0 )) || fail 'run as root'; [[ $(uname -s) == Linux ]] || fail 'Linux is required'; }
package_check(){ [[ -r $ROOT/SHA256SUMS ]] || fail 'run this command from a verified release package'; (cd "$ROOT" && sha256sum --check SHA256SUMS >/dev/null); }
usage(){ cat <<'HELP'
CFPP unified operator interface
  cfppctl plan [--interfaces IFACE,...]   read-only network discovery
  cfppctl check                         read-only existing-install preflight
  cfppctl install                       fresh install (verified package only)
  cfppctl upgrade                       preserve config/daemon; update network helpers
  cfppctl status [TCP_PORT]              read-only local diagnosis
  cfppctl sync                          reconcile Docker routes only
  cfppctl repair                        explicit rule reapply with backup/rollback
  cfppctl rollback BACKUP_DIR            restore a saved helper rollout
  cfppctl uninstall                     remove CFPP through installed uninstaller
Only supported Linux IPv4/TCP topologies are applied. Unsupported inputs fail.
HELP
}
rollout_lock(){
  need_root
  local lock=/run/lock/cfpp-rollout.lock
  [[ ! -L $lock ]] || fail 'unsafe rollout lock'
  if [[ $(readlink /proc/self/fd/8 2>/dev/null || true) != "$lock" ]]; then exec 8>"$lock"; fi
  flock -w 15 8 || fail 'another rollout is running'
}
root_config(){
  local file=$1 bits
  [[ -f $file && ! -L $file && $(stat -c %u "$file") == 0 ]] || fail "unsafe root configuration: $file"
  bits=$(stat -c %a "$file")
  (( (8#$bits & 8#022) == 0 )) || fail "configuration is writable by non-root: $file"
}
stop_units(){
  local unit load state
  for unit in "$@"; do
    load=$(systemctl show "$unit" --property=LoadState --value) || fail "cannot inspect $unit"
    [[ $load != not-found ]] || continue
    systemctl stop "$unit" || fail "cannot stop $unit"
    state=$(systemctl show "$unit" --property=ActiveState --value) || fail "cannot inspect stopped $unit"
    [[ $state == inactive || $state == failed ]] || fail "$unit is still $state"
  done
}
preflight(){
  local mode=${1:-check}
  need_root
  for tool in bash ip iptables iptables-save ipset awk grep sed sort sha256sum install cp mktemp chmod mv python3 flock sysctl systemctl stat; do command -v "$tool" >/dev/null || fail "missing $tool"; done
  python3 -c 'import sys;sys.exit(0 if sys.version_info >= (3,7) else 1)' || fail 'Python 3.7 or later is required'
  [[ -r $CONFIG/config.json && -r $CONFIG/network.env ]] || fail 'no existing CFPP configuration'
  root_config "$CONFIG/config.json"
  root_config "$CONFIG/network.env"
  systemctl is-active --quiet cf-pp-gateway.service || fail 'gateway is not active; inspect before upgrading'
  systemctl is-active --quiet cf-pp-gateway-network.service || fail 'gateway network unit is not active'
  /usr/local/sbin/cf-pp-gateway check --config "$CONFIG/config.json"
  (
    export CFPP_NETWORK_ENV_FILE="$CONFIG/network.env"
    source "$ROOT/network-common"
    [[ ${CFPP_ARMED:-0} == 1 ]] || fail 'network configuration is not armed'
    preflight_policy_table
    if [[ $mode != repair ]]; then
      rule_is_installed || fail 'existing policy rule is missing'
      route_is_installed || fail 'existing policy route is missing'
    fi
    desired=$(docker_bridge_routes)
    preflight_docker_routes "$desired"
    interface_has_address "$CFPP_INGRESS_INTERFACE" "$CFPP_INGRESS_ADDRESS" || fail 'ingress IP changed; explicit migration required'
    for pair in "mangle:$CFPP_PRE_CHAIN" "mangle:$CFPP_OUT_CHAIN" "nat:$CFPP_NAT_CHAIN" "filter:$CFPP_INPUT_CHAIN"; do
      table=${pair%%:*}; chain=${pair#*:}
      if ! existing=$(iptables -t "$table" -S "$chain" 2>/dev/null); then
        [[ $mode == repair ]] && continue
        fail "missing owned chain $chain; use explicit repair"
      fi
      foreign=$(foreign_chain_rules <<<"$existing")
      [[ -z $foreign ]] || fail "foreign rules in $chain"
    done
    echo 'Detected Docker bridge routes:'; printf '%s\n' "$desired"
  )
  echo 'PREFLIGHT PASSED: configuration and running gateway retained.'
}
transaction(){
  local mode=$1 name before_hash timer_enabled timer_active backup='' changed=0
  rollout_lock
  preflight "$mode"
  if [[ $mode == upgrade ]]; then package_check; fi
  timer_enabled=$(systemctl is-enabled cf-pp-gateway-reconcile.timer 2>/dev/null || true)
  timer_active=$(systemctl is-active cf-pp-gateway-reconcile.timer 2>/dev/null || true)
  rollback_failure(){
    local rc=$?
    trap - EXIT ERR HUP INT TERM
    if (( rc != 0 )); then
      if (( changed == 1 )); then
        echo 'Rollout failed; attempting rollback.' >&2
        bash "$backup/rollback.sh" || echo "ROLLBACK FAILED; inspect $backup" >&2
      elif [[ $timer_active == active ]]; then
        systemctl start cf-pp-gateway-reconcile.timer || echo 'Failed to restore previous timer state.' >&2
      fi
      exit "$rc"
    fi
  }
  trap rollback_failure EXIT
  trap 'exit 129' HUP
  trap 'exit 130' INT
  trap 'exit 143' TERM
  stop_units cf-pp-gateway-reconcile.timer cf-pp-gateway-reconcile.service
  mkdir -p /var/backups/cf-pp-gateway
  backup=$(mktemp -d /var/backups/cf-pp-gateway/unified.XXXXXX)
  chmod 0700 "$backup"
  mkdir "$backup/old" "$backup/new" "$backup/units"
  cp -a "$CONFIG" "$backup/config"
  cp -a "$TARGET/." "$backup/old/"
  for name in network-common network-apply network-check network-remove network-diagnose network-reconcile topology-plan.py cfppctl uninstall.sh; do
    [[ -f $ROOT/$name ]] && cp -a "$ROOT/$name" "$backup/new/$name"
  done
  for name in cf-pp-gateway-reconcile.service cf-pp-gateway-reconcile.timer; do
    [[ ! -e /etc/systemd/system/$name ]] || cp -a "/etc/systemd/system/$name" "$backup/units/$name"
  done
  if [[ -e /usr/local/sbin/cfppctl ]]; then cp -a /usr/local/sbin/cfppctl "$backup/launcher"; fi
  printf '%s\n' "$timer_enabled" >"$backup/timer.enabled"
  printf '%s\n' "$timer_active" >"$backup/timer.active"
  iptables-save >"$backup/iptables.before"
  ip -4 rule show >"$backup/rules.before"
  before_hash=$(sha256sum "$CONFIG/config.json" "$CONFIG/network.env")
  cat >"$backup/rollback.sh" <<'ROLLBACK'
#!/usr/bin/env bash
set -euo pipefail
(( EUID == 0 )) || exit 1
B=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
T=/usr/local/libexec/cf-pp-gateway
LOCK=/run/lock/cfpp-rollout.lock
[[ ! -L $LOCK ]] || exit 1
if [[ $(readlink /proc/self/fd/8 2>/dev/null || true) != "$LOCK" ]]; then exec 8>"$LOCK"; fi
flock -w 15 8 || exit 1
for unit in cf-pp-gateway-reconcile.timer cf-pp-gateway-reconcile.service; do
  load=$(systemctl show "$unit" --property=LoadState --value)
  [[ $load != not-found ]] || continue
  systemctl stop "$unit"
  state=$(systemctl show "$unit" --property=ActiveState --value)
  [[ $state == inactive || $state == failed ]] || exit 1
done
systemctl disable cf-pp-gateway-reconcile.timer >/dev/null 2>&1 || true
bash "$B/new/network-remove"
for n in network-common network-apply network-check network-remove network-diagnose network-reconcile topology-plan.py cfppctl uninstall.sh; do
  if [[ -e $B/old/$n ]]; then cp -a "$B/old/$n" "$T/$n"; else rm -f -- "$T/$n"; fi
done
for n in docker-bridge-routes.ipv4 reconcile.sha256; do
  if [[ -e $B/config/$n ]]; then cp -a "$B/config/$n" "/etc/cf-pp-gateway/$n"; else rm -f -- "/etc/cf-pp-gateway/$n"; fi
done
for n in cf-pp-gateway-reconcile.service cf-pp-gateway-reconcile.timer; do
  if [[ -e $B/units/$n ]]; then cp -a "$B/units/$n" "/etc/systemd/system/$n"; else rm -f -- "/etc/systemd/system/$n"; fi
done
if [[ -e $B/launcher ]]; then cp -a "$B/launcher" /usr/local/sbin/cfppctl; else rm -f -- /usr/local/sbin/cfppctl; fi
bash "$T/network-apply"
bash "$T/network-check"
systemctl daemon-reload
if [[ $(cat "$B/timer.enabled") == enabled ]]; then systemctl enable cf-pp-gateway-reconcile.timer; fi
if [[ $(cat "$B/timer.active") == active ]]; then systemctl start cf-pp-gateway-reconcile.timer; fi
echo 'ROLLBACK COMPLETE: old network helpers restored; customer configuration retained.'
ROLLBACK
  chmod 0700 "$backup/rollback.sh"
  echo "BACKUP=$backup"
  echo "ROLLBACK: bash $backup/rollback.sh"
  changed=1
  if [[ $mode == upgrade ]]; then
    staging=$(mktemp -d "$TARGET/.unified.XXXXXX")
    for name in network-common network-apply network-check network-remove network-diagnose network-reconcile topology-plan.py cfppctl uninstall.sh; do
      install -o root -g root -m 0755 "$ROOT/$name" "$staging/$name"
    done
    for name in "$staging/"*; do mv -f -- "$name" "$TARGET/"; done
    rmdir "$staging"
    for name in cf-pp-gateway-reconcile.service cf-pp-gateway-reconcile.timer; do
      install -o root -g root -m 0644 "$ROOT/$name" "/etc/systemd/system/$name"
    done
    printf '#!/usr/bin/env bash\nexec /usr/local/libexec/cf-pp-gateway/cfppctl "$@"\n' >/usr/local/sbin/cfppctl
    chmod 0755 /usr/local/sbin/cfppctl
  fi
  bash "$TARGET/network-apply"
  bash "$TARGET/network-check"
  [[ $(sha256sum "$CONFIG/config.json" "$CONFIG/network.env") == "$before_hash" ]] || fail 'configuration unexpectedly changed'
  systemctl is-active --quiet cf-pp-gateway.service
  bash "$TARGET/network-reconcile" --stamp
  systemctl daemon-reload
  systemctl enable --now cf-pp-gateway-reconcile.timer
  trap - EXIT ERR HUP INT TERM
  echo 'NETWORK ROLLOUT COMPLETE: running gateway/config preserved, Docker route reconciliation enabled.'
  echo 'Rule checks do not certify customer login. Retain the printed rollback command.'
}
case "$command_name" in
  help|-h|--help) usage ;;
  plan) python3 "$ROOT/topology-plan.py" "$@" ;;
  check) [[ $# == 0 ]] || fail 'check takes no arguments'; preflight ;;
  install)
    [[ $# == 0 ]] || fail 'install takes no arguments'
    need_root; package_check
    if [[ -f $ROOT/host-install.sh ]]; then bash "$ROOT/host-install.sh"; elif [[ -f $ROOT/install.sh ]]; then bash "$ROOT/install.sh"; else fail 'use the release installer for fresh installation'; fi
    ;;
  upgrade|repair)
    [[ $# == 0 ]] || fail "$command_name takes no arguments"
    transaction "$command_name"
    ;;
  status) bash "$TARGET/network-diagnose" "${1:-19086}" ;;
  sync) rollout_lock; bash "$TARGET/network-reconcile" --apply ;;
  rollback)
    need_root; [[ $# == 1 ]] || fail 'rollback requires backup directory'
    [[ $1 == /var/backups/cf-pp-gateway/unified.* && -f $1/rollback.sh ]] || fail 'not a saved unified backup'
    bash "$1/rollback.sh"
    ;;
  uninstall)
    need_root
    [[ -x /usr/local/libexec/cf-pp-gateway/uninstall.sh ]] || fail 'use the release uninstall.sh'
    bash /usr/local/libexec/cf-pp-gateway/uninstall.sh
    ;;
  *) usage; exit 2 ;;
esac
