#!/usr/bin/env bash
set -e

name=${RELEASE:-${1:-participant-portal}}
ns=${NAMESPACE:-${2:-rtgsglobal}}
CLUSTER_NAME=${CLUSTER_NAME:-${3:-}}
AWS_REGION=${AWS_REGION:-eu-west-2}
NLB_HOST=""

# Domains and Let's Encrypt are configured in the values files, not here:
#   - app.ingress.host / notifications.ingress.host (blank -> autoHostname below)
#   - letsencrypt.{enabled,createIssuer,email,staging}
# The only Let's Encrypt decision this script acts on is whether to install a
# cert-manager instance, which is needed when the chart creates its own
# ClusterIssuer (letsencrypt.createIssuer: true). See needs_cert_manager.

function set_eks_context {
    if [[ -z "$CLUSTER_NAME" ]]; then
        echo "Error: set CLUSTER_NAME env var or pass as 3rd argument"
        exit 1
    fi
    echo "Updating kubeconfig for EKS cluster \"$CLUSTER_NAME\" in $AWS_REGION..."
    aws eks update-kubeconfig --name "$CLUSTER_NAME" --region "$AWS_REGION"
}

function set_ns {
    local ns=$1
    local ns_exists
    ns_exists=$(kubectl get namespace "$ns" --ignore-not-found)

    if [[ -z "$ns_exists" ]]; then
        echo "Creating namespace \"$ns\"..."
        kubectl create namespace "$ns"
    fi

    local current_ns
    current_ns=$(kubectl config view --minify | grep namespace: | awk '{print $2}')

    if [[ "$current_ns" != "$ns" ]]; then
        echo "Setting namespace preference to \"$ns\""
        kubectl config set-context --current --namespace="$ns"
    fi
    :
}

function install_nginx {
    if helm status ingress-nginx --namespace ingress-nginx >/dev/null 2>&1; then
        echo "Skipping - nginx ingress is already installed."
    else
        echo "Installing ingress-nginx with AWS NLB backend..."
        helm repo add nginx https://kubernetes.github.io/ingress-nginx
        helm repo update
        helm upgrade --wait --install --dependency-update \
            --namespace ingress-nginx \
            --create-namespace \
            --set controller.service.type=LoadBalancer \
            --set "controller.service.annotations.service\.beta\.kubernetes\.io/aws-load-balancer-type=nlb" \
            ingress-nginx nginx/ingress-nginx
        kubectl rollout status deployment/ingress-nginx-controller -n ingress-nginx --timeout=120s
        echo
    fi
}

function install_metrics_server {
    if kubectl get deployment metrics-server -n kube-system &>/dev/null; then
        echo "Skipping - metrics server is already installed."
    else
        echo "Installing metrics-server..."
        kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml
        kubectl rollout status deployment/metrics-server -n kube-system --timeout=120s
        echo
    fi
}

function resolve_nlb_host {
    echo "Waiting for NLB hostname to be assigned..."
    local attempts=0
    local max_attempts=30
    while [[ -z "$NLB_HOST" && $attempts -lt $max_attempts ]]; do
        NLB_HOST=$(kubectl get svc ingress-nginx-controller -n ingress-nginx \
            -o jsonpath='{.status.loadBalancer.ingress[0].hostname}' 2>/dev/null || true)
        if [[ -z "$NLB_HOST" ]]; then
            attempts=$((attempts + 1))
            echo "  No hostname yet... ($attempts/$max_attempts)"
            sleep 10
        fi
    done

    if [[ -z "$NLB_HOST" ]]; then
        echo "Error: timed out waiting for NLB hostname to be assigned"
        exit 1
    fi

    echo "Resolved NLB hostname: $NLB_HOST"
    echo
}

