#!/bin/sh
# Public bootstrap. Release format and publishing: docs/bootstrap/README.md.
set -efu

usage() {
    printf '%s\n' "Usage: install.sh --deployment-id ID [--cloud aws|gcp] [options]

  --deployment-id ID          Deployment slug supplied by Granica (required).
  --cloud aws|gcp             Required when logged in to both clouds.
  -h, --help                  Show this help.
  --advanced                  Show all options, including advanced options.

Log in with the AWS CLI or gcloud before running. Bucket names are derived from
deployment settings; the latest release is selected automatically. No bucket
argument is needed."
    if [ "${1:-}" = all ]; then
        printf '\n%s\n' "Advanced options:
  --dev
      Use Granica's AWS dev hosting account. Has no effect on GCP and does not
      select a cloud.
  --granica-management-plane-gcp-project PROJECT_ID
      Override the Granica management-plane GCP project for a deployment using
      a nonstandard project name. Does not select a cloud."
    fi
}

error() { printf '\033[31mError: \033[0m%s\n' "$*" >&2; }
fail() { error "$*"; exit 1; }
usage_error() { error "$*"; printf '\n' >&2; usage >&2; exit 2; }
have() { command -v "$1" >/dev/null 2>&1; }

finish() {
    exit_status=$?
    trap - 0
    if [ -n "$work_dir" ] && ! rm -rf "$work_dir" 2>/dev/null; then
        error 'Could not remove temporary installer files.'
        [ "$exit_status" -ne 0 ] || exit_status=1
    fi
    if [ "$exit_status" -ne 0 ]; then
        printf '\n%s\n' 'If you need help resolving this issue, contact Granica support.' >&2
    fi
    exit "$exit_status"
}

digest() {
    if have "sha${1}sum"; then
        hash_output=$("sha${1}sum" < "$2") || return 1
    elif have shasum; then
        hash_output=$(shasum -a "$1" < "$2") || return 1
    elif have openssl; then
        hash_output=$(openssl dgst "-sha$1" -r < "$2") || return 1
    else
        return 1
    fi
    printf '%s\n' "${hash_output%% *}"
}

aws_get() {
    aws s3api get-object --bucket "$aws_bucket" --key "$1" \
        --expected-bucket-owner "$aws_owner" --cli-connect-timeout 10 \
        --cli-read-timeout 60 "$2" >/dev/null 2>"$work_dir/aws-error"
}

gcp_get() {
    gcloud storage cp --quiet "gs://$gcp_bucket/$1" "$2" \
        >/dev/null 2>"$work_dir/gcp-error"
}

probe_aws() {
    if ! have aws; then
        aws_status='AWS CLI is missing. Install it from https://aws.amazon.com/cli/.'
        return
    fi
    if ! aws_account=$(aws sts get-caller-identity --query Account --output text \
        --cli-connect-timeout 10 --cli-read-timeout 30 2>"$work_dir/aws-error"); then
        aws_status='Cannot verify AWS credentials. Log in (for SSO: aws sso login), check AWS_PROFILE and network access, then retry.'
        return
    fi
    case "$aws_account" in
        ''|*[!0-9]*) aws_status='AWS returned an invalid account ID. Check your CLI credentials and retry.'; return ;;
    esac
    if [ "${#aws_account}" -ne 12 ]; then
        aws_status='AWS returned an invalid account ID. Check your CLI credentials and retry.'
        return
    fi
    aws_ready=true
}

probe_gcp() {
    if ! have gcloud; then
        gcp_status='Google Cloud CLI is missing. Install it from https://cloud.google.com/sdk/docs/install.'
        return
    fi
    if ! gcloud auth print-access-token --quiet >/dev/null 2>"$work_dir/gcp-error"; then
        gcp_status='Cannot verify Google Cloud credentials. Run gcloud auth login (or configure your workload identity), check network access, then retry.'
        return
    fi
    gcp_ready=true
}

prepare_gcp() {
    gcp_project=$(gcloud config get-value project --quiet 2>"$work_dir/gcp-error") ||
        fail 'Cannot read the active Google Cloud project. Run gcloud config set project CUSTOMER_PROJECT_ID and retry.'
    case "$gcp_project" in
        ''|'(unset)'|*[!a-z0-9-]*)
            fail 'Select your customer Google Cloud project with gcloud config set project CUSTOMER_PROJECT_ID and retry.' ;;
    esac
    if [ "${#artifacts_project}" -gt 30 ]; then
        fail 'The derived GCP hosting project exceeds 30 characters. Supply --granica-management-plane-gcp-project with the hosting project ID from Granica.'
    fi
    printf '%s' "$artifacts_project" > "$work_dir/project"
    if ! project_hash=$(digest 1 "$work_dir/project" 2>/dev/null); then
        fail 'GCP bucket discovery needs SHA-1 support. Install sha1sum, shasum, or openssl and retry.'
    fi
    # Terraform uses substr(sha1(project_id), 0, 10); never hash the caller's project.
    project_hash=$(printf '%.10s' "$project_hash")
    gcp_bucket="granica-artifacts-$deployment_id-$project_hash"
}

