#!/bin/bash
#
#   Licensed under the Apache License, Version 2.0 (the "License");
#   you may not use this file except in compliance with the License.
#   You may obtain a copy of the License at
#
#       http://www.apache.org/licenses/LICENSE-2.0
#
#   Unless required by applicable law or agreed to in writing, software
#   distributed under the License is distributed on an "AS IS" BASIS,
#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#   See the License for the specific language governing permissions and
#   limitations under the License.
#

# Convert input string to a safe name for K8s resources
function safe_name() {
  local INPUT="$1"

  echo "${INPUT,,}" | \
    sed '/\.\// s|./||' | \
    sed 's|\.|-|g' | \
    sed 's|/|-|g' | \
    sed 's|_|-|g' | \
    sed 's| |-|g'
}


# Helper function to create a new age key pair
function create_age_keypair() {
  local AGE_KEY_NAME="$1"
  local CREDENTIALS_DIR="${2:-"${CREDENTIALS_DIR}"}"

  # Delete the keys in case they existed already
  rm -f "${CREDENTIALS_DIR}/${AGE_KEY_NAME}.key" "${CREDENTIALS_DIR}/${AGE_KEY_NAME}.pub"

  # Private key
  age-keygen -o "${CREDENTIALS_DIR}/${AGE_KEY_NAME}.key"

  # Public key (extracted from comment at private key)
  age-keygen -y "${CREDENTIALS_DIR}/${AGE_KEY_NAME}.key" > "${CREDENTIALS_DIR}/${AGE_KEY_NAME}.pub"
}


# Helper function to in-place encrypt secrets in manifest
function encrypt_secret_inplace() {
  local FILE="$1"
  local PUBLIC_KEY="$2"

  sops \
    --age=${PUBLIC_KEY} \
    --encrypt \
    --encrypted-regex '^(data|stringData)$' \
    --in-place "${FILE}"
}


# Helper function to encrypt secrets from stdin
function encrypt_secret_from_stdin() {
  local PUBLIC_KEY="$1"

  # Save secret manifest to temporary file
  local TMPFILE=$(mktemp /tmp/secret.XXXXXXXXXX) || exit 1
  cat > "${TMPFILE}"
  # NOTE: Required workaround for busybox's version of `mktemp`, which is quite limited and does not support temporary files with extensions.
  #       `.yaml` is required for proper `sops` behaviour.
  mv "${TMPFILE}" "${TMPFILE}.yaml"

  # Encrypt
  sops \
    --age=${PUBLIC_KEY} \
    --encrypt \
    --encrypted-regex '^(data|stringData)$' \
    --in-place "${TMPFILE}.yaml"

  # Outputs the result and removes the temporary file
  cat "${TMPFILE}.yaml" && rm -f "${TMPFILE}.yaml"
}


# Helper function to create secret manifest and encrypt with public key
function kubectl_encrypt() {
  local PUBLIC_KEY="$1"

  # Gathers all optional parameters for transformer funcion (if any) and puts them into an array for further use
  local ALL_PARAMS=( "${@}" )
  local PARAMS=( "${ALL_PARAMS[@]:1}" )

  kubectl \
    "${PARAMS[@]}" | \
  encrypt_secret_from_stdin \
    "${PUBLIC_KEY}"
}


# Generator function to convert source folder to `ResourceList`
function folder2list_generator() {
  local FOLDER="${1:-}"
  local SUBSTENV="${2:-"false"}"
  local FILTER="${3:-""}"

  if [[ "${SUBSTENV,,}" == "true" ]];
  then
    # Mix input with new generated manifests and replace environment variables
    join_lists \
      <(cat) \
      <(
        kpt fn source "${FOLDER}" | \
        replace_env_vars "${FILTER}"
      )
  else
    # Mix input with new generated manifests
    join_lists \
      <(cat) \
      <(
        kpt fn source "${FOLDER}"
      )
  fi

}


# Function to convert source folder to `ResourceList` (no generator)
function folder2list() {
  local FOLDER="${1:-}"

  kpt fn source "${FOLDER}"
}


# Helper function to convert manifest to `ResourceList`
function manifest2list() {
  kustomize cfg cat --wrap-kind ResourceList
}


# Helper function to convert `ResourceList` to manifests in folder structure.
# - New folder must be created to render the manifests.
function list2folder() {
  local FOLDER="${1:-}"
  local DRY_RUN="${2:-${DRY_RUN:-false}}"

  if [[ "${DRY_RUN,,}" == "true" ]];
  then
    cat
  else
    kpt fn sink "${FOLDER}"
  fi
}