function ensure_oidc_provider {
    local oidc_url
    oidc_url=$(aws eks describe-cluster \
        --name "$CLUSTER_NAME" \
        --region "$AWS_REGION" \
        --query "cluster.identity.oidc.issuer" \
        --output text)
    local account_id
    account_id=$(aws sts get-caller-identity --query Account --output text)
    local oidc_id
    oidc_id=$(echo "$oidc_url" | sed 's|https://||')
    local oidc_arn="arn:aws:iam::${account_id}:oidc-provider/${oidc_id}"

    if aws iam get-open-id-connect-provider --open-id-connect-provider-arn "$oidc_arn" &>/dev/null; then
        echo "Skipping - OIDC provider already registered."
        return
    fi

    echo "Registering OIDC provider with IAM..."
    local oidc_host
    oidc_host=$(echo "$oidc_id" | cut -d/ -f1)
    local thumbprint
    thumbprint=$(echo | openssl s_client -servername "$oidc_host" -connect "${oidc_host}:443" 2>/dev/null \
        | openssl x509 -fingerprint -sha1 -noout \
        | awk -F'=' '{gsub(/:|\r/, "", $NF); print tolower($NF)}')
    aws iam create-open-id-connect-provider \
        --url "$oidc_url" \
        --client-id-list sts.amazonaws.com \
        --thumbprint-list "$thumbprint"
    echo
}

EBS_CSI_ROLE_ARN=""

# Create or repoint the EBS CSI driver IAM role + trust policy for the CURRENT
# cluster. The role is account-global but its trust policy is bound to a single
# cluster's OIDC issuer, so this must run on every invocation - otherwise a
# recreated cluster keeps a stale trust policy and the controller gets a 403 on
# AssumeRoleWithWebIdentity (PVCs never provision, CNPG cluster hangs InProgress).
function reconcile_ebs_csi_iam {
    local role_name="AmazonEKS_EBS_CSI_DriverRole"
    local account_id
    account_id=$(aws sts get-caller-identity --query Account --output text)
    EBS_CSI_ROLE_ARN="arn:aws:iam::${account_id}:role/${role_name}"

    local oidc_issuer
    oidc_issuer=$(aws eks describe-cluster \
        --name "$CLUSTER_NAME" \
        --region "$AWS_REGION" \
        --query "cluster.identity.oidc.issuer" \
        --output text | sed 's|https://||')
    local trust_policy
    trust_policy=$(printf '{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": { "Federated": "arn:aws:iam::%s:oidc-provider/%s" },
    "Action": "sts:AssumeRoleWithWebIdentity",
    "Condition": {
      "StringEquals": {
        "%s:aud": "sts.amazonaws.com",
        "%s:sub": "system:serviceaccount:kube-system:ebs-csi-controller-sa"
      }
    }
  }]
}' "$account_id" "$oidc_issuer" "$oidc_issuer" "$oidc_issuer")

    if ! aws iam get-role --role-name "$role_name" &>/dev/null; then
        echo "  Creating IAM role for EBS CSI driver..."
        aws iam create-role \
            --role-name "$role_name" \
            --assume-role-policy-document "$trust_policy"
        aws iam attach-role-policy \
            --role-name "$role_name" \
            --policy-arn arn:aws:iam::aws:policy/service-role/AmazonEBSCSIDriverPolicy
    else
        echo "  Reconciling IAM role trust policy for this cluster..."
        aws iam update-assume-role-policy \
            --role-name "$role_name" \
            --policy-document "$trust_policy"
    fi
}

function install_ebs_csi_driver {
    echo "Reconciling AWS EBS CSI driver IAM for cluster \"$CLUSTER_NAME\"..."
    reconcile_ebs_csi_iam

    if kubectl get daemonset ebs-csi-node -n kube-system &>/dev/null; then
        echo "EBS CSI driver already installed - ensuring addon role and restarting controller to pick up trust policy..."
        aws eks update-addon \
            --cluster-name "$CLUSTER_NAME" \
            --addon-name aws-ebs-csi-driver \
            --region "$AWS_REGION" \
            --service-account-role-arn "$EBS_CSI_ROLE_ARN" >/dev/null 2>&1 || true
        kubectl rollout restart deployment/ebs-csi-controller -n kube-system
        kubectl rollout status deployment/ebs-csi-controller -n kube-system --timeout=120s
        echo
        return
    fi

    echo "Installing AWS EBS CSI driver addon..."
    aws eks create-addon \
        --cluster-name "$CLUSTER_NAME" \
        --addon-name aws-ebs-csi-driver \
        --region "$AWS_REGION" \
        --service-account-role-arn "$EBS_CSI_ROLE_ARN"

    echo "Waiting for EBS CSI addon to become active..."
    aws eks wait addon-active \
        --cluster-name "$CLUSTER_NAME" \
        --addon-name aws-ebs-csi-driver \
        --region "$AWS_REGION"

    echo "Waiting for EBS CSI driver to be ready..."
    kubectl rollout status daemonset/ebs-csi-node -n kube-system --timeout=120s
    echo
}

