#!/usr/bin/env bash
# assign-exchange-role.sh — assign the Entra "Exchange Administrator" directory role to the
# ComputerGuru Exchange Operator service principal in a customer tenant.
#
# WHY THIS EXISTS: app-only Exchange Online management (Search-UnifiedAuditLog, Get-MessageTrace,
# Get/Remove-InboxRule, Set-Mailbox, mailbox forwarding/delegate audit) requires the app's SP to
# hold BOTH the `Exchange.ManageAsApp` API permission (granted by admin consent) AND an Entra
# **directory role** (Exchange Administrator). Admin consent grants the API permission but NEVER
# the directory role — so every freshly-consented tenant 401/403s on EXO management until this one
# step is done. This script closes that gap, idempotently, and is wired into onboard-tenant.sh so
# new tenants get it automatically. Run `--all` to backfill the existing fleet.
#
# Usage:
#   assign-exchange-role.sh <domain-or-tenant-id>            assign for one tenant
#   assign-exchange-role.sh --all                            every tenant in references/tenants.md
#   assign-exchange-role.sh <target|--all> --verify          report current state only (no writes)
#   assign-exchange-role.sh <target|--all> --dry-run         show what WOULD change (no writes)
#
# Requires: the tenant-admin app consented in the target tenant (it carries
# RoleManagement.ReadWrite.Directory). Tenants where tenant-admin or the Exchange Operator app is
# not consented are SKIPPED with a clear reason (not an error).
#
# Read-only by default? NO — without --verify/--dry-run it performs the role assignment (a security
# change). It is idempotent: a tenant already assigned is reported and left untouched.

set -uo pipefail

EXCHANGE_OP_APPID="b43e7342-5b4b-492f-890f-bb5a4f7f40e9"   # ComputerGuru Exchange Operator
EXCH_ADMIN_TEMPLATE="29232cdf-9323-42fd-ade2-1d097af3e4de" # Entra "Exchange Administrator" roleTemplateId
GRAPH="https://graph.microsoft.com/v1.0"

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
GET_TOKEN="$SCRIPT_DIR/get-token.sh"
TENANTS_MD="$SCRIPT_DIR/../references/tenants.md"

# Resolve vault_path -> VAULT_ROOT_ENV so get-token.sh works regardless of ~/.claude/identity.json.
REPO_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)"
if [ -z "${VAULT_ROOT_ENV:-}" ]; then
    for idf in "$REPO_ROOT/.claude/identity.json" "$HOME/.claude/identity.json"; do
        [ -f "$idf" ] || continue
        vp="$(jq -r '.vault_path // empty' "$idf" 2>/dev/null)"
        [ -n "$vp" ] && { export VAULT_ROOT_ENV="$vp"; break; }
    done
fi

MODE="apply"
TARGET=""
for a in "$@"; do
    case "$a" in
        --verify)  MODE="verify" ;;
        --dry-run) MODE="dryrun" ;;
        --all)     TARGET="--all" ;;
        -h|--help) grep -E '^#( |$)' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
        *)         TARGET="$a" ;;
    esac
done
[ -n "$TARGET" ] || { echo "[ERROR] need a tenant (domain/id) or --all. See --help." >&2; exit 64; }

jqr() { jq -r "$1" 2>/dev/null | tr -d '\r'; }
gget() { curl -s --max-time 25 -H "Authorization: Bearer $1" "$2" | tr -d '\000'; }