# Helper function to convert `ResourceList` to manifests in folder structure.
# - It copies (cp) the generated files/subfolders over the target folder.
# - Pre-existing files and subfolder structure in target folder is preserved.
function list2folder_cp_over() {
  local FOLDER="${1:-}"
  local DRY_RUN="${2:-${DRY_RUN:-false}}"

  if [[ "${DRY_RUN,,}" == "true" ]];
  then
    cat
  else
    local TMPFOLDER=$(mktemp -d) || exit 1
    kpt fn sink "${TMPFOLDER}/manifests"

    # Copy the generated files over the target folder
    mkdir -p "${FOLDER}/"
    cp -r "${TMPFOLDER}/manifests/"* "${FOLDER}/"

    # Delete temporary folder
    rm -rf "${TMPFOLDER}"
  fi
}


# Helper function to convert `ResourceList` to manifests in folder structure.
# - It syncs the generated files/subfolders over the target folder.
# - Pre-existing files and subfolder structure in target folder is deleted if not present in `ResourceList`.
function list2folder_sync_replace() {
  local FOLDER="${1:-}"
  local DRY_RUN="${2:-${DRY_RUN:-false}}"

  if [[ "${DRY_RUN,,}" == "true" ]];
  then
    cat
  else
    local TMPFOLDER=$(mktemp -d) || exit 1
    kpt fn sink "${TMPFOLDER}/manifests"

    # Copy the generated files over the target folder
    mkdir -p "${FOLDER}/"
    rsync -arh --exclude ".git" --exclude ".*" --delete \
      "${TMPFOLDER}/manifests/" "${FOLDER}/"

    # Delete temporary folder
    rm -rf "${TMPFOLDER}"
  fi
}


# Helper function to render **SAFELY** a single manifest coming from stdin into a profile, with a proper KSU subfolder
function render_manifest_over_ksu() {
  local KSU_NAME="$1"
  local TARGET_PROFILE_FOLDER="$2"
  local MANIFEST_FILENAME="$3"

  manifest2list | \
  set_filename_to_items \
    "${MANIFEST_FILENAME}" | \
  prepend_folder_path \
    "${KSU_NAME}/" | \
  list2folder_cp_over \
    "${TARGET_PROFILE_FOLDER}"
}


# Set filename to `ResourceList` item
function set_filename_to_items() {
  local FILENAME="$1"

  yq "(.items[]).metadata.annotations.\"config.kubernetes.io/path\" |= \"${FILENAME}\"" | \
  yq "(.items[]).metadata.annotations.\"internal.config.kubernetes.io/path\" |= \"${FILENAME}\""
}


# Prepend folder path to `ResourceList`
function prepend_folder_path() {
  local PREFIX="$1"

  if [[ (-z "${PREFIX}") || ("${PREFIX}" == ".") ]];
  then
    cat
  else
    yq "(.items[]).metadata.annotations.\"config.kubernetes.io/path\" |= \"${PREFIX}\" + ." | \
    yq "(.items[]).metadata.annotations.\"internal.config.kubernetes.io/path\" |= \"${PREFIX}\" + ."
  fi
}


# Rename file in `ResourceList`
function rename_file_in_items() {
  local SOURCE_NAME="$1"
  local DEST_NAME="$2"

  yq "(.items[].metadata.annotations | select (.\"config.kubernetes.io/path\" == \"${SOURCE_NAME}\")).\"config.kubernetes.io/path\" = \"${DEST_NAME}\"" | \
  yq "(.items[].metadata.annotations | select (.\"internal.config.kubernetes.io/path\" == \"${SOURCE_NAME}\")).\"internal.config.kubernetes.io/path\" = \"${DEST_NAME}\""
}


# Get value from key in object in `ResourceList`
function get_value_from_resourcelist() {
  local KEY_PATH="$1"
  local TARGET_FILTERS="${2:-}"
  # Example: To get a specific kind ("ProviderConfig") with a specific name ("default"). (TIP: Note the escaped double quotes).
  # TARGET_FILTERS="| select(.kind == \"ProviderConfig\") | select(.metadata.name == \"default\")"

  yq "(.items[]${TARGET_FILTERS})${KEY_PATH}"
}


