#!/usr/bin/env bash

set -e

# String formatting subsystem

formatting_init() {
  endl=$'\n'
}

enquote() {
    while (( $# > 1 )); do
      printf "%q " "${1}"; shift
    done
    if (( $# == 1 )); then
      printf "%q" "${1}"; shift
    fi
}

multiline() {
  # shellcheck disable=SC1004
  echo "${1} " | awk '
    function pm(a, b, l) {
      return length(a) > l \
          && length(b) > l \
          && substr(a, 1, l+1) == substr(b, 1, l+1) \
            ? pm(a, b, l+1) : l;
    }

    !started && $0 !~ /^[ \t]*$/ {
      started=1
      match($0, /^[ \t]*/)
      prefix=substr($0, 1, RLENGTH)
    }

    started {
      print(substr($0, 1 + pm($0, prefix)));
    }
  '
}

dbg() {
  echo >&2 "$@"
}


detect_git_dir() {
  # https://stackoverflow.com/questions/3618078/pipe-only-stderr-through-a-filter
  (
    git -C "${scriptdir}" rev-parse --show-toplevel 3>&1 1>&2 2>&3 3>&- \
      | sed '
          /not a git repository/d;
          s/^/WARNING: /'
  ) 3>&1 1>&2 2>&3 3>&-
}

# Cleanup subsystem (sigterm)

cleanup_init() {
  cleanup_actions=()
  trap cleanup_apply exit
}

cleanup_apply() {
  local f
  for f in "${cleanup_actions[@]}"; do
    eval "${f}"
  done
}

cleanup() {
  cleanup_actions+=("$(multiline "${1}")")
}

# Transactional execution subsystem

frag_init() {
  explain=0
  frag_transaction=()
  frag "
    #! /bin/bash
    set -e"
}

frag_apply() {
  local f
  for f in "${frag_transaction[@]}"; do
    if (( explain == 1 )); then
      dbg "${f}"
    fi
    eval "${f}"
  done
}

frag() {
  frag_transaction+=("$(multiline "${1}")")
}

frag_append() {
  local len; len="${#frag_transaction[@]}"
  frag_transaction=("${frag_transaction[@]:0:len-1}" "${frag_transaction[len-1]}${1}")
}

frag_append_esc() {
  frag_append " \\${endl}${1}"
}

# Usage documentation subsystem
usage_init() {
  usagestack=("${script}")
}

usage_snap() {
  echo "${#usagestack}"
}

usage_restore() {
  local n; n="${1}"
  dbg REST "${1}"
  usagestack=("${usagestack[@]:0:n-2}")
}


usage() {
  dbg "Usage: ${usagestack[*]}"
}

fatal() {
  dbg "FATAL: $*"
  usage
  exit 1
}

genkey() {
  usagestack+=("PRIVATE_KEYS_DIR")
  local skdir
  skdir="${1%/}"; shift || fatal "Required positional argument: PRIVATE_KEYS_DIR"

  while (( $# > 0 )); do
    local arg; arg="$1"; shift
    case "${arg}" in
      -h | -help | --help | help) usage; return 0 ;;
      *) fatal "Unknown option ${arg}";;
    esac
  done

  if test -e "${skdir}"; then
    fatal "PRIVATE_KEYS_DIR \"${skdir}\" already exists"
  fi

  frag "
    umask 077
    mkdir -p $(enquote "${skdir}")
    wg genkey > $(enquote "${skdir}"/wgsk)
    $(enquote "${binary}") gen-keys \\
      -s $(enquote "${skdir}"/pqsk) \\
      -p  $(enquote "${skdir}"/pqpk)"
}

pubkey() {
  usagestack+=("PRIVATE_KEYS_DIR" "PUBLIC_KEYS_DIR")
  local skdir pkdir
  skdir="${1%/}"; shift || fatal "Required positional argument: PRIVATE_KEYS_DIR"
  pkdir="${1%/}"; shift || fatal "Required positional argument: PUBLIC_KEYS_DIR"

  while (( $# > 0 )); do
    local arg; arg="$1"; shift
    case "${arg}" in
      -h | -help | --help | help) usage; exit 0;;
      *) fatal "Unknown option ${arg}";;
    esac
  done

  if test -e "${pkdir}"; then
    fatal "PUBLIC_KEYS_DIR \"${pkdir}\" already exists"
  fi

  frag "
    mkdir -p $(enquote "${pkdir}")
    wg pubkey < $(enquote "${skdir}"/wgsk) > $(enquote "${pkdir}/wgpk")
    cp $(enquote "${skdir}"/pqpk) $(enquote "${pkdir}/pqpk")"
}

exchange() {
  usagestack+=("PRIVATE_KEYS_DIR" "[dev <device>]" "[listen <ip>:<port>]" "[peer PUBLIC_KEYS_DIR [endpoint <ip>:<port>] [persistent-keepalive <interval>] [allowed-ips <ip1>/<cidr1>[,<ip2>/<cidr2>]...]]...")
  local skdir dev lport
  dev="${project_name}0"
  skdir="${1%/}"; shift || fatal "Required positional argument: PRIVATE_KEYS_DIR"

  while (( $# > 0 )); do
    local arg; arg="$1"; shift
    case "${arg}" in
      dev) dev="${1}"; shift || fatal "dev option requires parameter";;
      peer) set -- "peer" "$@"; break;; # Parsed down below
      listen)
        local listen; listen="${1}";
        lip="${listen%:*}";
        lport="${listen/*:/}";
        if [[ "$lip" = "$lport" ]]; then
          lip="[::]"
        fi
        shift;;
      -h | -help | --help | help) usage; return 0;;
      *) fatal "Unknown option ${arg}";;
    esac
  done

  if (( $# == 0 )); then
    fatal "Needs at least one peer specified"
  fi

  # os dependent setup
  case "$OSTYPE" in
    linux-*) # could be linux-gnu or linux-musl
      frag "
        # Create the WireGuard interface
        ip link add dev $(enquote "${dev}") type wireguard || true"

      cleanup "
        ip link del dev $(enquote "${dev}") || true"

      frag "
        ip link set dev $(enquote "${dev}") up"
      ;;

    freebsd*)
      frag "
        # load the WireGuard kernel module
        kldload -n if_wg || fatal 'Cannot load if_wg kernel module'"

      frag "
        # Create the WireGuard interface
        ifconfig wg create name $(enquote "${dev}") || true"

      cleanup "
        ifconfig $(enquote "${dev}") destroy || true"

      frag "
        ifconfig $(enquote "${dev}") up"
      ;;

    *)
      fatal "Your system $OSTYPE is not yet supported. We are happy to receive patches to address this :)"
      ;;

  esac

  frag "
    # Deploy the classic wireguard private key
    wg set $(enquote "${dev}") private-key $(enquote "${skdir}/wgsk")"


  if test -n "${lport}"; then
    frag_append "listen-port $(enquote "$(( lport + 1 ))")"
  fi

  frag "
    # Launch the post quantum wireguard exchange daemon
    $(enquote "${binary}") exchange"

  if (( verbose == 1 )); then
    frag_append "verbose"
  fi

  frag_append_esc "    secret-key $(enquote "${skdir}/pqsk")"
  frag_append_esc "    public-key  $(enquote "${skdir}/pqpk")"

  if test -n "${lport}"; then
    frag_append_esc "    listen $(enquote "${lip}:${lport}")"
  fi

  usagestack+=("peer" "PUBLIC_KEYS_DIR endpoint IP:PORT")

  while (( $# > 0 )); do
    shift; # Skip "peer" argument

    local peerdir ip port keepalive allowedips
    peerdir="${1%/}"; shift || fatal "Required peer argument: PUBLIC_KEYS_DIR"

    while (( $# > 0 )); do
      local arg; arg="$1"; shift
      case "${arg}" in
        peer) set -- "peer" "$@"; break;; # Next peer
        endpoint) ip="${1%:*}"; port="${1##*:}"; shift;;
        persistent-keepalive) keepalive="${1}"; shift;;
        allowed-ips) allowedips="${1}"; shift;;
        -h | -help | --help | help) usage; return 0;;
        *) fatal "Unknown option ${arg}";;
      esac
    done

    # Public key
    frag_append_esc "    peer public-key $(enquote "${peerdir}/pqpk")"

    # PSK
    local pskfile; pskfile="${peerdir}/psk"
    if test -f "${pskfile}"; then
      frag_append_esc "      preshared-key $(enquote "${pskfile}")"
    fi


    if test -n "${ip}"; then
      frag_append_esc "      endpoint $(enquote "${ip}:${port}")"
    fi

    frag_append_esc "      wireguard $(enquote "${dev}") $(enquote "$(cat "${peerdir}/wgpk")")"

    if test -n "${ip}"; then
      frag_append_esc "        endpoint $(enquote "${ip}:$(( port + 1 ))")"
    fi

    if test -n "${keepalive}"; then
      frag_append_esc "        persistent-keepalive $(enquote "${keepalive}")"
    fi

    if test -n "${allowedips}"; then
      frag_append_esc "        allowed-ips $(enquote "${allowedips}")"
    fi
  done
}