# process_one <domain-or-tenant-id>
process_one() {
    local tgt="$1" tok sp_id role_id members present rc body
    printf '%-42s ' "$tgt"

    tok="$(VAULT_ROOT_ENV="${VAULT_ROOT_ENV:-}" bash "$GET_TOKEN" "$tgt" tenant-admin 2>/dev/null | tr -d '[:space:]')"
    if [ -z "$tok" ] || [ "${#tok}" -lt 100 ]; then echo "SKIP  (tenant-admin not consented)"; return; fi

    sp_id="$(gget "$tok" "$GRAPH/servicePrincipals?\$filter=appId%20eq%20'$EXCHANGE_OP_APPID'&\$select=id" | jqr '.value[0].id // empty')"
    if [ -z "$sp_id" ]; then echo "SKIP  (Exchange Operator app not consented in tenant)"; return; fi

    # Use the AUTHORITATIVE unified role-assignment API (roleManagement/directory/roleAssignments)
    # for both the idempotency check and the write. The legacy directoryRoles/{id}/members list
    # reads back unreliably (replication lag) and falsely reports not-assigned; roleAssignments is
    # consistent. For built-in roles, roleDefinitionId == the roleTemplateId.
    present="$(gget "$tok" "$GRAPH/roleManagement/directory/roleAssignments?\$filter=principalId%20eq%20'$sp_id'%20and%20roleDefinitionId%20eq%20'$EXCH_ADMIN_TEMPLATE'" | jqr '.value | length')"
    if [ "${present:-0}" -gt 0 ] 2>/dev/null; then echo "OK    (already assigned)"; return; fi

    if [ "$MODE" != "apply" ]; then echo "WOULD assign Exchange Admin to SP $sp_id"; return; fi

    rc="$(curl -s --max-time 25 -o /tmp/aer_resp.$$ -w '%{http_code}' -X POST "$GRAPH/roleManagement/directory/roleAssignments" \
        -H "Authorization: Bearer $tok" -H "Content-Type: application/json" \
        -d "{\"principalId\":\"$sp_id\",\"roleDefinitionId\":\"$EXCH_ADMIN_TEMPLATE\",\"directoryScopeId\":\"/\"}")"
    body="$(tr -d '\000' </tmp/aer_resp.$$ 2>/dev/null)"; rm -f /tmp/aer_resp.$$ 2>/dev/null
    case "$rc" in
        201) echo "ASSIGNED (Exchange Admin -> Exchange Operator SP)" ;;
        400) if echo "$body" | grep -qiE 'conflicting object|already (exist|present)'; then echo "OK    (already assigned)"
             else echo "ERROR  (HTTP 400: $(echo "$body" | jqr '.error.message // .' | head -c 120))"
                  bash "$REPO_ROOT/.claude/scripts/log-skill-error.sh" "remediation-tool" "assign-exchange-role: role assignment POST failed" --context "tenant=$tgt http=400 msg=$(echo "$body" | jqr '.error.message // .' | head -c 80)" >/dev/null 2>&1 || true; fi ;;
        *)   echo "ERROR  (HTTP $rc: $(echo "$body" | jqr '.error.message // .' | head -c 120))"
             bash "$REPO_ROOT/.claude/scripts/log-skill-error.sh" "remediation-tool" "assign-exchange-role: role assignment POST failed" --context "tenant=$tgt http=$rc msg=$(echo "$body" | jqr '.error.message // .' | head -c 80)" >/dev/null 2>&1 || true ;;
    esac
}

echo "=== assign-exchange-role  [mode=$MODE] ==="
echo "Role: Exchange Administrator ($EXCH_ADMIN_TEMPLATE)  ->  SP: Exchange Operator ($EXCHANGE_OP_APPID)"
echo "------------------------------------------------------------------------"
if [ "$TARGET" = "--all" ]; then
    [ -f "$TENANTS_MD" ] || { echo "[ERROR] tenants.md not found: $TENANTS_MD" >&2; bash "$REPO_ROOT/.claude/scripts/log-skill-error.sh" "remediation-tool" "assign-exchange-role: --all run but references/tenants.md not found" --context "path=$TENANTS_MD" >/dev/null 2>&1 || true; exit 66; }
    # extract tenant GUIDs from the markdown table (column 3)
    grep -oE '[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}' "$TENANTS_MD" \
        | sort -u | while read -r tid; do process_one "$tid"; done
else
    process_one "$TARGET"
fi
echo "------------------------------------------------------------------------"
echo "Done. (Re-run with --verify any time to audit fleet state.)"
