#!/usr/bin/env bash
# Suger Azure Marketplace — Azure-side one-shot setup script.
#
# In the customer's Azure tenant, this script configures the Entra app + permissions + secret in one run:
#   1) Create (or reuse) a multitenant Entra application with Suger's Web redirect URI + implicit grant enabled
#   2) Pre-create service principals for the Marketplace SaaS API + 3 Partner APIs
#   3) Grant the app `user_impersonation` on the 3 Partner APIs (needed for co-sell)
#      (Marketplace SaaS API does not require a declarative grant — the token endpoint reads `resource=`)
#   4) Grant admin consent
#   5) Generate a client secret (default 365 days)
#   6) Self-verify by exchanging the secret for a real Marketplace API token
# At the end, the Tenant ID / App ID / Client Secret are printed and written to ./suger-azure-credentials.txt (chmod 600).
#
# Prerequisites (auto-detected — non-technical users do not need to install anything in advance):
#   - Azure CLI (az) + jq + curl: if missing, the script prints OS-specific install commands
#     (Azure Cloud Shell is the simplest option — all three are pre-installed)
#   - Azure login: if not signed in, the script will run `az login` (opens a browser)
#   - The executor must be a Global Administrator in the target tenant (required by Suger)
#
# Usage:
#   # First-time setup
#   ./setup-azure-app.sh
#
#   # Rotate the client secret (run before the 365-day expiry; appends a new secret,
#   # the old one stays valid until its natural expiry — no downtime)
#   REUSE_EXISTING=true ./setup-azure-app.sh
#
#   # —— Advanced options below; 99% of customers do not need these ——
#   SECRET_DAYS=90 ./setup-azure-app.sh              # Customize secret lifetime (default 365 days)
#   APP_NAME="Suger Custom Name" ./setup-azure-app.sh # Customize the Entra app display name
#   SKIP_VERIFY=true ./setup-azure-app.sh            # Skip token self-verification (offline/restricted networks)
#   CLEANUP=true ./setup-azure-app.sh                # ⚠️ Dangerous: deletes same-named apps and recreates. Changes the App ID, breaking any existing offer Technical Configuration. Testing only

# Note: -u has quirks with heredoc / command substitution on macOS bash 5.2,
# so it's intentionally not enabled. We keep -e (exit on error) and pipefail.
set -eo pipefail

APP_NAME="${APP_NAME:-Suger Marketplace Integration}"
SECRET_DAYS="${SECRET_DAYS:-365}"
REUSE_EXISTING="${REUSE_EXISTING:-false}"
CLEANUP="${CLEANUP:-false}"
SKIP_VERIFY="${SKIP_VERIFY:-false}"
OUTPUT_FILE="${OUTPUT_FILE:-./suger-azure-credentials.txt}"

# Well-known Microsoft resource IDs in the public directory
MARKETPLACE_SAAS_RESOURCE_ID="20e940b3-4c77-4b0b-9a53-9e16a1b010a7"   # Marketplace SaaS Fulfillment API (client_credentials)
MICROSOFT_PARTNER_RESOURCE_ID="4990cffe-04e8-4e8b-808a-1175604b879f"  # MicrosoftPartner (legacy partner API)
PARTNER_CENTER_RESOURCE_ID_1="fa3d9a0c-3fb0-42cc-9193-47c7ecd2edbd"   # Microsoft Partner Center #1
PARTNER_CENTER_RESOURCE_ID_2="fabfbdc4-5751-471c-ac43-3826fa1afc31"   # Microsoft Partner Center #2

SUGER_REDIRECT_URI="https://api.suger.cloud/public/integration/azure/authCode"

# 3 Partner APIs that need user_impersonation
PARTNER_RESOURCE_IDS=(
  "$MICROSOFT_PARTNER_RESOURCE_ID"
  "$PARTNER_CENTER_RESOURCE_ID_1"
  "$PARTNER_CENTER_RESOURCE_ID_2"
)
# All resources that need an SP pre-created (includes Marketplace SaaS)
ALL_RESOURCE_IDS=(
  "$MARKETPLACE_SAAS_RESOURCE_ID"
  "${PARTNER_RESOURCE_IDS[@]}"
)

# OS detection (used for OS-specific install instructions)
detect_os() {
  case "$(uname -s)" in
    Darwin*) echo "macos" ;;
    Linux*)
      if grep -qiE 'ubuntu|debian' /etc/os-release 2>/dev/null; then echo "debian"
      elif grep -qiE 'rhel|centos|fedora|rocky|alma' /etc/os-release 2>/dev/null; then echo "rhel"
      else echo "linux"; fi
      ;;
    MINGW*|CYGWIN*|MSYS*) echo "windows" ;;
    *) echo "unknown" ;;
  esac
}

