#!/usr/bin/env bash
# Root-only Docker topology reconciliation. It never rewrites daemon config or
# applies firewall hooks automatically: missing/conflicting hooks require the
# normal installer transaction, because a full firewall replay is not atomic.
set -euo pipefail
readonly SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
mode=${1:---check-only}
[[ $# -le 1 && ( $mode == --apply || $mode == --check-only || $mode == --stamp ) ]] || {
  echo "Usage: network-reconcile [--check-only|--apply|--stamp]" >&2; exit 2;
}
[[ $(id -u) == 0 ]] || { echo "cf-pp-gateway reconcile: root required" >&2; exit 1; }
for tool in flock stat systemctl ip bash env sort sed mktemp chmod mv rm sha256sum cat awk; do
  command -v "$tool" >/dev/null || { echo "cf-pp-gateway reconcile: missing $tool" >&2; exit 1; }
done

# Validate files before sourcing shell configuration. The daemon service account
# may read these files but must never be able to change root's executable input.
root_input() {
  local path=$1 owner mode_bits
  [[ -f $path && ! -L $path ]] || { echo "cf-pp-gateway reconcile: unsafe/missing file $path" >&2; exit 1; }
  owner=$(stat -c %u -- "$path")
  mode_bits=$(stat -c %a -- "$path")
  [[ $owner == 0 && $mode_bits =~ ^[0-7]{3,4}$ ]] || { echo "cf-pp-gateway reconcile: root ownership required for $path" >&2; exit 1; }
  (( (8#$mode_bits & 8#022) == 0 )) || { echo "cf-pp-gateway reconcile: writable root input $path" >&2; exit 1; }
}
root_input "$SCRIPT_DIR/network-common"
root_input "$SCRIPT_DIR/network-check"
root_input "${CFPP_NETWORK_ENV_FILE:-/etc/cf-pp-gateway/network.env}"

# An inactive gateway is intentional (including during uninstall). Never bring
# it back or arm it from a timer. Exit silently to avoid unchanged-state logs.
if ! systemctl is-active --quiet cf-pp-gateway.service; then
  [[ $mode != --stamp ]] || { echo "cf-pp-gateway reconcile: cannot stamp an inactive gateway" >&2; exit 1; }
  [[ $mode == --apply ]] || echo "SKIP: gateway is inactive; no changes"
  exit 0
fi
# shellcheck source=network-common
source "$SCRIPT_DIR/network-common"
readonly LOCK_PATH="$CFPP_NETWORK_LOCK_FILE"
# flock is also used by the installer and manual network operations.
[[ ! -L $LOCK_PATH ]] || { echo "cf-pp-gateway reconcile: unsafe lock" >&2; exit 1; }
exec 9>"$LOCK_PATH"
flock -n 9 || exit 0
[[ ${CFPP_ARMED:-0} == 1 ]] || fail "reconciliation is not armed"
root_input "$CFPP_DOCKER_ROUTES_FILE"
root_input "$CFPP_TRUSTED_CIDRS_FILE"
root_input "$CFPP_DESTINATION_ADDRESSES_FILE"
readonly GATEWAY_CONFIG=${CFPP_GATEWAY_CONFIG_FILE:-/etc/cf-pp-gateway/config.json}
readonly MANIFEST=${CFPP_RECONCILE_MANIFEST_FILE:-/etc/cf-pp-gateway/reconcile.sha256}
root_input "$GATEWAY_CONFIG"
configuration_digest() {
  sha256sum "$CFPP_NETWORK_ENV" "$GATEWAY_CONFIG" "$CFPP_TRUSTED_CIDRS_FILE" "$CFPP_DESTINATION_ADDRESSES_FILE"
}
if [[ $mode != --stamp ]]; then
  root_input "$MANIFEST"
  [[ $(configuration_digest) == "$(cat "$MANIFEST")" ]] || fail "configuration changed since explicit rollout; inspect, validate and --stamp it before reconciliation"
fi

# Docker failures abort here without touching routes. Never treat a temporarily
# unavailable daemon as an empty inventory and remove otherwise working routes.
desired=$(docker_bridge_routes)
recorded=$(sed '/^[[:space:]]*$/d' "$CFPP_DOCKER_ROUTES_FILE" | sort -u)
actual=$(ip -o -4 route show table "$CFPP_ROUTE_TABLE" proto "$CFPP_DOCKER_ROUTE_PROTO" | awk '{for(i=1;i<=NF;i++) if($i=="dev") print $1,$(i+1)}' | sort -u)
preflight_docker_routes "$desired"
"$SCRIPT_DIR/network-check" --skip-docker-routes >/dev/null
if [[ $mode == --stamp ]]; then
  [[ $desired == "$recorded" && $desired == "$actual" ]] || fail "cannot stamp differing Docker topology; explicitly repair network first"
  "$SCRIPT_DIR/network-check" >/dev/null
  if [[ -e $MANIFEST || -L $MANIFEST ]]; then root_input "$MANIFEST"; fi
  manifest_temp=$(mktemp "${MANIFEST}.XXXXXX")
  if ! configuration_digest >"$manifest_temp" || ! chmod 0600 "$manifest_temp" || ! mv -f -- "$manifest_temp" "$MANIFEST"; then
    rm -f -- "$manifest_temp"
    fail "cannot save reconciliation fingerprint"
  fi
  echo "cf-pp-gateway reconcile: validated current configuration fingerprint saved"
  exit 0
fi
if [[ $desired == "$recorded" && $desired == "$actual" ]]; then
  [[ $mode == --apply ]] || echo "RECONCILE CHECK PASSED: topology and network rules unchanged"
  exit 0
fi
if [[ $mode == --check-only ]]; then
  echo "RECONCILE REQUIRED: Docker topology differs; --apply updates only owned routes"
  exit 3
fi

# Snapshot only our route inventory. Never take a global iptables snapshot or
# restore other applications' rules. Children run with errexit independently;
# testing a shell function in an if statement would disable its errexit.
inventory_mode=$(stat -c %a -- "$CFPP_DOCKER_ROUTES_FILE")
inventory_backup=$(mktemp "${CFPP_DOCKER_ROUTES_FILE}.reconcile.XXXXXX")
cat "$CFPP_DOCKER_ROUTES_FILE" >"$inventory_backup"
chmod 0600 "$inventory_backup"
network_digest=$(configuration_digest)
transaction_started=0
transaction_finished=0
rollback_owned_routes() {
  local status=$? rollback_ok=1
  trap - EXIT HUP INT TERM
  if (( transaction_started == 1 && transaction_finished == 0 )); then
    echo "cf-pp-gateway reconcile: failed; restoring owned Docker routes" >&2
    if ! env CFPP_NETWORK_ENV_FILE="$CFPP_NETWORK_ENV" CFPP_DOCKER_ROUTES_FILE="$CFPP_DOCKER_ROUTES_FILE" \
      bash -c 'set -euo pipefail; source "$1"; sync_docker_routes "$2"' _ "$SCRIPT_DIR/network-common" "$actual"; then
      rollback_ok=0
      echo "cf-pp-gateway reconcile: owned-route rollback failed; operator inspection required" >&2
    fi
    if ! chmod "$inventory_mode" "$inventory_backup" || ! mv -f -- "$inventory_backup" "$CFPP_DOCKER_ROUTES_FILE"; then
      rollback_ok=0
      echo "cf-pp-gateway reconcile: inventory rollback failed" >&2
    fi
    (( rollback_ok == 1 )) || status=1
  fi
  rm -f -- "$inventory_backup"
  exit "$status"
}
trap rollback_owned_routes EXIT
trap 'exit 129' HUP
trap 'exit 130' INT
trap 'exit 143' TERM
[[ $(configuration_digest) == "$network_digest" ]] || fail "network configuration changed during reconciliation"
systemctl is-active --quiet cf-pp-gateway.service || fail "gateway stopped during reconciliation"
transaction_started=1
env CFPP_NETWORK_ENV_FILE="$CFPP_NETWORK_ENV" CFPP_DOCKER_ROUTES_FILE="$CFPP_DOCKER_ROUTES_FILE" \
  bash -c 'set -euo pipefail; source "$1/network-common"; sync_docker_routes "$2"; save_docker_routes "$2"; "$1/network-check" >/dev/null' _ "$SCRIPT_DIR" "$desired"
[[ $(configuration_digest) == "$network_digest" ]] || fail "network configuration changed during reconciliation"
transaction_finished=1
echo "cf-pp-gateway reconcile: Docker routes updated; gateway and business services unchanged"
