#!/usr/bin/env bash
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

# set -ux # uncomment this line to debug
# TODO: use https://www.shellcheck.net to lint this script and make recommended updates

aksNamespace="graphrag"

# Optional parameters with default values
AI_SEARCH_AUDIENCE="https://search.azure.com"
AISEARCH_ENDPOINT_SUFFIX="search.windows.net"
APIM_NAME=""
APIM_TIER="Developer"
CLOUD_NAME="AzurePublicCloud"
GRAPHRAG_IMAGE="graphrag:backend"
PUBLISHER_EMAIL="publisher@microsoft.com"
PUBLISHER_NAME="publisher"
RESOURCE_BASE_NAME=""
COGNITIVE_SERVICES_AUDIENCE="https://cognitiveservices.azure.com/.default"
CONTAINER_REGISTRY_LOGIN_SERVER=""
GRAPHRAG_API_BASE=""
GRAPHRAG_API_VERSION="2023-03-15-preview"
GRAPHRAG_LLM_MODEL="gpt-4"
GRAPHRAG_LLM_MODEL_VERSION="turbo-2024-04-09"
GRAPHRAG_LLM_DEPLOYMENT_NAME="gpt-4"
GRAPHRAG_LLM_MODEL_QUOTA="80"
GRAPHRAG_EMBEDDING_MODEL="text-embedding-ada-002"
GRAPHRAG_EMBEDDING_MODEL_VERSION="2"
GRAPHRAG_EMBEDDING_DEPLOYMENT_NAME="text-embedding-ada-002"
GRAPHRAG_EMBEDDING_MODEL_QUOTA="300"

requiredParams=(
    LOCATION
    RESOURCE_GROUP
)
optionalParams=(
    AI_SEARCH_AUDIENCE
    AISEARCH_ENDPOINT_SUFFIX
    APIM_NAME
    APIM_TIER
    CLOUD_NAME
    GRAPHRAG_IMAGE
    PUBLISHER_EMAIL
    PUBLISHER_NAME
    RESOURCE_BASE_NAME
    COGNITIVE_SERVICES_AUDIENCE
    CONTAINER_REGISTRY_LOGIN_SERVER
    GRAPHRAG_API_BASE
    GRAPHRAG_API_VERSION
    GRAPHRAG_LLM_MODEL
    GRAPHRAG_LLM_MODEL_QUOTA
    GRAPHRAG_LLM_MODEL_VERSION
    GRAPHRAG_LLM_DEPLOYMENT_NAME
    GRAPHRAG_EMBEDDING_MODEL
    GRAPHRAG_EMBEDDING_MODEL_QUOTA
    GRAPHRAG_EMBEDDING_MODEL_VERSION
    GRAPHRAG_EMBEDDING_DEPLOYMENT_NAME
)

errorBanner () {
# https://cowsay-svelte.vercel.app
cat << "EOF"
 ________________________________
/  Uh oh, an error has occurred. \
\  Please see message below.     /
 ‾‾‾‾‾‾‾‾‾‾/‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
          /
      __ /
     /  \
    ~    ~
   / \  /_\
   \o/  \o/
    |    |
    ||   |/
    ||   ||
    ||   ||
    | \_/ |
    \     /
     \___/
EOF
printf "\n"
}