# Friendly prompt when tools are missing: recommend Cloud Shell first, then local install
missing_tools=()
command -v az   >/dev/null 2>&1 || missing_tools+=("az")
command -v jq   >/dev/null 2>&1 || missing_tools+=("jq")
command -v curl >/dev/null 2>&1 || missing_tools+=("curl")
if (( ${#missing_tools[@]} > 0 )); then
  echo "❌ Missing tools: ${missing_tools[*]}"
  echo
  echo "📦 Easiest option: use Azure Cloud Shell (browser-based, no install, auto-signed-in)"
  echo "   → Open https://shell.azure.com, choose Bash, upload this script and run it"
  echo
  echo "To install locally:"
  case "$(detect_os)" in
    macos)
      echo "   brew install azure-cli jq    # curl is built into macOS"
      ;;
    debian)
      echo "   curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash"
      echo "   sudo apt-get install -y jq curl"
      ;;
    rhel)
      echo "   sudo dnf install -y azure-cli jq curl"
      ;;
    windows)
      echo "   Recommended: install WSL (Windows Subsystem for Linux) first, then follow the debian instructions"
      echo "   Or install Azure CLI for Windows directly: https://aka.ms/installazurecliwindows"
      ;;
    *)
      echo "   Azure CLI install docs: https://learn.microsoft.com/cli/azure/install-azure-cli"
      ;;
  esac
  exit 1
fi

# Filter stderr noise (WARNING lines) but keep real errors visible.
run_az() {
  local out rc tmperr
  tmperr=$(mktemp)
  if out=$(az "$@" 2>"$tmperr"); then
    grep -v "^WARNING" "$tmperr" >&2 || true
    rm -f "$tmperr"
    printf '%s' "$out"
    return 0
  else
    rc=$?
    cat "$tmperr" >&2
    rm -f "$tmperr"
    return $rc
  fi
}

# Wait for an SP to become visible in the directory (up to ~30s).
wait_for_sp() {
  local id="$1" label="$2" i
  for i in $(seq 1 15); do
    if az ad sp show --id "$id" >/dev/null 2>&1; then return 0; fi
    sleep 2
  done
  echo "❌ Timed out waiting for $label service principal (id=$id)"
  return 1
}

echo "==> Current Azure context"
if ! az account show >/dev/null 2>&1; then
  echo "⚠️  Not signed in to Azure — invoking az login..."
  echo "    (Opens a browser. For headless/remote shells, run manually:"
  echo "       az login --use-device-code --tenant <your-tenant-id>)"
  if ! az login; then
    echo "❌ Login failed or cancelled"
    exit 1
  fi
fi
az account show --query "{tenantId: tenantId, subscription: name, user: user.name}" -o table
TENANT_ID=$(az account show --query tenantId -o tsv)
read -r -p "Create/update the Suger integration app in this tenant? [y/N] " ans
[[ "$ans" =~ ^[Yy]$ ]] || { echo "Cancelled"; exit 1; }

echo
echo "==> 1/6 Register Entra application (multitenant + Suger redirect URI + implicit grant): $APP_NAME"

# CLEANUP=true: delete all same-named apps (including historical failures), then go through the fresh-create path
if [[ "$CLEANUP" =~ ^([Tt]rue|1)$ ]]; then
  EXISTING_IDS="$(az ad app list --display-name "$APP_NAME" --query "[].appId" -o tsv 2>/dev/null || true)"
  if [[ -n "$EXISTING_IDS" ]]; then
    echo "    CLEANUP=true → deleting same-named apps:"
    for id in $EXISTING_IDS; do
      run_az ad app delete --id "$id"
      echo "      deleted $id"
    done
  else
    echo "    CLEANUP=true → no same-named apps to delete"
  fi
fi

EXISTING_APP_ID="$(az ad app list --display-name "$APP_NAME" --query "[0].appId" -o tsv 2>/dev/null || true)"
EXISTING_APP_ID="${EXISTING_APP_ID:-}"
if [[ -n "$EXISTING_APP_ID" ]]; then
  if [[ "${REUSE_EXISTING:-false}" =~ ^([Tt]rue|1)$ ]]; then
    APP_ID="$EXISTING_APP_ID"
    echo "    Exists → reusing APP_ID=$APP_ID"
    # Align existing app settings with the official requirements
    run_az ad app update --id "$APP_ID" \
      --sign-in-audience AzureADMultipleOrgs \
      --web-redirect-uris "$SUGER_REDIRECT_URI" \
      --enable-id-token-issuance true \
      --enable-access-token-issuance true >/dev/null
    echo "    Aligned multitenant + redirect URI + implicit grant"
  else
    echo "❌ An app with the same name already exists (appId=${EXISTING_APP_ID})."
    echo "   To re-run, set REUSE_EXISTING=true (keeps the app, re-applies permissions/consent + appends a new secret);"
    echo "   or change APP_NAME to use a different name."
    exit 1
  fi