find_rosenpass_binary() {
  local binary; binary=""
  if [[ -n "${gitdir}" ]]; then
    # If rp is run from the git repo, use the newest build artifact
    binary=$(
      find "${gitdir}/result/bin/${project_name}" \
           "${gitdir}"/target/{release,debug}/"${project_name}" \
           -printf "%T@ %p\n" 2>/dev/null \
        | sort -nr \
        | awk 'NR==1 { print($2) }'
    )
  elif [[ -n "${nixdir}" ]]; then
    # If rp is run from nix, use the nix-installed rosenpass version
    binary="${nixdir}/bin/${project_name}"
  fi

  if [[ -z "${binary}" ]]; then
    binary="${project_name}"
  fi

  echo "${binary}"
}

main() {
  formatting_init
  cleanup_init
  usage_init
  frag_init

  project_name="rosenpass"
  verbose=0
  scriptdir="$(dirname "${script}")"
  gitdir="$(detect_git_dir)" || true
  if [[ -d /nix ]]; then
    nixdir="$(readlink -f result/bin/rp | grep -Pio '^/nix/store/[^/]+(?=/bin/[^/]+)')" || true
  fi
  binary="$(find_rosenpass_binary)"

  # Parse command

  usagestack+=("[explain]" "[verbose]" "genkey|pubkey|exchange" "[ARGS]...")

  local cmd
  while (( $# > 0 )); do
    local arg; arg="$1"; shift
    case "${arg}" in
      genkey|pubkey|exchange) cmd="${arg}"; break;;
      explain) explain=1;;
      verbose) verbose=1;;
      -h | -help | --help | help) usage; return 0 ;;
      *) fatal "Unknown command ${arg}";;
    esac
  done

  test -n "${cmd}" || fatal "No command supplied"
  usagestack=("${script}")

  # Execute command

  usagestack+=("${cmd}")
  "${cmd}" "$@"
  usagestack=("${script}")

  # Apply transaction

  frag_apply
}

script="$0"
main "$@"