successBanner () {
# https://patorjk.com/software/taag
cat << "EOF"
   _____                             __       _
  / ____|                           / _|     | |
 | (___  _   _  ___ ___ ___ ___ ___| |_ _   _| |
  \___ \| | | |/ __/ __/ _ / __/ __|  _| | | | |
  ____) | |_| | (_| (_|  __\__ \__ | | | |_| | |
 |_____/ \__,_|\___\___\___|___|___|_|  \__,_|_|      _   _
     | |          | |                                | | | |
   __| | ___ _ __ | | ___  _   _ _ __ ___   ___ _ __ | |_| |
  / _` |/ _ | '_ \| |/ _ \| | | | '_ ` _ \ / _ | '_ \| __| |
 | (_| |  __| |_) | | (_) | |_| | | | | | |  __| | | | |_|_|
  \__,_|\___| .__/|_|\___/ \__, |_| |_| |_|\___|_| |_|\__(_)
            | |             __/ |
            |_|            |___/
EOF
printf "\n\n"
}

startBanner () {
# https://patorjk.com/software/taag
cat << "EOF"
   _____                 _     _____           _____
  / ____|               | |   |  __ \    /\   / ____|
 | |  __ _ __ __ _ _ __ | |__ | |__) |  /  \ | |  __
 | | |_ | '__/ _` | '_ \| '_ \|  _  /  / /\ \| | |_ |
 | |__| | | | (_| | |_) | | | | | \ \ / ____ | |__| |
  \_____|_|  \__,_| .__/|_| |_|_|  \_/_/_   \_\_____|
     /\           | | | |              | |
    /  \   ___ ___|_|_| | ___ _ __ __ _| |_ ___  _ __
   / /\ \ / __/ __/ _ | |/ _ | '__/ _` | __/ _ \| '__|
  / ____ | (_| (_|  __| |  __| | | (_| | || (_) | |
 /_/    \_\___\___\___|_|\___|_|  \__,_|\__\___/|_|
EOF
printf "\n\n"
}

exitIfCommandFailed () {
    local res=$1
    local msg=$2
    if [ 0 -ne $res ]; then
        errorBanner
        printf "$msg\n"
        exit 1
    fi
}

exitIfValueEmpty () {
    local value=$1
    local msg=$2
    # check if the value is empty or "null" (jq returns "null" when a value is not found)
    if [ -z "$value" ] || [[ "$value" == "null" ]]; then
        errorBanner
        printf "$msg\n"
        exit 1
    fi
}

exitIfThresholdExceeded () {
    local value=$1
    local threshold=$2
    local msg=$3
    # throw an error if input value exceeds threshold
    if [ $value -ge $threshold ]; then
        errorBanner
        printf "$msg\n"
        exit 1
    fi
}

versionCheck () {
    # assume the version is in the format major.minor.patch
    local TOOL=$1
    local VERSION=$2
    local MINIMUM_VERSION_REQUIREMENT=$3
    for i in 1 2 3; do
        part1=$(echo $VERSION | cut -d "." -f $i)
        part2=$(echo $MINIMUM_VERSION_REQUIREMENT | cut -d "." -f $i)
        if [ $part1 -gt $part2 ]; then
            return 0
        fi
        if [ $part1 -lt $part2 ]; then
            echo "$TOOL version requirement >= $MINIMUM_VERSION_REQUIREMENT, but you have version $VERSION"
            exit 1
        fi
    done
}

checkRequiredTools () {
    local JQ_VERSION
    local major minor patch
    local YQ_VERSION
    local AZ_VERSION

    printf "Checking for required tools... "

    which sed > /dev/null
    exitIfCommandFailed $? "sed is required, exiting..."

    which kubectl > /dev/null
    exitIfCommandFailed $? "kubectl is required, exiting..."

    which kubelogin > /dev/null
    exitIfCommandFailed $? "kubelogin is required, exiting..."

    which helm > /dev/null
    exitIfCommandFailed $? "helm is required, exiting..."

    which jq > /dev/null
    exitIfCommandFailed $? "jq is required, exiting..."

    which yq > /dev/null
    exitIfCommandFailed $? "yq is required, exiting..."

    which az > /dev/null
    exitIfCommandFailed $? "azcli is required, exiting..."

    which curl > /dev/null
    exitIfCommandFailed $? "curl is required, exiting..."

    # minimum version check for jq, yq, and az cli
    JQ_VERSION=$(jq --version | cut -d'-' -f2)
    IFS='.' read -r major minor patch <<< "$JQ_VERSION"
    if [ -z $patch ]; then
        # NOTE: older acceptable versions of jq report a version
        # number without the patch number. if patch version is
        # not present, set it to 0
        patch=0
        JQ_VERSION="$major.$minor.$patch"
    fi
    YQ_VERSION=$(yq --version | awk '{print substr($4,2)}')
    AZ_VERSION=$(az version -o json | jq -r '.["azure-cli"]')
    versionCheck "jq" $JQ_VERSION "1.6.0"
    versionCheck "yq" $YQ_VERSION "4.40.7"
    versionCheck "az cli" $AZ_VERSION "2.55.0"
    printf "Done.\n"
}

checkRequiredParams () {
    local paramsFile=$1
    local paramValue
    for param in "${requiredParams[@]}"; do
        paramValue=$(jq -r .$param < $paramsFile)
        if [ -z "$paramValue" ] || [ "$paramValue" == "null" ]; then
            echo "Parameter $param is required, exiting..."
            exit 1
        fi
    done
}

populateParams () {
    local paramsFile=$1
    printf "Checking required parameters... "
    checkRequiredParams $paramsFile
    printf "Done.\n"

    # The jq command below sets env variables based on the key-value pairs defined in a JSON-formatted parameters file.
    # This will override default values of previously defined env variables.
    eval $(jq -r 'to_entries | .[] | "export \(.key)=\(.value)"' $paramsFile)

    # print environment variables for end user
    echo "Setting environment variables..."
    for param in "${requiredParams[@]}"; do
        # skip empty variables
        if [ -z "${!param}" ]; then
            continue
        fi
        printf "\t$param = ${!param}\n"
    done
    for param in "${optionalParams[@]}"; do
        # skip empty variables
        if [ -z "${!param}" ]; then
            continue
        fi
        printf "\t$param = ${!param}\n"
    done
}

createResourceGroupIfNotExists () {
    local location=$1
    local rg=$2
    printf "Checking if resource group $rg exists... "
    az group show -n $rg -o json > /dev/null 2>&1
    if [ $? -ne 0 ]; then
        printf "No.\n"
        printf "Creating resource group... "
        az group create -l $location -n $rg > /dev/null 2>&1
        printf "Done.\n"
    else
        printf "Yes.\n"
    fi
}

getAksCredentials () {
    local rg=$1
    local aks_name
    local principalId
    local scope

    printf "Getting AKS credentials... "
    aks_name=$(jq -r .azure_aks_name.value <<< $AZURE_DEPLOY_OUTPUTS)
    az aks get-credentials -g $rg -n $aks_name --overwrite-existing > /dev/null 2>&1
    exitIfCommandFailed $? "Error getting AKS credentials, exiting..."
    kubelogin convert-kubeconfig -l azurecli
    exitIfCommandFailed $? "Error logging into AKS, exiting..."
    # get principal/object id of the signed in user
    principalId=$(az ad signed-in-user show --output json | jq -r .id)
    exitIfValueEmpty $principalId "Principal ID of deployer not found"
    # assign "Azure Kubernetes Service RBAC Admin" role to deployer
    scope=$(az aks show --resource-group $rg --name $aks_name --query "id" -o tsv)
    exitIfValueEmpty "$scope" "Unable to get AKS scope, exiting..."
    az role assignment create --role "Azure Kubernetes Service RBAC Cluster Admin" --assignee-object-id $principalId --scope $scope
    exitIfCommandFailed $? "Error assigning 'Azure Kubernetes Service RBAC Cluster Admin' role to deployer, exiting..."
    kubectl config set-context $aks_name --namespace=$aksNamespace
    printf "Done\n"
}

checkForApimSoftDelete () {
    local apimName
    local location
    local deleted_service_list_results

    printf "Checking if APIM was soft-deleted... "
    # This is an optional step to check if an APIM instance previously existed in the
    # resource group and is in a soft-deleted state. If so, purge it before deploying
    # a new APIM instance to prevent conflicts with the new deployment.
    deleted_service_list_results=$(az apim deletedservice list -o json --query "[?contains(serviceId, 'resourceGroups/$RESOURCE_GROUP/')].{name:name, location:location}")
    exitIfCommandFailed $? "Error checking for soft-deleted APIM instances, exiting..."
    apimName=$(jq -r .[0].name <<< $deleted_service_list_results)
    location=$(jq -r .[0].location <<< $deleted_service_list_results)
    # jq returns "null" if a value is not found
    if [ -z "$apimName" ] || [[ "$apimName" == "null" ]] || [ -z "$location" ] || [[ "$location" == "null" ]]; then
        printf "Done.\n"
        return 0
    fi
    if [ ! -z "$apimName" ] && [ ! -z "$location" ]; then
        printf "\nAPIM instance found in soft-deleted state. Purging...\n"
        az apim deletedservice purge -n $apimName --location "$location" > /dev/null
    fi
    printf "Done.\n"
}

deployAzureResources () {
    local deployAoai
    local existingAoaiId=""
    local deployAcr
    local graphragImageName
    local graphragImageVersion

    echo "Deploying Azure resources..."
    # deploy AOAI if the user did not provide links to an existing AOAI service
    deployAoai="true"
    if [ -n "$GRAPHRAG_API_BASE" ]; then
        deployAoai="false"
        existingAoaiId=$(az cognitiveservices account list --query "[?contains(properties.endpoint, '$GRAPHRAG_API_BASE')].id" -o tsv)
        exitIfValueEmpty "$existingAoaiId" "Unable to get AOAI resource id from GRAPHRAG_API_BASE, exiting..."
    fi
    deployAcr="true"
    if [ -n "$CONTAINER_REGISTRY_LOGIN_SERVER" ]; then
        deployAcr="false"
    fi
    graphragImageName=$(sed -rn "s/([^:]+).*/\1/p" <<< "$GRAPHRAG_IMAGE")
    graphragImageVersion=$(sed -rn "s/[^:]+:(.*)/\1/p" <<< "$GRAPHRAG_IMAGE")
    exitIfValueEmpty "$graphragImageName" "Unable to parse graphrag docker image name, exiting..."
    exitIfValueEmpty "$graphragImageVersion" "Unable to parse graphrag docker image version, exiting..."

    local datetime deployName AZURE_DEPLOY_RESULTS
    datetime="`date +%Y-%m-%d_%H-%M-%S`"
    deployName="graphrag-deploy-$datetime"
    echo "Deployment name: $deployName"
    AZURE_DEPLOY_RESULTS=$(az deployment group create --name "$deployName" \
        --no-prompt \
        --resource-group $RESOURCE_GROUP \
        --template-file ./main.bicep \
        --parameters "resourceBaseName=$RESOURCE_BASE_NAME" \
        --parameters "apimName=$APIM_NAME" \
        --parameters "apimTier=$APIM_TIER" \
        --parameters "apiPublisherEmail=$PUBLISHER_EMAIL" \
        --parameters "apiPublisherName=$PUBLISHER_NAME" \
        --parameters "enablePrivateEndpoints=$ENABLE_PRIVATE_ENDPOINTS" \
        --parameters "deployAcr=$deployAcr" \
        --parameters "existingAcrLoginServer=$CONTAINER_REGISTRY_LOGIN_SERVER" \
        --parameters "graphragImageName=$graphragImageName" \
        --parameters "graphragImageVersion=$graphragImageVersion" \
        --parameters "deployAoai=$deployAoai" \
        --parameters "existingAoaiId=$existingAoaiId" \
        --parameters "llmModelName=$GRAPHRAG_LLM_MODEL" \
        --parameters "llmModelDeploymentName=$GRAPHRAG_LLM_DEPLOYMENT_NAME" \
        --parameters "llmModelVersion=$GRAPHRAG_LLM_MODEL_VERSION" \
        --parameters "llmModelQuota=$GRAPHRAG_LLM_MODEL_QUOTA" \
        --parameters "embeddingModelName=$GRAPHRAG_EMBEDDING_MODEL" \
        --parameters "embeddingModelDeploymentName=$GRAPHRAG_EMBEDDING_DEPLOYMENT_NAME" \
        --parameters "embeddingModelVersion=$GRAPHRAG_EMBEDDING_MODEL_VERSION" \
        --parameters "embeddingModelQuota=$GRAPHRAG_EMBEDDING_MODEL_QUOTA" \
        --output json)
    # errors in deployment may not be caught by exitIfCommandFailed function so we also check the output for errors
    exitIfCommandFailed $? "Error deploying Azure resources..."
    exitIfValueEmpty "$AZURE_DEPLOY_RESULTS" "Error deploying Azure resources..."
    AZURE_DEPLOY_OUTPUTS=$(jq -r .properties.outputs <<< $AZURE_DEPLOY_RESULTS)
    exitIfCommandFailed $? "Error parsing outputs from Azure deployment..."
    exitIfValueEmpty "$AZURE_DEPLOY_OUTPUTS" "Error parsing outputs from Azure deployment..."

    # Must assign ACRPull role to aks if ACR was not part of the deployment (i.e. user chose to utilize an ACR resource external to this deployment)
    if [ -n "$CONTAINER_REGISTRY_LOGIN_SERVER" ]; then
        assignACRPullRoleToAKS $RESOURCE_GROUP $CONTAINER_REGISTRY_LOGIN_SERVER
    fi
}

assignACRPullRoleToAKS() {
    local rg=$1
    local registry=$2
    local aks_name kubelet_id acr_id

    echo "Assigning 'ACRPull' role to AKS..."
    aks_name=$(jq -r .azure_aks_name.value <<< $AZURE_DEPLOY_OUTPUTS)
    exitIfValueEmpty "$aks_name" "Unable to parse aks name from azure outputs, exiting..."
    kubelet_id=$(az aks show --resource-group $rg --name $aks_name --query identityProfile.kubeletidentity.objectId --output tsv)
    exitIfValueEmpty "$kubelet_id" "Unable to retrieve AKS kubelet id, exiting..."
    acr_id=$(az acr show --name $registry --query id -o tsv)
    exitIfValueEmpty "$acr_id" "Unable to retrieve ACR id, exiting..."
    az role assignment create --role "AcrPull" --assignee $kubelet_id --scope $acr_id
    exitIfCommandFailed $? "Error assigning ACRPull role to AKS, exiting..."
}

validateSKUs() {
    # Run SKU validation functions unless skip flag is set
    local location=$1
    local validate_skus=$2
    if [ $validate_skus = true ]; then
        checkSKUAvailability $location
        checkSKUQuotas $location
    fi
}

checkSKUAvailability() {
    # Function to validate that the required SKUs are not restricted for the given region
    local location=$1
    local sku_checklist
    local sku_check_result
    local sku_validation_listing

    sku_checklist=("standard_d4s_v5" "standard_d8s_v5" "standard_e8s_v5")
    printf "Checking cloud region for VM sku availability... "
    for sku in ${sku_checklist[@]}; do
        sku_check_result=$(
            az vm list-skus --location $location --size $sku --output json
        )
        sku_validation_listing=$(jq -r .[0].name <<< $sku_check_result)
        exitIfValueEmpty $sku_validation_listing "SKU $sku is restricted for location $location under the current subscription."
    done
    printf "Done.\n"
}

checkSKUQuotas() {
    local location=$1
    local vm_usage_report

    # Function to validation that the SKU quotas would not be exceeded during deployment
    printf "Checking Location for SKU Quota Usage... "
    vm_usage_report=$(
        az vm list-usage --location $location -o json
    )

    # Check quota for Standard DSv5 Family vCPUs
    local dsv5_usage_report dsv5_limit dsv5_currVal dsv5_reqVal
    dsv5_usage_report=$(jq -c '.[] | select(.localName | contains("Standard DSv5 Family vCPUs"))' <<< $vm_usage_report)
    dsv5_limit=$(jq -r .limit <<< $dsv5_usage_report)
    dsv5_currVal=$(jq -r .currentValue <<< $dsv5_usage_report)
    dsv5_reqVal=$(expr $dsv5_currVal + 12)
    exitIfThresholdExceeded $dsv5_reqVal $dsv5_limit "Not enough Standard DSv5 Family vCPU quota for deployment. At least 12 vCPU is required."

    # Check quota for Standard ESv5 Family vCPUs
    local esv5_usage_report esv5_limit esv5_currVal esv5_reqVal
    esv5_usage_report=$(jq -c '.[] | select(.localName | contains("Standard ESv5 Family vCPUs"))' <<< $vm_usage_report)
    esv5_limit=$(jq -r .limit <<< $esv5_usage_report)
    esv5_currVal=$(jq -r .currentValue <<< $esv5_usage_report)
    esv5_reqVal=$(expr $esv5_currVal + 8)
    exitIfThresholdExceeded $esv5_reqVal $esv5_limit "Not enough Standard ESv5 Family vCPU quota for deployment. At least 8 vCPU is required."
    printf "Done.\n"
}

installGraphRAGHelmChart () {
    local containerRegistryServer=""
    local graphragImageName graphragImageVersion
    local workloadId serviceAccountName appInsightsConnectionString aiSearchName cosmosEndpoint appHostname storageAccountBlobUrl
    local graphragApiBase graphragApiVersion graphragLlmModel graphragLlmModelDeployment graphragEmbeddingModel graphragEmbeddingModelDeployment

    echo "Deploying graphrag helm chart... "
    workloadId=$(jq -r .azure_workload_identity_client_id.value <<< $AZURE_DEPLOY_OUTPUTS)
    exitIfValueEmpty "$workloadId" "Unable to parse workload id from Azure outputs, exiting..."

    serviceAccountName=$(jq -r .azure_aks_service_account_name.value <<< $AZURE_DEPLOY_OUTPUTS)
    exitIfValueEmpty "$serviceAccountName" "Unable to parse service account name from Azure outputs, exiting..."

    appInsightsConnectionString=$(jq -r .azure_app_insights_connection_string.value <<< $AZURE_DEPLOY_OUTPUTS)
    exitIfValueEmpty "$appInsightsConnectionString" "Unable to parse app insights connection string from Azure outputs, exiting..."

    aiSearchName=$(jq -r .azure_ai_search_name.value <<< $AZURE_DEPLOY_OUTPUTS)
    exitIfValueEmpty "$aiSearchName" "Unable to parse AI search name from Azure outputs, exiting..."

    cosmosEndpoint=$(jq -r .azure_cosmosdb_endpoint.value <<< $AZURE_DEPLOY_OUTPUTS)
    exitIfValueEmpty "$cosmosEndpoint" "Unable to parse CosmosDB endpoint from Azure outputs, exiting..."

    appHostname=$(jq -r .azure_app_hostname.value <<< $AZURE_DEPLOY_OUTPUTS)
    exitIfValueEmpty "$appHostname" "Unable to parse graphrag hostname from deployment outputs, exiting..."

    storageAccountBlobUrl=$(jq -r .azure_storage_account_blob_url.value <<< $AZURE_DEPLOY_OUTPUTS)
    exitIfValueEmpty "$storageAccountBlobUrl" "Unable to parse storage account blob url from deployment outputs, exiting..."

    # retrieve container registry info either from the deployment or from user provided input
    if [ -n "$CONTAINER_REGISTRY_LOGIN_SERVER" ]; then
        containerRegistryServer="$CONTAINER_REGISTRY_LOGIN_SERVER"
    else
        containerRegistryServer=$(jq -r .azure_acr_login_server.value <<< $AZURE_DEPLOY_OUTPUTS)
    fi
    exitIfValueEmpty "$containerRegistryServer" "Unable to parse container registry url from deployment outputs, exiting..."
    graphragImageName=$(sed -rn "s/([^:]+).*/\1/p" <<< "$GRAPHRAG_IMAGE")
    graphragImageVersion=$(sed -rn "s/[^:]+:(.*)/\1/p" <<< "$GRAPHRAG_IMAGE")
    exitIfValueEmpty "$graphragImageName" "Unable to parse graphrag docker image name, exiting..."
    exitIfValueEmpty "$graphragImageVersion" "Unable to parse graphrag docker image version, exiting..."

    # retrieve AOAOI values either from the deployment or from user provided input
    if [ -n "$GRAPHRAG_API_BASE" ]; then
        graphragApiBase="$GRAPHRAG_API_BASE"
        graphragApiVersion="$GRAPHRAG_API_VERSION"
        graphragLlmModel="$GRAPHRAG_LLM_MODEL"
        graphragLlmModelDeployment="$GRAPHRAG_LLM_DEPLOYMENT_NAME"
        graphragEmbeddingModel="$GRAPHRAG_EMBEDDING_MODEL"
        graphragEmbeddingModelDeployment="$GRAPHRAG_EMBEDDING_DEPLOYMENT_NAME"
    else
        graphragApiBase=$(jq -r .azure_aoai_endpoint.value <<< $AZURE_DEPLOY_OUTPUTS)
        exitIfValueEmpty "$graphragApiBase" "Unable to parse AOAI endpoint from deployment outputs, exiting..."
        graphragApiVersion=$(jq -r .azure_aoai_llm_model_api_version.value <<< $AZURE_DEPLOY_OUTPUTS)
        exitIfValueEmpty "$graphragApiVersion" "Unable to parse AOAI model api version from deployment outputs, exiting..."
        graphragLlmModel=$(jq -r .azure_aoai_llm_model.value <<< $AZURE_DEPLOY_OUTPUTS)
        exitIfValueEmpty "$graphragLlmModel" "Unable to parse LLM model name from deployment outputs, exiting..."
        graphragLlmModelDeployment=$(jq -r .azure_aoai_llm_model_deployment_name.value <<< $AZURE_DEPLOY_OUTPUTS)
        exitIfValueEmpty "$graphragLlmModelDeployment" "Unable to parse LLM model deployment name from deployment outputs, exiting..."
        graphragEmbeddingModel=$(jq -r .azure_aoai_embedding_model.value <<< $AZURE_DEPLOY_OUTPUTS)
        exitIfValueEmpty "$graphragEmbeddingModel" "Unable to parse embedding model name from deployment outputs, exiting..."
        graphragEmbeddingModelDeployment=$(jq -r .azure_aoai_embedding_model_deployment_name.value <<< $AZURE_DEPLOY_OUTPUTS)
        exitIfValueEmpty "$graphragEmbeddingModelDeployment" "Unable to parse embedding model deployment name from deployment outputs, exiting..."
    fi

    reset_x=true
    if ! [ -o xtrace ]; then
        set -x
    else
        reset_x=false
    fi

    helm upgrade -i graphrag ./helm/graphrag -f ./helm/graphrag/values.yaml \
        --namespace $aksNamespace --create-namespace \
        --set "serviceAccount.name=$serviceAccountName" \
        --set "serviceAccount.annotations.azure\.workload\.identity/client-id=$workloadId" \
        --set "master.image.repository=$containerRegistryServer/$graphragImageName" \
        --set "master.image.tag=$graphragImageVersion" \
        --set "ingress.host=$appHostname" \
        --set "graphragConfig.AI_SEARCH_URL=https://$aiSearchName.$AISEARCH_ENDPOINT_SUFFIX" \
        --set "graphragConfig.AI_SEARCH_AUDIENCE=$AI_SEARCH_AUDIENCE" \
        --set "graphragConfig.APPLICATIONINSIGHTS_CONNECTION_STRING=$appInsightsConnectionString" \
        --set "graphragConfig.COGNITIVE_SERVICES_AUDIENCE=$COGNITIVE_SERVICES_AUDIENCE" \
        --set "graphragConfig.COSMOS_URI_ENDPOINT=$cosmosEndpoint" \
        --set "graphragConfig.GRAPHRAG_API_BASE=$graphragApiBase" \
        --set "graphragConfig.GRAPHRAG_API_VERSION=$graphragApiVersion" \
        --set "graphragConfig.GRAPHRAG_LLM_MODEL=$graphragLlmModel" \
        --set "graphragConfig.GRAPHRAG_LLM_DEPLOYMENT_NAME=$graphragLlmModelDeployment" \
        --set "graphragConfig.GRAPHRAG_EMBEDDING_MODEL=$graphragEmbeddingModel" \
        --set "graphragConfig.GRAPHRAG_EMBEDDING_DEPLOYMENT_NAME=$graphragEmbeddingModelDeployment" \
        --set "graphragConfig.STORAGE_ACCOUNT_BLOB_URL=$storageAccountBlobUrl"

    local helmResult
    helmResult=$?
    "$reset_x" && set +x
    exitIfCommandFailed $helmResult "Error deploying helm chart, exiting..."
}

waitForExternalIp () {
    local -i maxTries=14
    local available="false"
    local TMP_GRAPHRAG_SERVICE_IP

    printf "Checking for GraphRAG external IP"
    for ((i=0;i < $maxTries; i++)); do
        TMP_GRAPHRAG_SERVICE_IP=$(kubectl get ingress --namespace graphrag graphrag -o json | jq -r .status.loadBalancer.ingress[0].ip)
        # jq returns "null" if a value is not found
        if [[ "$TMP_GRAPHRAG_SERVICE_IP" != "null" ]]; then
            available="true"
            GRAPHRAG_SERVICE_IP=$TMP_GRAPHRAG_SERVICE_IP
            break
        fi
        sleep 10
        printf "."
    done
    if [ $available == "true" ]; then
        printf " Available.\n"
    else
        printf " Failed.\n"
    fi
}

waitForGraphragBackend () {
    local backendSwaggerUrl=$1
    local -i maxTries=20
    local available="false"

    printf "Checking for GraphRAG API availability..."
    for ((i=0;i < $maxTries; i++)); do
        az rest --method get --url $backendSwaggerUrl > /dev/null 2>&1
        if [ $? -eq 0 ]; then
            available="true"
            break
        fi
        sleep 20
        printf "."
    done
    if [ $available == "true" ]; then
        printf " Available.\n"
    else
        printf " Failed.\n"
        exitIfValueEmpty "" "GraphRAG API unavailable, exiting..."
    fi
}

deployDnsRecord () {
    waitForExternalIp
    exitIfValueEmpty "$GRAPHRAG_SERVICE_IP" "Unable to get GraphRAG external IP."

    local dnsZoneName
    dnsZoneName=$(jq -r .azure_dns_zone_name.value <<< $AZURE_DEPLOY_OUTPUTS)
    exitIfValueEmpty "$dnsZoneName" "Error parsing DNS zone name from azure outputs, exiting..."
    az deployment group create --only-show-errors --no-prompt \
        --name graphrag-dns-deployment \
        --resource-group $RESOURCE_GROUP \
        --template-file core/vnet/private-dns-zone-a-record.bicep \
        --parameters "name=graphrag" \
        --parameters "dnsZoneName=$dnsZoneName" \
        --parameters "ipv4Address=$GRAPHRAG_SERVICE_IP" > /dev/null
    exitIfCommandFailed $? "Error creating GraphRAG DNS record, exiting..."
}

deployGraphragAPI () {
    local apimGatewayUrl apimName backendSwaggerUrl graphragUrl

    echo "Registering GraphRAG API with APIM..."
    apimGatewayUrl=$(jq -r .azure_apim_gateway_url.value <<< $AZURE_DEPLOY_OUTPUTS)
    exitIfValueEmpty "$apimGatewayUrl" "Unable to parse APIM gateway url from Azure outputs, exiting..."
    apimName=$(jq -r .azure_apim_name.value <<< $AZURE_DEPLOY_OUTPUTS)
    exitIfValueEmpty "$apimName" "Error parsing apim name from azure outputs, exiting..."
    backendSwaggerUrl="$apimGatewayUrl/manpage/openapi.json"
    graphragUrl=$(jq -r .azure_app_url.value <<< $AZURE_DEPLOY_OUTPUTS)
    exitIfValueEmpty "$graphragUrl" "Error parsing GraphRAG URL from azure outputs, exiting..."

    waitForGraphragBackend $backendSwaggerUrl

    # download the openapi spec from the backend and load it into APIM
    az rest --only-show-errors --method get --url $backendSwaggerUrl -o json > core/apim/openapi.json
    exitIfCommandFailed $? "Error downloading graphrag openapi spec, exiting..."
    az deployment group create --only-show-errors --no-prompt \
        --name upload-graphrag-api-to-apim \
        --resource-group $RESOURCE_GROUP \
        --template-file core/apim/apim.graphrag-api.bicep \
        --parameters "backendUrl=$graphragUrl" \
        --parameters "name=GraphRAG" \
        --parameters "apiManagementName=$apimName" > /dev/null
    exitIfCommandFailed $? "Error registering graphrag API, exiting..."
    # cleanup
    #rm core/apim/openapi.json
}

grantDevAccessToAzureResources() {
    # This function is used to grant the deployer of this script "developer" access
    # to GraphRAG Azure resources by assigning the necessary RBAC roles for
    # Azure Storage, AI Search, and CosmosDB to the signed-in user. This grants
    # the deployer access to data in the storage account, cosmos db, and AI search services
    # from the Azure portal.
    echo "Granting deployer developer access to Azure resources..."

    # get subscription id of the active subscription
    local subscriptionId
    subscriptionId=$(az account show --output json | jq -r .id)
    exitIfValueEmpty $subscriptionId "Subscription ID not found"

    # get principal/object id of the signed in user
    local principalId
    principalId=$(az ad signed-in-user show --output json | jq -r .id)
    exitIfValueEmpty $principalId "Principal ID of deployer not found"

    # assign storage account roles
    local storageAccountName
    storageAccountName=$(az storage account list --resource-group $RESOURCE_GROUP --output json | jq -r .[0].name)
    exitIfValueEmpty $storageAccountName "Storage account not found"
    az role assignment create \
        --role "Storage Blob Data Contributor" \
        --assignee $principalId \
        --scope "/subscriptions/$subscriptionId/resourceGroups/$RESOURCE_GROUP/providers/Microsoft.Storage/storageAccounts/$storageAccountName" > /dev/null

    # assign cosmos db role
    local cosmosDbName
    cosmosDbName=$(az cosmosdb list --resource-group $RESOURCE_GROUP -o json | jq -r .[0].name)
    exitIfValueEmpty $cosmosDbName "CosmosDB account not found"
    az cosmosdb sql role assignment create \
        --account-name $cosmosDbName \
        --resource-group $RESOURCE_GROUP \
        --scope "/" \
        --principal-id $principalId \
        --role-definition-id /subscriptions/$subscriptionId/resourceGroups/$RESOURCE_GROUP/providers/Microsoft.DocumentDB/databaseAccounts/graphrag/sqlRoleDefinitions/00000000-0000-0000-0000-000000000002 > /dev/null

    # assign AI search roles
    local searchServiceName
    searchServiceName=$(az search service list --resource-group $RESOURCE_GROUP -o json | jq -r .[0].name)
    exitIfValueEmpty $searchServiceName "AI Search service not found"
    az role assignment create \
        --role "Contributor" \
        --assignee $principalId \
        --scope "/subscriptions/$subscriptionId/resourceGroups/$RESOURCE_GROUP/providers/Microsoft.Search/searchServices/$searchServiceName" > /dev/null
    az role assignment create \
        --role "Search Index Data Contributor" \
        --assignee $principalId \
        --scope "/subscriptions/$subscriptionId/resourceGroups/$RESOURCE_GROUP/providers/Microsoft.Search/searchServices/$searchServiceName" > /dev/null
    az role assignment create \
        --role "Search Index Data Reader" \
        --assignee $principalId \
        --scope "/subscriptions/$subscriptionId/resourceGroups/$RESOURCE_GROUP/providers/Microsoft.Search/searchServices/$searchServiceName" > /dev/null
}

deployDockerImageToInternalACR() {
    local containerRegistry
    containerRegistry=$(jq -r .azure_acr_login_server.value <<< $AZURE_DEPLOY_OUTPUTS)
    exitIfValueEmpty "$containerRegistry" "Unable to parse container registry from azure deployment outputs, exiting..."
    echo "Deploying docker image '${GRAPHRAG_IMAGE}' to container registry '${containerRegistry}'..."

    local scriptDir
    scriptDir="$( cd -- "$( dirname -- "${BASH_SOURCE[0]:-$0}"; )" &> /dev/null && pwd 2> /dev/null; )";
    az acr build --only-show-errors \
        --registry $containerRegistry \
        --file $scriptDir/../docker/Dockerfile-backend \
        --image $GRAPHRAG_IMAGE \
        $scriptDir/../
    exitIfCommandFailed $? "Error deploying docker image, exiting..."
}

################################################################################
# Help menu                                                                    #
################################################################################
usage() {
   echo
   echo "Usage: bash $0 [-h|d|g|s] -p <deploy.parameters.json>"
   echo "Description: Deployment script for the GraphRAG Solution Accelerator."
   echo "options:"
   echo "  -h     Print this help menu."
   echo "  -d     Disable private endpoint usage."
   echo "  -g     Developer mode. Grants deployer of this script access to Azure Storage, AI Search, and CosmosDB. Will disable private endpoints (-d) and enable debug mode."
   echo "  -s     Skip validation of SKU availability and quota for a faster deployment"
   echo "  -p     A JSON file containing the deployment parameters (deploy.parameters.json)."
   echo
}
# print usage if no arguments are supplied
[ $# -eq 0 ] && usage && exit 0
# parse arguments
ENABLE_PRIVATE_ENDPOINTS=true
VALIDATE_SKUS_FLAG=true
GRANT_DEV_ACCESS=0 # false
PARAMS_FILE=""
while getopts ":dgsp:h" option; do
    case "${option}" in
        d)
            ENABLE_PRIVATE_ENDPOINTS=false
            ;;
        g)
            ENABLE_PRIVATE_ENDPOINTS=false
            GRANT_DEV_ACCESS=1 # true
            ;;
        s)
            VALIDATE_SKUS_FLAG=false
            ;;
        p)
            PARAMS_FILE=${OPTARG}
            ;;
        h | *)
            usage
            exit 0
            ;;
    esac
done
shift $((OPTIND-1))
# check if required arguments are supplied
if [ ! -f $PARAMS_FILE ]; then
    echo "Error: invalid required argument."
    usage
    exit 1
fi
################################################################################
# Main Program                                                                 #
################################################################################
startBanner

checkRequiredTools
populateParams $PARAMS_FILE

# Check SKU availability and quotas
validateSKUs $LOCATION $VALIDATE_SKUS_FLAG

# Create resource group
createResourceGroupIfNotExists $LOCATION $RESOURCE_GROUP

# Deploy Azure resources
checkForApimSoftDelete
deployAzureResources

# Deploy graphrag docker image to internal ACR if an external ACR was not provided
if [ -z "$CONTAINER_REGISTRY_LOGIN_SERVER" ]; then
    deployDockerImageToInternalACR
fi

# Retrieve AKS credentials and install GraphRAG helm chart
getAksCredentials $RESOURCE_GROUP
installGraphRAGHelmChart

# Import and setup GraphRAG API in APIM
deployDnsRecord
deployGraphragAPI

if [ $GRANT_DEV_ACCESS -eq 1 ]; then
    grantDevAccessToAzureResources
fi

successBanner