else
  APP_ID=$(run_az ad app create \
    --display-name "$APP_NAME" \
    --sign-in-audience AzureADMultipleOrgs \
    --web-redirect-uris "$SUGER_REDIRECT_URI" \
    --enable-id-token-issuance true \
    --enable-access-token-issuance true \
    --is-fallback-public-client false \
    --query appId -o tsv)
  run_az ad sp create --id "$APP_ID" >/dev/null
  echo "    Created APP_ID=$APP_ID"
fi

wait_for_sp "$APP_ID" "this app"

echo
echo "==> 2/6 Pre-create service principals for 4 Microsoft APIs"
for RESOURCE_ID in "${ALL_RESOURCE_IDS[@]}"; do
  if az ad sp show --id "$RESOURCE_ID" >/dev/null 2>&1; then
    echo "    $RESOURCE_ID already exists"
  else
    run_az ad sp create --id "$RESOURCE_ID" >/dev/null
    echo "    $RESOURCE_ID created"
  fi
  wait_for_sp "$RESOURCE_ID" "$RESOURCE_ID"
done

echo
echo "==> 3/6 Add API permissions"
# Microsoft Graph User.Read: the portal adds this by default when creating an app via the UI,
# but the CLI does not. Add it explicitly to match the portal-created standard.
GRAPH_RESOURCE_ID="00000003-0000-0000-c000-000000000000"
GRAPH_USER_READ_SCOPE_ID="e1fe6dd8-ba31-4d61-89e7-88639da4683d"
run_az ad app permission add \
  --id "$APP_ID" \
  --api "$GRAPH_RESOURCE_ID" \
  --api-permissions "${GRAPH_USER_READ_SCOPE_ID}=Scope" >/dev/null
echo "    Microsoft Graph User.Read ✓"

# Marketplace SaaS: the SP is in the tenant; client_credentials flow does not require a declarative grant — skip
echo "    Marketplace SaaS: only requires SP in tenant; token endpoint reads resource=, no declarative grant needed"

# 3 Partner APIs: add user_impersonation (delegated, required for co-sell flows)
for RESOURCE_ID in "${PARTNER_RESOURCE_IDS[@]}"; do
  SCOPE_ID=$(az ad sp show --id "$RESOURCE_ID" \
    --query "oauth2PermissionScopes[?value=='user_impersonation'].id | [0]" -o tsv 2>/dev/null || true)
  if [[ -n "$SCOPE_ID" ]]; then
    run_az ad app permission add \
      --id "$APP_ID" \
      --api "$RESOURCE_ID" \
      --api-permissions "${SCOPE_ID}=Scope" >/dev/null
    echo "    $RESOURCE_ID user_impersonation ✓"
  else
    echo "    ⚠️  $RESOURCE_ID does not expose the user_impersonation scope, skipping"
  fi
done

echo
echo "    ⚠️  For co-sell, you also need to assign the Referrals admin role in the Partner Center Referrals workspace — see the docs"

echo
echo "==> 4/6 Grant admin consent + explicit grant (fallback)"
# First run admin-consent (covers the whole app in one call)
consent_ok=false
for i in $(seq 1 10); do
  if run_az ad app permission admin-consent --id "$APP_ID" >/dev/null 2>&1; then
    consent_ok=true
    break
  fi
  sleep 3
done
if ! $consent_ok; then
  echo "❌ admin-consent failed. Common causes:"
  echo "   - The current account is not a Global Administrator (Suger requires Global Admin)"
  echo "   - The tenant has disabled user/app consent"
  exit 1
fi

# Known az CLI quirk: admin-consent returning 0 doesn't always mean OAuth2PermissionGrant was created.
# Apply explicit `permission grant` as a fallback (idempotent — will not duplicate).
sleep 3  # let admin-consent propagate
for RESOURCE_ID in "$GRAPH_RESOURCE_ID" "${PARTNER_RESOURCE_IDS[@]}"; do
  SCOPE_VALUE="user_impersonation"
  [[ "$RESOURCE_ID" == "$GRAPH_RESOURCE_ID" ]] && SCOPE_VALUE="User.Read"
  run_az ad app permission grant \
    --id "$APP_ID" \
    --api "$RESOURCE_ID" \
    --scope "$SCOPE_VALUE" >/dev/null 2>&1 || true
done
echo "    ✓ admin-consent + explicit grant complete"