valid_version() {
    case "$1" in v*) version_numbers=${1#v} ;; *) return 1 ;; esac
    # Exactly three numeric components, with no path, suffix, or shell syntax.
    old_ifs=$IFS
    IFS=.
    # shellcheck disable=SC2086
    set -- $version_numbers
    IFS=$old_ifs
    [ "$#" -eq 3 ] || return 1
    for component do
        case "$component" in ''|*[!0-9]*|0[0-9]*) return 1 ;; esac
    done
    [ "$version_numbers" = "$1.$2.$3" ]
}

latest_release() {
    if [ "$cloud" = aws ]; then
        # Preserve automatic pagination and emit one immediate child prefix per line.
        aws s3api list-objects-v2 --bucket "$aws_bucket" --prefix releases/ \
            --delimiter / --query 'CommonPrefixes[].[Prefix]' --output text \
            --expected-bucket-owner "$aws_owner" --cli-connect-timeout 10 \
            --cli-read-timeout 60 >"$work_dir/releases" 2>"$work_dir/aws-error" ||
            fail "Could not list $bucket_uri/releases/. You are using AWS account ID $aws_account and deployment ID $deployment_id. Are these the account and deployment IDs registered with Granica?"
        release_prefix=releases/
    else
        gcloud storage ls --quiet "$bucket_uri/releases/" \
            >"$work_dir/releases" 2>"$work_dir/gcp-error" ||
            fail "Could not list $bucket_uri/releases/. You are using GCP project ID $gcp_project and deployment ID $deployment_id. Are these the project and deployment IDs registered with Granica?"
        release_prefix="$bucket_uri/releases/"
    fi
    : > "$work_dir/versions"
    while IFS= read -r entry || [ -n "$entry" ]; do
        case "$entry" in
            "$release_prefix"*/)
                candidate=${entry#"$release_prefix"}
                candidate=${candidate%/}
                if valid_version "$candidate"; then
                    printf '%s\n' "${candidate#v}" >> "$work_dir/versions"
                fi ;;
        esac
    done < "$work_dir/releases"
    [ -s "$work_dir/versions" ] || fail "No release directories named vMAJOR.MINOR.PATCH were found in $bucket_uri/releases/."
    # POSIX numeric sort works on macOS too; lexical order puts v3.14.9 above v3.14.18.
    LC_ALL=C sort -t . -k1,1nr -k2,2nr -k3,3nr "$work_dir/versions" > "$work_dir/sorted" 2>/dev/null ||
        fail 'Could not sort the release versions. Check temporary storage permissions and free disk space.'
    IFS= read -r version_numbers < "$work_dir/sorted"
    version="v$version_numbers"
}