function install_cnpg {
    if helm status cnpg --namespace cnpg-system >/dev/null 2>&1; then
        echo "Skipping - cloud native postgres is already installed."
    else
        echo "Installing cloud-native-pg..."
        helm repo add cnpg https://cloudnative-pg.github.io/charts
        helm repo update
        helm upgrade --wait --install --dependency-update \
            --namespace cnpg-system \
            --create-namespace \
            cnpg cnpg/cloudnative-pg
        kubectl rollout status deployment/cnpg-cloudnative-pg -n cnpg-system --timeout=120s
        echo
    fi
}

# True when values-aws.yaml opts into this chart creating/owning the ClusterIssuer
# (letsencrypt.createIssuer: true). That path needs a cert-manager instance present
# before the chart applies, so the script installs one. When false (the shared
# issuer from the signing module), cert-manager already exists and we leave it be.
function needs_cert_manager {
    grep -Eq '^[[:space:]]*createIssuer:[[:space:]]*true([[:space:]]|#|$)' ./values-aws.yaml
}

# Install cert-manager so the chart can create its own Let's Encrypt ClusterIssuer.
# Only runs when needs_cert_manager is true; skips if cert-manager already exists.
function install_cert_manager {
    if helm status cert-manager --namespace cert-manager >/dev/null 2>&1; then
        echo "Skipping - cert-manager is already installed."
    else
        echo "Installing cert-manager..."
        helm repo add jetstack https://charts.jetstack.io
        helm repo update
        helm upgrade --wait --install --dependency-update \
            --namespace cert-manager \
            --create-namespace \
            --set crds.enabled=true \
            cert-manager jetstack/cert-manager
        kubectl rollout status deployment/cert-manager -n cert-manager --timeout=120s
        kubectl rollout status deployment/cert-manager-webhook -n cert-manager --timeout=120s
        echo
    fi
}

# Find the Route 53 hosted zone that owns a host by walking up its labels
# (foo.bar.example.com -> bar.example.com -> example.com). Echoes the bare zone
# id, or nothing if no hosted zone in this account covers the name.
function route53_zone_for_host {
    local candidate="$1"
    local zone_id=""
    while [[ "$candidate" == *.* ]]; do
        zone_id=$(aws route53 list-hosted-zones-by-name --dns-name "$candidate" \
            --query "HostedZones[?Name=='${candidate}.'].Id | [0]" --output text 2>/dev/null || true)
        if [[ -n "$zone_id" && "$zone_id" != "None" ]]; then
            echo "${zone_id#/hostedzone/}"
            return 0
        fi
        candidate="${candidate#*.}"
    done
}

# Point a single host at the NLB via a Route 53 alias A record (matching the
# convention used elsewhere in the zone; an alias A also can't be issued as a
# CNAME alongside existing A records). Idempotent: skips when the record already
# aliases the NLB, otherwise UPSERTs (itself safe to repeat) and waits for the
# change to propagate so cert-manager's HTTP-01 self-check resolves first time.
function upsert_route53_alias {
    local host=$1
    local nlb_zone=$2

    local zone_id
    zone_id=$(route53_zone_for_host "$host")
    if [[ -z "$zone_id" ]]; then
        echo "Route 53: no hosted zone found for ${host} - create the record manually."
        return 0
    fi

    local current
    current=$(aws route53 list-resource-record-sets --hosted-zone-id "$zone_id" \
        --query "ResourceRecordSets[?Name=='${host}.' && Type=='A'].AliasTarget.DNSName | [0]" \
        --output text 2>/dev/null || true)
    if [[ "$current" == "${NLB_HOST}" || "$current" == "${NLB_HOST}." ]]; then
        echo "Route 53: ${host} already aliased to the NLB - skipping."
        return 0
    fi

    echo "Route 53: pointing ${host} at the NLB (zone ${zone_id})..."
    local change_id
    change_id=$(aws route53 change-resource-record-sets --hosted-zone-id "$zone_id" \
        --change-batch "{\"Changes\":[{\"Action\":\"UPSERT\",\"ResourceRecordSet\":{\"Name\":\"${host}\",\"Type\":\"A\",\"AliasTarget\":{\"HostedZoneId\":\"${nlb_zone}\",\"DNSName\":\"${NLB_HOST}\",\"EvaluateTargetHealth\":false}}}]}" \
        --query 'ChangeInfo.Id' --output text)
    echo "Route 53: waiting for ${host} to propagate..."
    aws route53 wait resource-record-sets-changed --id "$change_id"
}