echo
echo "==> 5/6 Generate client secret (${SECRET_DAYS}-day validity)"
# Use --end-date instead of --years for precise day-level expiry. Default 365 days
# (~1 year) matches a standard secret-rotation cadence; rotate by re-running
# with REUSE_EXISTING=true before expiry.
SECRET_END_DATE=$(
  date -u -d "+${SECRET_DAYS} days" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null \
    || date -u -v+"${SECRET_DAYS}"d +%Y-%m-%dT%H:%M:%SZ 2>/dev/null
)
[[ -n "$SECRET_END_DATE" ]] || { echo "❌ Could not compute secret expiry"; exit 1; }

SECRET=$(run_az ad app credential reset \
  --id "$APP_ID" \
  --end-date "$SECRET_END_DATE" \
  --append \
  --display-name "suger-$(date +%Y%m%d)" \
  --query password -o tsv)
[[ -n "$SECRET" ]] || { echo "❌ Failed to generate client secret"; exit 1; }

SECRET_EXPIRY_DATE="${SECRET_END_DATE%%T*}"

echo
echo "==> 6/6 Self-verify: exchange the new secret for a Marketplace API token"
if [[ "$SKIP_VERIFY" =~ ^([Tt]rue|1)$ ]]; then
  echo "    SKIP_VERIFY=true, skipping"
else
  # Freshly-issued secrets occasionally take a moment to propagate in AAD — retry up to ~30s.
  verify_ok=false
  for i in $(seq 1 10); do
    resp=$(curl -sS -X POST \
      "https://login.microsoftonline.com/${TENANT_ID}/oauth2/token" \
      -H "Content-Type: application/x-www-form-urlencoded" \
      --data-urlencode "grant_type=client_credentials" \
      --data-urlencode "client_id=${APP_ID}" \
      --data-urlencode "client_secret=${SECRET}" \
      --data-urlencode "resource=${MARKETPLACE_SAAS_RESOURCE_ID}" || true)
    if echo "$resp" | jq -e '.access_token' >/dev/null 2>&1; then
      verify_ok=true
      break
    fi
    sleep 3
  done
  if $verify_ok; then
    echo "    ✓ Got a Marketplace API token — the end-to-end auth flow is working"
  else
    echo "    ⚠️  Self-verification failed. Suger will likely also fail to authenticate. Last response:"
    echo "$resp" | jq . 2>/dev/null || echo "$resp"
    echo "    Continuing to print credentials; resolve the issue before handing them over to Suger."
  fi
fi

# Persist credentials to a file to avoid scrollback/copy loss.
umask 077
cat > "$OUTPUT_FILE" <<EOF
# Suger Azure integration credentials (generated $(date -u +%Y-%m-%dT%H:%M:%SZ))
# ⚠️ Contains a client secret — store safely and delete this file afterwards.
APP_NAME="$APP_NAME"
TENANT_ID="$TENANT_ID"
APP_ID="$APP_ID"
CLIENT_SECRET="$SECRET"
SECRET_EXPIRES_IN_DAYS=$SECRET_DAYS
SECRET_EXPIRY_DATE="$SECRET_EXPIRY_DATE"
EOF
chmod 600 "$OUTPUT_FILE"

cat <<EOF

================================================================
✅ Azure-side setup complete. Paste these three values into the Suger Console:
================================================================

  Tenant ID:       $TENANT_ID
  Application ID:  $APP_ID
  Client Secret:   $SECRET
  Secret expires:  $SECRET_EXPIRY_DATE  ⚠️ Rotate before expiry or Suger sync will silently break

  Look up your Publisher ID in Partner Center → Account settings,
  and fill it in alongside the three values above (the fourth field in the Suger UI).

  📁 Persisted (mode 600) to: $OUTPUT_FILE

================================================================
Next step: configure Technical Configuration for your SaaS offer in Partner Center
================================================================

  Microsoft Entra tenant ID:       $TENANT_ID
  Microsoft Entra application ID:  $APP_ID
  Landing page URL:                <provided by Suger, format:
                                    https://api.{region}.suger.io/public/signup/azure/orgId/{orgId}>
  Connection webhook:              <provided by Suger, format:
                                    https://api.{region}.suger.io/public/azure/fulfillment/webhook/orgId/{orgId}>

  Save → republish the offer → the next Marketplace order will deliver a webhook to Suger.

  ⚠️ The App ID in Technical Configuration MUST equal the APP_ID above, or both directions break:
     - Suger rejects incoming MS webhooks on JWT aud mismatch
     - MS SaaS Fulfillment API rejects Suger's calls: "token is valid, however it does not
       include the correct audience, the entra tenant id, or the entra app id"

================================================================
⚠️  The client secret was written to $OUTPUT_FILE. Copy it to a password manager, then delete that file.
================================================================
EOF