# Patch "replace" to item in `ResourceList`
function patch_replace() {
  local KEY_PATH="$1"
  local VALUE="$2"
  local TARGET_FILTERS="${3:-}"
  # Example: To only patch a specific kind ("ProviderConfig") with a specific name ("default"). (TIP: Note the escaped double quotes).
  # TARGET_FILTERS="| select(.kind == \"ProviderConfig\") | select(.metadata.name == \"default\")"

  yq "(.items[]${TARGET_FILTERS})${KEY_PATH} = \"${VALUE}\""
}


# Add label to item in `ResourceList`
function set_label() {
  local KEY="$1"
  local VALUE="$2"
  local TARGET_FILTERS="${3:-}"
  # Example: To only patch a specific kind ("ProviderConfig") with a specific name ("default"). (TIP: Note the escaped double quotes).
  # TARGET_FILTERS="| select(.kind == \"ProviderConfig\") | select(.metadata.name == \"default\")"

  yq "(.items[]${TARGET_FILTERS}).metadata.labels.${KEY} = \"${VALUE}\""
}


# Patch which "appends" to list existing in item in `ResourceList`
function patch_add_to_list() {
  local KEY_PATH="$1"
  local VALUE="$2"
  local TARGET_FILTERS="${3:-}"
  # Example: To only patch a specific kind ("ProviderConfig") with a specific name ("default"). (TIP: Note the escaped double quotes).
  # TARGET_FILTERS="| select(.kind == \"ProviderConfig\") | select(.metadata.name == \"default\")"

  local VALUE_AS_JSON="$(echo "${VALUE}" | yq -o json -I0)"

  yq "(.items[]${TARGET_FILTERS})${KEY_PATH} += ${VALUE_AS_JSON}"
}


# Patch which removes from list, existing in item in `ResourceList`
function patch_delete_from_list() {
  local KEY_PATH="$1"
  local TARGET_FILTERS="${2:-}"

  # local VALUE_AS_JSON="$(echo "${VALUE}" | yq -o json -I0)"

  yq "del((.items[]${TARGET_FILTERS})${KEY_PATH})"
}


# Check if an element/value is in a given list, existing in item in `ResourceList`
function is_element_on_list() {
  local KEY_PATH="$1"
  local VALUE="$2"
  local TARGET_FILTERS="${3:-}"
  # Example: To only patch a specific kind ("ProviderConfig") with a specific name ("default"). (TIP: Note the escaped double quotes).
  # TARGET_FILTERS="| select(.kind == \"ProviderConfig\") | select(.metadata.name == \"default\")"

  TEST_RESULT=$(
    cat | \
    yq "(.items[]${TARGET_FILTERS})${KEY_PATH} == \"${VALUE}\"" | grep "true"
  )

  if [[ "${TEST_RESULT}" != "true" ]]
  then
    echo "false"
  else
    echo "true"
  fi
}


# Patch "replace" to item in `ResourceList` using a JSON as value
function patch_replace_inline_json() {
  local KEY_PATH="$1"
  local VALUE="$2"
  local TARGET_FILTERS="${3:-}"
  # Example: To only patch a specific kind ("ProviderConfig") with a specific name ("default"). (TIP: Note the escaped double quotes).
  # TARGET_FILTERS="| select(.kind == \"ProviderConfig\") | select(.metadata.name == \"default\")"

  VALUE_AS_JSON="$(echo "${VALUE}" | yq -o=json)" yq "(.items[]${TARGET_FILTERS})${KEY_PATH} = strenv(VALUE_AS_JSON)"
}


# Delete full object from `ResourceList`
function delete_object() {
  local OBJECT_NAME="$1"
  local KIND_NAME="$2"
  local API_VERSION="${3:-""}"

  # Calculated inputs
  if [[ -z "${API_VERSION}" ]]
  then
    # If `apiVersion` is not specified
    local TARGET_FILTER="| select(.kind == \"${KIND_NAME}\") | select(.metadata.name == \"${OBJECT_NAME}\")"
  else
    # Otherwise, it is taken into account
    local TARGET_FILTER="| select(.kind == \"${KIND_NAME}\") | select(.apiVersion == \"${API_VERSION}\") | select(.metadata.name == \"${OBJECT_NAME}\")"
  fi

  # Delete object
  yq "del((.items[]${TARGET_FILTER}))"
}


# Empty transformer function
function noop_transformer() {
  cat
}


