#!/usr/bin/env bash

set -euo pipefail

usage() {
  cat >&2 <<'EOF'
usage:
  multihome-route plan CONFIG
  multihome-route apply CONFIG STATE_DIR
  multihome-route verify CONFIG STATE_DIR EVIDENCE
  multihome-route rollback STATE_DIR
EOF
  exit 64
}

die() {
  printf 'multihome-route: %s\n' "$*" >&2
  exit 1
}

read_rows() {
  local config=$1
  [[ -r "$config" ]] || die "cannot read config: $config"
  awk '
    /^[[:space:]]*(#|$)/ { next }
    NF != 8 {
      printf "multihome-route: %s:%d: expected 8 fields, got %d\n", FILENAME, NR, NF > "/dev/stderr"
      failed = 1
      next
    }
    { print }
    END { if (failed) exit 1 }
  ' "$config"
}

validate_config() {
  local config=$1
  local role iface address prefix gateway table metric priority extra
  local count=0
  declare -A seen_role=() seen_table=() seen_priority=() seen_address=()

  while read -r role iface address prefix gateway table metric priority extra; do
    ((count += 1))
    [[ -z "${extra:-}" ]] || die "invalid row in config"
    [[ "$role" == management || "$role" == application ]] || die "unsupported role: $role"
    [[ "$iface" =~ ^[a-zA-Z0-9_.:-]+$ ]] || die "invalid interface: $iface"
    [[ "$address" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] || die "invalid address: $address"
    [[ "$gateway" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] || die "invalid gateway: $gateway"
    [[ "$prefix" =~ ^[0-9]+$ ]] && ((prefix >= 0 && prefix <= 32)) || die "invalid prefix: $prefix"
    [[ "$metric" =~ ^[0-9]+$ ]] || die "invalid metric: $metric"
    [[ "$priority" =~ ^[0-9]+$ ]] || die "invalid priority: $priority"
    [[ "$table" =~ ^[a-zA-Z][a-zA-Z0-9_-]*$ ]] || die "invalid table: $table"
    [[ -z "${seen_role[$role]:-}" ]] || die "duplicate role: $role"
    [[ -z "${seen_table[$table]:-}" ]] || die "duplicate table: $table"
    [[ -z "${seen_priority[$priority]:-}" ]] || die "duplicate priority: $priority"
    [[ -z "${seen_address[$address]:-}" ]] || die "duplicate address: $address"
    seen_role[$role]=1
    seen_table[$table]=1
    seen_priority[$priority]=1
    seen_address[$address]=1
  done < <(read_rows "$config")

  ((count == 2)) || die "config must contain exactly two interfaces"
  [[ -n "${seen_role[management]:-}" ]] || die "missing management interface"
  [[ -n "${seen_role[application]:-}" ]] || die "missing application interface"
}

build_plan() {
  local config=$1
  local role iface address prefix gateway table metric priority

  validate_config "$config"
  while read -r role iface address prefix gateway table metric priority; do
    printf 'route replace table main default via %s dev %s metric %s\n' \
      "$gateway" "$iface" "$metric"
    printf 'route replace table %s default via %s dev %s src %s metric %s\n' \
      "$table" "$gateway" "$iface" "$address" "$metric"
    printf 'rule replace priority %s from %s/32 lookup %s\n' \
      "$priority" "$gateway" "$table"
  done < <(read_rows "$config")
}

apply_plan() {
  local config=$1 state=$2 candidate
  [[ ! -e "$state/transaction.open" ]] || die "a rollback is already pending in $state"
  mkdir -p "$state"
  candidate="$state/.candidate.$$"
  trap 'rm -f -- "$candidate"' RETURN
  build_plan "$config" > "$candidate"

  if [[ -e "$state/active.plan" ]]; then
    cp "$state/active.plan" "$state/rollback.plan"
    : > "$state/rollback.had-active"
  else
    : > "$state/rollback.plan"
    rm -f -- "$state/rollback.had-active"
  fi
  mv "$candidate" "$state/active.plan"
  : > "$state/transaction.open"
  trap - RETURN
}

rollback_plan() {
  local state=$1
  [[ -e "$state/transaction.open" ]] || die "no rollback is pending in $state"
  if [[ -e "$state/rollback.had-active" ]]; then
    mv "$state/rollback.plan" "$state/active.plan"
  else
    rm -f -- "$state/active.plan" "$state/rollback.plan"
  fi
  rm -f -- "$state/rollback.had-active" "$state/transaction.open"
}

lookup_egress() {
  local plan=$1 source=$2
  local kind action word priority from tagged_source lookup table
  local route_table default via gateway dev iface src tagged_src metric value
  local selected_table=main selected_priority=2147483647
  local selected_iface='' selected_gateway='' selected_metric=2147483647

  while read -r kind action word priority from tagged_source lookup table; do
    [[ "$kind" == rule && "$action" == replace && "$word" == priority && \
       "$from" == from && "$lookup" == lookup ]] || continue
    if [[ "$tagged_source" == "$source/32" ]] && ((priority < selected_priority)); then
      selected_priority=$priority
      selected_table=$table
    fi
  done < "$plan"

  while read -r kind action word route_table default via gateway dev iface src tagged_src metric value; do
    [[ "$kind" == route && "$action" == replace && "$word" == table && \
       "$route_table" == "$selected_table" && "$default" == default && \
       "$via" == via && "$dev" == dev ]] || continue
    if [[ "$selected_table" == main ]]; then
      [[ "$src" == metric ]] || continue
      value=$tagged_src
    else
      [[ "$src" == src && "$metric" == metric ]] || continue
    fi
    if ((value < selected_metric)); then
      selected_metric=$value
      selected_iface=$iface
      selected_gateway=$gateway
    fi
  done < "$plan"

  [[ -n "$selected_iface" ]] || die "no default route in table $selected_table"
  printf '%s %s %s\n' "$selected_iface" "$selected_gateway" "$selected_table"
}

verify_paths() {
  local config=$1 state=$2 evidence=$3
  local flow role remote local_address ingress extra
  local cfg_role cfg_iface cfg_address prefix cfg_gateway table metric priority
  local expected_iface expected_gateway found egress gateway selected_table
  local checked=0 failures=0
  declare -A seen_flow=()

  validate_config "$config"
  [[ -r "$state/active.plan" ]] || die "no active plan in $state"
  [[ -r "$evidence" ]] || die "cannot read evidence: $evidence"

  while read -r flow role remote local_address ingress extra; do
    [[ -z "$flow" || "$flow" == \#* ]] && continue
    [[ -z "${extra:-}" ]] || die "$evidence: malformed evidence row"
    [[ "$role" == management || "$role" == application ]] || die "$evidence: unsupported role: $role"
    [[ -z "${seen_flow[$role]:-}" ]] || die "$evidence: duplicate evidence for $role"
    seen_flow[$role]=1
    expected_iface=''
    expected_gateway=''
    found=0
    while read -r cfg_role cfg_iface cfg_address prefix cfg_gateway table metric priority; do
      if [[ "$cfg_role" == "$role" && "$cfg_address" == "$local_address" ]]; then
        expected_iface=$cfg_iface
        expected_gateway=$cfg_gateway
        found=1
        break
      fi
    done < <(read_rows "$config")
    ((found == 1)) || die "$evidence: $role local address is not configured: $local_address"
    [[ "$ingress" == "$expected_iface" ]] || die "$evidence: $role ingress $ingress does not own $local_address"

    read -r egress gateway selected_table < <(lookup_egress "$state/active.plan" "$local_address")
    if [[ "$egress" == "$ingress" && "$gateway" == "$expected_gateway" ]]; then
      printf 'OK %s reply from %s to %s returns via %s (%s)\n' \
        "$role" "$local_address" "$remote" "$egress" "$selected_table"
    else
      printf 'FAIL %s reply from %s to %s entered %s but returns via %s through %s (%s)\n' \
        "$role" "$local_address" "$remote" "$ingress" "$egress" "$gateway" "$selected_table"
      ((failures += 1))
    fi
    ((checked += 1))
  done < "$evidence"

  ((checked == 2)) || die "$evidence: expected management and application evidence"
  [[ -n "${seen_flow[management]:-}" && -n "${seen_flow[application]:-}" ]] || \
    die "$evidence: expected management and application evidence"
  ((failures == 0))
}

(( $# > 0 )) || usage
command=$1
shift
case "$command" in
  plan)
    (( $# == 1 )) || usage
    build_plan "$1"
    ;;
  apply)
    (( $# == 2 )) || usage
    apply_plan "$1" "$2"
    ;;
  rollback)
    (( $# == 1 )) || usage
    rollback_plan "$1"
    ;;
  verify)
    (( $# == 3 )) || usage
    verify_paths "$1" "$2" "$3"
    ;;
  *) usage ;;
esac
