#!/bin/bash

norm=$(tput sgr0) || true
red=$(tput setaf 1) || true
green=$(tput setaf 2) || true
yellow=$(tput setaf 3) || true
bold=$(tput bold) || true

timestamp() {
    date -u "+[%Y-%m-%dT%H:%M:%SZ]"
}

log-info() {
    echo "${bold}$(timestamp) $*${norm}"
}

log-warn() {
    echo "${yellow}$(timestamp) $*${norm}"
}

log-success() {
    echo "${green}$(timestamp) $*${norm}"
}

log-debug() {
    echo "${norm}$(timestamp) $*"
}

fail-now() {
    echo "${red}$(timestamp) $*${norm}"
    exit 1
}

docker-up() {
    if [ $# -eq 0 ]; then
        log-debug "bringing up services..."
    else
        log-debug "bringing up $*..."
    fi
    docker compose up -d "$@" || fail-now "failed to bring up services."
}

docker-wait-for-healthy() {
    if [ $# -ne 3 ]; then
        fail-now "docker-wait-for-healthy: <container> <maxchecks> <interval>"
    fi

    local ctr_name=$1
    local maxchecks=$2
    local interval=$3
    for ((i=1;i<=maxchecks;i++)); do
        set +e
        health_status=$(docker inspect --format '{{.State.Health.Status}}' "${ctr_name}" 2>/dev/null)
        if [ "${health_status}" == "healthy" ]; then
            return
        else
            log-debug "waiting for container ${ctr_name} to launch"
        fi
        set -e

        sleep "${interval}"
    done

    fail-now "timed out waiting for ${ctr_name} to start"
}

docker-stop() {
    if [ $# -eq 0 ]; then
        log-debug "stopping services..."
    else
        log-debug "stopping $*..."
    fi
    docker compose stop "$@"
}

docker-down() {
    log-debug "bringing down services..."
    docker compose down
}

docker-cleanup() {
    log-debug "cleaning up services..."
    docker compose down -v --remove-orphans
}

docker-spire-server-up() {
    docker-up "$@"

    for container in "$@"; do
        check-server-started ${container}
    done
}

fingerprint() {
	# calculate the SHA1 digest of the DER bytes of the certificate using the
	# "coreutils" output format (`-r`) to provide uniform output from
	# `openssl sha1` on macOS and linux.
	openssl x509 -in "$1" -outform DER | openssl sha1 -r | awk '{print $1}'
}

check-server-started() {
  # Check at most 20 times (with one second in between) that the server has
  # successfully started.
  MAXCHECKS=20
  CHECKINTERVAL=1
  for ((i=1;i<=MAXCHECKS;i++)); do
      log-info "checking for starting server APIs ($i of $MAXCHECKS max)..."
      docker compose logs "$1"
      if docker compose logs "$1" | grep "Starting Server APIs"; then
          return 0
      fi
      sleep "${CHECKINTERVAL}"
  done

  fail-now "timed out waiting for server to start"
}

check-log-line() {
  # Check at most 30 times (with one second in between) that the agent has
  # successfully synced down the workload entry.
  MAXCHECKS=30
  CHECKINTERVAL=1
  for ((i=1;i<=MAXCHECKS;i++)); do
      log-info "checking for log line ($i of $MAXCHECKS max)..."
      if docker compose logs "$1" | grep -E "$2"; then
          return 0
      fi
      sleep "${CHECKINTERVAL}"
  done

  docker compose logs "$1"
  fail-now "timed out waiting for "$2" in log in container $1"
}

check-synced-entry() {
  # Check at most 30 times (with one second in between) that the agent has
  # successfully synced down the workload entry.
  MAXCHECKS=30
  CHECKINTERVAL=1
  for ((i=1;i<=MAXCHECKS;i++)); do
      log-info "checking for synced entry ($i of $MAXCHECKS max)..."
      docker compose logs "$1"
      if docker compose logs "$1" | grep "$2"; then
          return 0
      fi
      sleep "${CHECKINTERVAL}"
  done

  fail-now "timed out waiting for agent to sync down entry"
}

check-x509-svid-count() {
  MAXCHECKS=50
  CHECKINTERVAL=1

  for ((i=1;i<=MAXCHECKS;i++)); do
    log-info "check X.509-SVID count on agent debug endpoint ($((i)) of $MAXCHECKS max)..."
    COUNT=$(docker compose exec -T "$1" /opt/spire/conf/agent/debugclient -testCase "printDebugPage" | jq '.svidsCount')
    log-info "X.509-SVID Count: ${COUNT}"
    if [ "$COUNT" -eq "$2" ]; then
      log-info "X.509-SVID count of $COUNT from cache matches the expected count of $2"
      break
    fi
    sleep "${CHECKINTERVAL}"
  done

  if (( i>MAXCHECKS )); then
      fail-now "X.509-SVID count validation failed"
  fi
}

build-mashup-image() {
    ENVOY_VERSION=$1
    ENVOY_IMAGE_TAG="${ENVOY_VERSION}-latest"

    cat > Dockerfile <<EOF
FROM spire-agent:latest-local as spire-agent
FROM envoyproxy/envoy:${ENVOY_IMAGE_TAG} AS envoy-agent-mashup
COPY --from=spire-agent /opt/spire/bin/spire-agent /opt/spire/bin/spire-agent
RUN apt-get update
RUN apt-get install -y dumb-init
RUN apt-get install -y supervisor
COPY conf/supervisord.conf /etc/
ENTRYPOINT ["/usr/bin/dumb-init", "supervisord", "--nodaemon", "--configuration", "/etc/supervisord.conf"]
CMD []
EOF

    docker build --target envoy-agent-mashup -t envoy-agent-mashup .
}

curl-with-retry() {
    curl --retry 5 --retry-all-errors --retry-max-time 120 "$@"
}

envoy-releases() {
    # Get the version list by downloading the JSON release listing, grabbing all of
    # the tag names, cutting out the quoted version, and sorting by reverse version
    # order. jq would make this much nicer, but we don't want to rely on it being
    # in the environment or downloading it just for this use case. Also, "sort -V"
    # is a thing, but unfortunately isn't available everyhere.
    #
    # The rest of the command strips off the point release to produce a sorted,
    # unique list of minor versions. We take the most recent MAX_ENVOY_RELEASES_TO_TEST
    # number of versions to test.
    ALL_ENVOY_RELEASES="$(curl-with-retry -Ls https://api.github.com/repos/envoyproxy/envoy/releases?per_page=100 \
        | grep tag_name \
        | cut -d\" -f4-4 \
        | cut -d. -f-2 \
        | sort -u -t. -k 1,1nr -k 2,2nr \
        | head -n"${MAX_ENVOY_RELEASES_TO_TEST}"
        )"

    # Now scan the releases and ensure the ones we test are available on Docker
    # Hub.  Normally they should all be available, but there has been latency from
    # the Envoy team publishing the docker image, so this prevents us from trying
    # to test an unpublished release image. This loop also stops if we try and test
    # earlier than v1.13, which is the first release to adopt the v3 API (v2 is
    # deprecated).
    for release in ${ALL_ENVOY_RELEASES}; do
        if ! curl-with-retry --silent -f -lSL "https://hub.docker.com/v2/repositories/envoyproxy/envoy/tags/${release}-latest" > /dev/null 2>/dev/null; then
            continue
        fi

        ENVOY_RELEASES_TO_TEST+=( "${release}" )

        if [ "${release}" = "${EARLIEST_ENVOY_RELEASE_TO_TEST}" ]; then
            break
        fi
    done

    if [ "${#ENVOY_RELEASES_TO_TEST[@]}" -eq 0 ]; then
        fail-now "Could not identify an appropriate Envoy image to test against"
    fi
}

download-bin() {
    local bin_path=$1
    local bin_url=$2
    if [ ! -f "${bin_path}" ] ; then
        log-info "downloading $(basename "${bin_path}") from ${bin_url}..."
        curl-with-retry -# -f -Lo "${bin_path}" "${bin_url}"
        chmod +x "${bin_path}"
    fi
}

download-kind() {
    KINDVERSION=${KINDVERSION:-v0.27.0}
    KINDPATH=$(command -v kind || echo)
    UNAME=$(uname | awk '{print tolower($0)}')
    ARCH=$(uname -m)
    if [ "${ARCH}" = "x86_64" ]; then
        ARCH=amd64
    elif [ "${ARCH}" = "aarch64" ]; then
        ARCH=arm64
    fi
    echo "Ensuring kind version $KINDVERSION is available..." 
    KINDURL="https://github.com/kubernetes-sigs/kind/releases/download/$KINDVERSION/kind-$UNAME-$ARCH"

    local kind_link_or_path=$1
    # Ensure kind exists at the expected version
    if [ -x "${KINDPATH}" ] && "${KINDPATH}" version | grep -q "${KINDVERSION}"; then
        ln -s "${KINDPATH}" "${kind_link_or_path}"
    else
        download-bin "${kind_link_or_path}" "${KINDURL}"
    fi
}

download-helm() {
    HELMVERSION=${HELMVERSION:-v3.17.1}
    HELMPATH=$(command -v helm || echo)
    UNAME=$(uname | awk '{print tolower($0)}')
    ARCH=$(uname -m)
    if [ "${ARCH}" = "x86_64" ]; then
        ARCH=amd64
    elif [ "${ARCH}" = "aarch64" ]; then
        ARCH=arm64
    fi

    echo "Ensuring helm version $HELMVERSION is available..." 
    HELMURL="https://get.helm.sh/helm-${HELMVERSION}-${UNAME}-${ARCH}.tar.gz"

    local helm_link_or_path=$1
    # Ensure helm exists at the expected version
    if [ -x "${HELMPATH}" ] && "${HELMPATH}" version | grep -q "${HELMSION}"; then
        ln -s "${HELMPATH}" "${helm_link_or_path}"
    else
        curl-with-retry -# -f -LO "${HELMURL}"
        tar zxvf "helm-${HELMVERSION}-${UNAME}-${ARCH}.tar.gz"
        cp "${UNAME}-${ARCH}"/helm "${helm_link_or_path}"
        chmod +x "${helm_link_or_path}"
        ls -l "${helm_link_or_path}"
    fi
}

download-kubectl() {
    WANTVERSION=${KUBECTLVERSION:-v1.30.10}
    KUBECTLPATH=$(command -v kubectl || echo)
    UNAME=$(uname | awk '{print tolower($0)}')
    ARCH=$(uname -m)
    if [ "${ARCH}" = "x86_64" ]; then
        ARCH=amd64
    elif [ "${ARCH}" = "aarch64" ]; then
        ARCH=arm64
    fi

    KUBECTLURL="https://dl.k8s.io/release/$WANTVERSION/bin/$UNAME/$ARCH/kubectl"

    HAVEVERSION=""
    if [ -x "${KUBECTLPATH}" ]; then
        HAVEVERSION="$("${KUBECTLPATH}" version --client --output=json | jq -r .clientVersion.gitVersion)"
    fi

    echo "Want kubectl version: ${WANTVERSION}"
    echo "Have kubectl version: ${HAVEVERSION}"

    local kubectl_link_or_path=$1
    # Ensure kubectl exists at the expected version
    if [ "${HAVEVERSION}" = "${WANTVERSION}" ]; then
        ln -s "${KUBECTLPATH}" "${kubectl_link_or_path}"
    else
        download-bin "${kubectl_link_or_path}" "${KUBECTLURL}"
    fi
}

start-kind-cluster() {
    K8SIMAGE=${K8SIMAGE:-kindest/node:v1.30.10}

    local kind_path=$1
    local kind_name=$2
    local kind_config_path=$3

    log-info "starting cluster..."
    "${kind_path}" create cluster --name "${kind_name}" --config "${kind_config_path}" --image "${K8SIMAGE}" || fail-now "unable to create cluster"
}

load-images() {
    local kind_path=$1; shift
    local kind_name=$1; shift
    local container_images=("$@")

    log-info "loading container images..."
    for image in "${container_images[@]}"; do
        "${kind_path}" load docker-image --name "${kind_name}" "${image}"
    done
}

set-kubectl-context() {
    local kubectl_path=$1
    local context=$2

    log-info "setting kubectl cluster context..."
    "${kubectl_path}" cluster-info --context "${context}"
}