# Add patch to `Kustomization` item in `ResourceList`
function add_patch_to_kustomization() {
  local KUSTOMIZATION_NAME="$1"
  local FULL_PATCH_CONTENT="$2"

  patch_add_to_list \
    ".spec.patches" \
    "${FULL_PATCH_CONTENT}" \
    "| select(.kind == \"Kustomization\") | select(.metadata.name == \"${KUSTOMIZATION_NAME}\")"
}


# Helper function to produce a JSON Patch as specified in RFC 6902
function as_json_patch() {
  local OPERATION="$1"
  local PATCH_PATH="$2"
  local VALUES="$3"

  # Convert to JSON dictionary to insert as map instead of string
  local VALUES_AS_DICT=$(echo "${VALUES}" | yq -o=json)

  # Generate a patch list
  cat <<EOF | yq ".[0].value = ${VALUES_AS_DICT}"
- op: ${OPERATION}
  path: ${PATCH_PATH}
EOF
}


# Helper function to produce a full patch, with target object + JSON Patch RFC 6902
function full_json_patch() {
  local TARGET_KIND="$1"
  local TARGET_NAME="$2"
  local OPERATION="$3"
  local PATCH_PATH="$4"
  # Gathers all optional parameters for transformer funcion (if any) and puts them into an array for further use
  local ALL_PARAMS=( "${@}" )
  local VALUES=( "${ALL_PARAMS[@]:4}" )

  # Accumulates value items into the patch
  local PATCH_CONTENT=""
  for VAL in "${VALUES[@]}"
  do
    local VAL_AS_DICT=$(echo "${VAL}" | yq -o=json)

    ITEM=$(
      yq --null-input ".op = \"${OPERATION}\", .path = \"${PATCH_PATH}\"" | \
      yq ".value = ${VAL_AS_DICT}" | \
      yq "[ . ]"
    )

    PATCH_CONTENT="$(echo -e "${PATCH_CONTENT}\n${ITEM}")"
  done

  # Wrap a full patch around, adding target specification
  local PATCH_FULL=$(
    yq --null-input ".target.kind = \"${TARGET_KIND}\", .target.name = \"${TARGET_NAME}\"" | \
    yq ".patch = \"${PATCH_CONTENT}\"" | \
    yq "[ . ]"
  )

  echo "${PATCH_FULL}"
}


# Add values to `HelmRelease` by patch into `Kustomization` item in `ResourceList`
function add_values_to_helmrelease_via_ks() {
  local KUSTOMIZATION_NAME="$1"
  local HELMRELEASE_NAME="$2"
  local VALUES="$3"

  # Embed into patch list
  local FULL_PATCH_CONTENT="$(
    full_json_patch \
      "HelmRelease" \
      "${HELMRELEASE_NAME}" \
      "add" \
      "/spec/values" \
      "${VALUES}"
  )"

  # Path via intermediate Kustomization object
  add_patch_to_kustomization \
    "${KUSTOMIZATION_NAME}" \
    "${FULL_PATCH_CONTENT}"
}


# Add values from Secret/ConfigMap to `HelmRelease` by patch into `Kustomization` item in `ResourceList`
function add_referenced_values_to_helmrelease_via_ks() {
  local KUSTOMIZATION_NAME="$1"
  local HELMRELEASE_NAME="$2"
  local VALUES_FROM="$3"

  # Embed into patch list
  local FULL_PATCH_CONTENT="$(
    full_json_patch \
      "HelmRelease" \
      "${HELMRELEASE_NAME}" \
      "add" \
      "/spec/valuesFrom" \
      "${VALUES_FROM}"
  )"

  # Path via intermediate Kustomization object
  add_patch_to_kustomization \
    "${KUSTOMIZATION_NAME}" \
    "${FULL_PATCH_CONTENT}"
}