main() {
    deployment_id=
    cloud=
    dev=false
    artifacts_project=
    while [ "$#" -gt 0 ]; do
        case "$1" in
            --deployment-id|--cloud|--granica-management-plane-gcp-project)
                [ "$#" -ge 2 ] || usage_error "$1 needs a value."
                case "$2" in ''|--*) usage_error "$1 needs a value." ;; esac
                case "$1" in
                    --deployment-id) deployment_id=$2 ;;
                    --cloud) cloud=$2 ;;
                    --granica-management-plane-gcp-project) artifacts_project=$2 ;;
                esac
                shift 2 ;;
            --dev) dev=true; shift ;;
            -h|--help) usage; exit 0 ;;
            --advanced) usage all; exit 0 ;;
            *) usage_error "Unknown argument: $1" ;;
        esac
    done
    case "$deployment_id" in
        '') usage_error '--deployment-id is required.' ;;
        *[!a-z0-9-]*|-*|*-) usage_error '--deployment-id must contain lowercase letters, digits and internal hyphens only.' ;;
    esac
    [ "${#deployment_id}" -le 32 ] || usage_error '--deployment-id must be at most 32 characters to fit the artifacts bucket name.'
    case "$cloud" in ''|aws|gcp) ;; *) usage_error '--cloud must be aws or gcp.' ;; esac
    if [ -n "$artifacts_project" ]; then
        case "$artifacts_project" in
            [!a-z]*|*[!a-z0-9-]*|*-) usage_error '--granica-management-plane-gcp-project must be a valid GCP project ID.' ;;
        esac
        [ "${#artifacts_project}" -ge 6 ] && [ "${#artifacts_project}" -le 30 ] || usage_error '--granica-management-plane-gcp-project must be 6 to 30 characters.'
        [ "$cloud" != aws ] || usage_error '--granica-management-plane-gcp-project is only valid for GCP.'
    fi

    for dependency in cat mktemp rm sh sort; do
        have "$dependency" || fail "Required command '$dependency' is missing. Install it using your operating system's package manager and retry."
    done
    if ! have sha256sum && ! have shasum && ! have openssl; then
        fail 'Installer verification needs SHA-256 support. Install sha256sum, shasum, or openssl and retry.'
    fi
    umask 077
    work_dir=$(mktemp -d "${TMPDIR:-/tmp}/granica-install.XXXXXXXX" 2>/dev/null) || fail 'Cannot create a temporary directory. Check TMPDIR permissions and free disk space.'
    trap 'exit 129' HUP
    trap 'exit 130' INT
    trap 'exit 143' TERM
    AWS_PAGER='' AWS_CLI_AUTO_PROMPT=off CLOUDSDK_CORE_DISABLE_PROMPTS=1
    export AWS_PAGER AWS_CLI_AUTO_PROMPT CLOUDSDK_CORE_DISABLE_PROMPTS
    aws_owner=697908643012
    [ "$dev" = false ] || aws_owner=401252763139
    aws_ready=false
    gcp_ready=false
    aws_status=
    gcp_status=
    printf 'Checking cloud credentials for %s...\n' "$deployment_id" >&2
    [ "$cloud" = gcp ] || probe_aws
    [ "$cloud" = aws ] || probe_gcp
    if [ "$aws_ready" = true ] && [ "$gcp_ready" = true ]; then
        fail 'You are logged in to both AWS and GCP. Rerun with --cloud aws or --cloud gcp.'
    elif [ "$aws_ready" = true ]; then
        cloud=aws
        [ -z "$artifacts_project" ] || usage_error '--granica-management-plane-gcp-project is only valid for GCP.'
        aws_bucket="granica-artifacts-$deployment_id-$aws_owner"
        bucket_uri="s3://$aws_bucket"
        target_id=$aws_account
    elif [ "$gcp_ready" = true ]; then
        cloud=gcp
        artifacts_project=${artifacts_project:-cx-ctrl-plane-$deployment_id}
        prepare_gcp
        bucket_uri="gs://$gcp_bucket"
        target_id=$gcp_project
    else
        [ -z "$aws_status" ] || error "$aws_status"
        [ -z "$gcp_status" ] || error "$gcp_status"
        fail 'No authenticated cloud CLI was found. Fix the issue above and rerun; nothing was installed.'
    fi

    printf 'Finding the latest release in %s/releases/...\n' "$bucket_uri" >&2
    latest_release
    printf 'Downloading %s from %s (customer %s: %s).\n' "$version" "$bucket_uri" "$cloud" "$target_id" >&2
    if ! "${cloud}_get" "releases/$version/install.sh" "$work_dir/install.sh"; then
        fail "The installer at $bucket_uri/releases/$version/install.sh is unavailable."
    fi
    [ -s "$work_dir/install.sh" ] || fail 'The release installer is empty.'
    if ! "${cloud}_get" "releases/$version/install.sh.sha256" "$work_dir/install.sh.sha256"; then
        fail "The installer checksum at $bucket_uri/releases/$version/install.sh.sha256 is unavailable."
    fi
    checksum=$(cat "$work_dir/install.sh.sha256" 2>/dev/null) || fail 'Could not read the downloaded installer checksum.'
    # Accept a bare digest or the standard sha256sum/shasum output for install.sh.
    # shellcheck disable=SC2086
    set -- $checksum
    case "$#" in
        1) ;;
        2) case "$2" in install.sh|'*install.sh') ;; *) fail 'Invalid installer checksum filename.' ;; esac ;;
        *) fail 'Invalid installer checksum file.' ;;
    esac
    expected_hash=$1
    case "$expected_hash" in ''|*[!0-9a-f]*) fail 'Invalid release SHA-256.' ;; esac
    [ "${#expected_hash}" -eq 64 ] || fail 'Invalid release SHA-256 length.'
    actual_hash=$(digest 256 "$work_dir/install.sh" 2>/dev/null) || fail 'Could not calculate the installer SHA-256. Check your checksum tool.'
    [ "$actual_hash" = "$expected_hash" ] || fail 'Installer checksum mismatch. Nothing was executed. Retry the installation.'
    sh -n "$work_dir/install.sh" 2>/dev/null || fail 'The release installer is not valid shell code.'
    printf 'Starting verified installer %s.\n' "$version" >&2
    if sh "$work_dir/install.sh" --deployment-id "$deployment_id" --cloud "$cloud" \
        --artifacts-bucket "$bucket_uri" --release-version "$version" \
        --customer-cloud-id "$target_id"; then
        return 0
    else
        installer_status=$?
        error "The release installer exited with status $installer_status."
        return "$installer_status"
    fi
}

work_dir=
trap finish 0
main "$@"