# Ensure every chart ingress host that isn't the NLB fallback resolves to the NLB
# in Route 53. Runs before the chart is applied so DNS is live when cert-manager
# first validates the HTTP-01 challenge (otherwise CoreDNS caches the NXDOMAIN and
# the cert stays stuck). Hosts are read from a dry render of the same values the
# deploy uses, so this stays in sync with the values files automatically.
function ensure_route53_records {
    local nlb_zone
    nlb_zone=$(aws elbv2 describe-load-balancers --region "$AWS_REGION" \
        --query "LoadBalancers[?DNSName=='${NLB_HOST}'].CanonicalHostedZoneId | [0]" \
        --output text 2>/dev/null || true)
        
    # Fallback for a Classic ELB (v1): elbv2 above only returns ALB/NLB, so an
    # nginx LoadBalancer provisioned as a classic ELB (e.g. by the signing module)
    # is invisible to it. Look it up via the classic API before giving up.
    if [[ -z "$nlb_zone" || "$nlb_zone" == "None" ]]; then
        nlb_zone=$(aws elb describe-load-balancers --region "$AWS_REGION" \
            --query "LoadBalancerDescriptions[?DNSName=='${NLB_HOST}'].CanonicalHostedZoneNameID | [0]" \
            --output text 2>/dev/null || true)
    fi

    if [[ -z "$nlb_zone" || "$nlb_zone" == "None" ]]; then
        echo "Warning: could not resolve the load balancer's canonical hosted zone id; skipping Route 53 setup."
        return 0
    fi

    local hosts
    hosts=$(helm template "$name" . \
        --values ./values.yaml --values ./values-aws.yaml \
        --set "autoHostname=${NLB_HOST}" 2>/dev/null \
        | awk '/^[[:space:]]*-[[:space:]]+host:[[:space:]]/ {print $3}' | sort -u)

    while IFS= read -r host; do
        [[ -z "$host" || "$host" == "$NLB_HOST" ]] && continue
        upsert_route53_alias "$host" "$nlb_zone"
    done <<< "$hosts"
}

function helm_install {
    local name=$1
    local ns=$2

    # Domains and Let's Encrypt come from the values files. autoHostname is the
    # chart-wide fallback used for any ingress host left blank, so the deploy still
    # works with no domain configured (served on the raw NLB hostname, no cert).
    local -a args=(
        --set "autoHostname=${NLB_HOST}"
    )

    echo "Deploying ./Chart.yaml as \"$name\" into \"$ns\" (NLB fallback: $NLB_HOST)..."
    helm upgrade --wait --install --dependency-update \
        --namespace "$ns" \
        --values "./values.yaml" \
        --values "./values-aws.yaml" \
        "${args[@]}" \
        "$name" \
        "." | awk '/^NOTES:/{found=1} found{print}'
    [[ ${PIPESTATUS[0]} -eq 0 ]]

    echo
    echo "NLB hostname: $NLB_HOST"
}

set_eks_context
pushd "$(dirname "${BASH_SOURCE[0]}")/.."
install_nginx
resolve_nlb_host
install_metrics_server
ensure_oidc_provider
install_ebs_csi_driver
install_cnpg
if needs_cert_manager; then
    install_cert_manager
fi
set_ns "$ns"
ensure_route53_records
helm_install "$name" "$ns"
popd