# High level function to add values from Secret, ConfigMap or both to `HelmRelease` by patch into `Kustomization` item in `ResourceList`
function add_ref_values_to_hr_via_ks() {
  local KUSTOMIZATION_NAME="$1"
  local HELMRELEASE_NAME="$2"
  local VALUES_SECRET_NAME="${3:-""}"
  local VALUES_CM_NAME="${4:-""}"

  local YAML_VALUES_FROM_BOTH=$(cat <<EOF
- kind: Secret
  name: "${VALUES_SECRET_NAME}"
- kind: ConfigMap
  name: "${VALUES_CM_NAME}"
EOF
  )
  local YAML_VALUES_FROM_SECRET=$(cat <<EOF
- kind: Secret
  name: "${VALUES_SECRET_NAME}"
EOF
  )
  local YAML_VALUES_FROM_CM=$(cat <<EOF
- kind: ConfigMap
  name: "${VALUES_CM_NAME}"
EOF
  )

  # Chooses the appropriate YAML
  VALUES_FROM=""
  if [[ ( -n "${VALUES_SECRET_NAME}" ) && ( -n "${VALUES_CM_NAME}" ) ]];
  then
    VALUES_FROM="${YAML_VALUES_FROM_BOTH}"
  elif [[ -n "${VALUES_SECRET_NAME}" ]];
  then
    VALUES_FROM="${YAML_VALUES_FROM_SECRET}"
  elif [[ -n "${VALUES_CM_NAME}" ]];
  then
    VALUES_FROM="${YAML_VALUES_FROM_CM}"
  else
    # If none is set, it must be an error
    return 1
  fi

  # Calls the low-level function
  add_referenced_values_to_helmrelease_via_ks \
    "${KUSTOMIZATION_NAME}" \
    "${HELMRELEASE_NAME}" \
    "${VALUES_FROM}"
}

# Substitute environment variables from stdin
function replace_env_vars() {
  # Optional parameter to filter environment variables that can be replaced
  local FILTER=${1:-}

  if [[ -n "${FILTER}" ]];
  then
    envsubst "${FILTER}"
  else
    envsubst
  fi
}


# Join two `ResourceList` **files**
#
# Examples of use:
# $ join_lists list_file1.yaml list_file2.yaml
# $ join_lists <(manifest2list < manifest_file1.yaml) <(manifest2list < manifest_file2.yaml)
# $ cat prueba1.yaml | manifest2list | join_lists - <(manifest2list < prueba2.yaml)
#
# NOTE: Duplicated keys and arrays may be overwritten by the latest file.
# See: https://stackoverflow.com/questions/66694238/merging-two-yaml-documents-while-concatenating-arrays
function join_lists() {
  local FILE1="$1"
  local FILE2="$2"

  yq eval-all '. as $item ireduce ({}; . *+ $item)' \
    "${FILE1}" \
    "${FILE2}"
}


# Helper function to create a generator from a function that creates manifests
function make_generator() {
  local MANIFEST_FILENAME="$1"
  local SOURCER_FUNCTION="$2"
  # Gathers all optional parameters for the funcion (if any) and puts them into an array for further use
  local ALL_PARAMS=( "${@}" )
  local PARAMS=( "${ALL_PARAMS[@]:2}" )

  # Mix input with new generated manifests
  join_lists \
    <(cat) \
    <(
      "${SOURCER_FUNCTION}" \
        "${PARAMS[@]}" | \
      manifest2list | \
      set_filename_to_items "${MANIFEST_FILENAME}"
    )
}


function transform_if() {
  local TEST_RESULT=$1

  # Gathers all optional parameters for transformer funcion (if any) and puts them into an array for further use
  local ALL_PARAMS=( "${@}" )
  local PARAMS=( "${ALL_PARAMS[@]:1}" )

  # If test result is true (==0), then runs the transformation normally
  if [[ "${TEST_RESULT}" == "0" ]];
  then
    "${PARAMS[@]}"
  # Otherwise, just pass through
  else
    cat
  fi
}


# Helper function to convert multiline input from stdin to comma-separed output
function multiline2commalist() {
  mapfile -t TMP_ARRAY < <(cat)
  printf -v TMP_LIST '%s,' "${TMP_ARRAY[@]}"
  echo "${TMP_LIST}" | sed 's/,$//g'
}


# Helper function to check pending changes in workdir to `fleet` repo
function check_fleet_workdir_status() {
  local FLEET_REPO_DIR="${1:-${FLEET_REPO_DIR}}"

  pushd "${FLEET_REPO_DIR}"
  git status
  popd
}


# Helper function to commit changes in workdir to `fleet` repo
function commit_and_push_to_fleet() {
  local DEFAULT_COMMIT_MESSAGE="Committing latest changes to fleet repo at $(date +'%Y-%m-%d %H:%M:%S')"
  local COMMIT_MESSAGE="${1:-${DEFAULT_COMMIT_MESSAGE}}"
  local FLEET_REPO_DIR="${2:-${FLEET_REPO_DIR}}"

  pushd "${FLEET_REPO_DIR}"
  git status
  git add -A
  git commit -m "${COMMIT_MESSAGE}"
  echo "${COMMIT_MESSAGE}"
  git push
  popd
}
