# =============================================================================
# setup-access-tiers.ps1
#
# CredManager のアクセス区分バックボーン (Azure Key Vault・区分鍵・Entra グループ・
# 鍵スコープの RBAC) を自動プロビジョニングするスクリプト。setup-sharepoint.ps1 と
# 対になる「Azure / Entra 側」セットアップ。
#
# やること:
#   1. リソースグループを (無ければ) 作成
#   2. Key Vault を RBAC モードで (無ければ) 作成 — 既存名があれば再利用
#   3. 区分ごとに Key Vault 鍵 (KEK, RSA-2048) を (無ければ) 作成
#   4. 区分ごとに Entra セキュリティグループを用意 (新規作成 or 既存 ObjectId 再利用)
#   5. 各区分の Entra グループに、その区分の鍵*単体スコープ* で
#      "Key Vault Crypto User" ロールを (無ければ) 割り当て
#   6. 区分マッピング + Key Vault URI を accessTiers.json として出力
#
# 出力 accessTiers.json は setup-sharepoint.ps1 がそのまま読める形式。
# 2 本のスクリプトはこの JSON を介して疎結合。
#
# 冪等性:
#   全工程「無ければ作る・あれば再利用」。再実行安全。
#   既存資源 (Vault / 鍵 / グループ / RBAC) を *削除* する処理は一切無し。
#
# 必要権限 (操作者):
#   - 対象サブスクリプションで Owner または User Access Administrator 相当
#     (Key Vault 作成 + 鍵スコープのロール割り当てに必要)
#   - Entra ID で Group.ReadWrite.All の委任同意 (グループ新規作成・読取に使用)
#   不足する場合は明確なエラーで停止する。
#
# 例:
#   pwsh setup-access-tiers.ps1 `
#       -SubscriptionId <YOUR_SUBSCRIPTION_ID> `
#       -ResourceGroup <YOUR_RESOURCE_GROUP> `
#       -VaultName <YOUR_VAULT_NAME> `
#       -Location japaneast `
#       -TiersJsonPath .\tiers-input.json
#
#   (対話モード) pwsh setup-access-tiers.ps1
#     → サブスクリプション / RG / Vault / Location を順次プロンプト
#     → 区分定義も対話で収集 (社外秘 1 区分が既定提案)
# =============================================================================

param(
    # Azure リソース
    [string]$SubscriptionId,
    [string]$ResourceGroup,
    [string]$VaultName,
    [string]$Location = "japaneast",

    # 区分定義: JSON ファイル経由 (推奨) or オブジェクト配列直接
    # JSON 形式:
    #   [
    #     { "DisplayName": "社外秘", "KeyName": "credmanager-kek",
    #       "GroupName": "CredManager-Confidential", "GroupObjectId": null }
    #   ]
    # GroupObjectId が指定されていれば既存グループを再利用。null/空なら GroupName で新規作成。
    [string]$TiersJsonPath,
    [object[]]$Tiers,

    # 出力先 (省略時はスクリプト同階層の accessTiers.json)
    [string]$AccessTiersJsonOutputPath,

    # 非対話モード (ウィザード等から呼ばれるとき)。Read-Host "Enter で終了します" をスキップ
    # する。必要な値が未指定で対話入力に落ちる箇所も、ここを true にしてあれば error にする想定。
    [switch]$NonInteractive
)

$ErrorActionPreference = "Stop"

function Pause { if (-not $NonInteractive) { Read-Host "Enter で終了します" } }

# ===== 既定パス =====
if ($PSScriptRoot) { $defaultDir = $PSScriptRoot } else { $defaultDir = (Get-Location).Path }
if (-not $AccessTiersJsonOutputPath) {
    $AccessTiersJsonOutputPath = Join-Path $defaultDir "accessTiers.json"
}

# =============================================================================
# モジュール準備 (Az + Microsoft.Graph) — setup-sharepoint.ps1 と同じ思想
# =============================================================================
function Ensure-Module {
    param([string]$Name)
    try {
        Import-Module $Name -ErrorAction Stop
        return $true
    } catch {
        # 続いてインストール試行
    }
    Write-Host "  '$Name' をインストールしています (CurrentUser)..." -ForegroundColor Cyan
    try {
        try {
            [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
        } catch {
            # 古い .NET ランタイムでは失敗することがあるが続行
        }
        if (-not (Get-PackageProvider -Name NuGet -ErrorAction SilentlyContinue)) {
            Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -Scope CurrentUser -ErrorAction Stop | Out-Null
        }
        try {
            $repo = Get-PSRepository -Name PSGallery -ErrorAction SilentlyContinue
            if ($repo -and $repo.InstallationPolicy -ne "Trusted") {
                Set-PSRepository -Name PSGallery -InstallationPolicy Trusted -ErrorAction SilentlyContinue
            }
        } catch {
            # -Force で抑制
        }
        Install-Module -Name $Name -Scope CurrentUser -Force -AllowClobber -ErrorAction Stop
        Import-Module $Name -ErrorAction Stop
        return $true
    } catch {
        Write-Host "  '$Name' の準備に失敗しました: $($_.Exception.Message)" -ForegroundColor Red
        Write-Host "  手動: Install-Module $Name -Scope CurrentUser -Force" -ForegroundColor Yellow
        return $false
    }
}

Write-Host "===== setup-access-tiers.ps1 =====" -ForegroundColor Cyan
Write-Host "依存モジュールを確認しています..." -ForegroundColor Cyan

$required = @("Az.Accounts", "Az.Resources", "Az.KeyVault", "Microsoft.Graph.Authentication", "Microsoft.Graph.Groups")
$allOk = $true
foreach ($mod in $required) {
    if (-not (Ensure-Module -Name $mod)) { $allOk = $false }
}
if (-not $allOk) {
    Write-Host "依存モジュールを準備できなかったため終了します。" -ForegroundColor Red
    Pause
    exit 1
}

# =============================================================================
# 区分定義の取得 (param 優先 → JSON ファイル → 対話入力)
# =============================================================================
function Resolve-Tiers {
    if ($Tiers -and $Tiers.Count -gt 0) {
        Write-Host "  区分定義: -Tiers パラメータから ($($Tiers.Count) 件)" -ForegroundColor DarkGray
        return $Tiers
    }
    if ($TiersJsonPath) {
        if (-not (Test-Path $TiersJsonPath)) {
            throw "TiersJsonPath が見つかりません: $TiersJsonPath"
        }
        $raw = Get-Content -Raw $TiersJsonPath | ConvertFrom-Json
        Write-Host "  区分定義: $TiersJsonPath から ($($raw.Count) 件)" -ForegroundColor DarkGray
        return @($raw)
    }

    # 対話モード: 社外秘 1 区分を既定提案、追加可
    Write-Host ""
    Write-Host "区分定義を対話で収集します (空 Enter=完了)" -ForegroundColor Cyan
    $defaultProposal = @(
        [ordered]@{ DisplayName = "社外秘"; KeyName = "credmanager-kek"; GroupName = "CredManager-Confidential" }
    )
    $collected = @()
    $i = 0
    while ($true) {
        $proposal = if ($i -lt $defaultProposal.Count) { $defaultProposal[$i] } else { $null }
        $idx = $i + 1
        Write-Host ""
        if ($proposal) {
            Write-Host "[区分 $idx] 既定提案あり (Enter=既定で確定 / 値を入力して上書き)" -ForegroundColor Cyan
            $name = Read-Host "  表示名 [$($proposal.DisplayName)]"
            if ([string]::IsNullOrWhiteSpace($name)) { $name = $proposal.DisplayName }
            $keyName = Read-Host "  Key Vault キー名 [$($proposal.KeyName)]"
            if ([string]::IsNullOrWhiteSpace($keyName)) { $keyName = $proposal.KeyName }
            $groupName = Read-Host "  Entra グループ名 (新規作成時) [$($proposal.GroupName)]"
            if ([string]::IsNullOrWhiteSpace($groupName)) { $groupName = $proposal.GroupName }
        } else {
            Write-Host "[区分 $idx] 追加 (区分名を空 Enter で完了)" -ForegroundColor Cyan
            $name = Read-Host "  表示名"
            if ([string]::IsNullOrWhiteSpace($name)) { break }
            $keyName = Read-Host "  Key Vault キー名"
            $groupName = Read-Host "  Entra グループ名 (新規作成時)"
        }
        $existingOid = Read-Host "  既存グループの Object ID で再利用するなら入力 (空 Enter=新規作成)"
        $collected += [pscustomobject]@{
            DisplayName   = $name.Trim()
            KeyName       = $keyName.Trim()
            GroupName     = $groupName.Trim()
            GroupObjectId = if ([string]::IsNullOrWhiteSpace($existingOid)) { $null } else { $existingOid.Trim() }
        }
        $i++
        if ($i -ge $defaultProposal.Count) {
            $more = Read-Host "  さらに区分を追加しますか? [y/N]"
            if ($more -ne "y" -and $more -ne "Y") { break }
        }
    }
    return @($collected)
}

$tierDefs = Resolve-Tiers
if (-not $tierDefs -or $tierDefs.Count -eq 0) {
    Write-Host "区分が 1 件もありません。終了します。" -ForegroundColor Red
    Pause
    exit 1
}

# =============================================================================
# Azure リソース引数の対話補完
# =============================================================================
if (-not $SubscriptionId) {
    $SubscriptionId = Read-Host "サブスクリプション ID"
    if ([string]::IsNullOrWhiteSpace($SubscriptionId)) {
        Write-Host "サブスクリプション ID が空のため終了します。" -ForegroundColor Red
        Pause
        exit 1
    }
}
if (-not $ResourceGroup) {
    $ResourceGroup = Read-Host "リソースグループ名"
    if ([string]::IsNullOrWhiteSpace($ResourceGroup)) {
        Write-Host "リソースグループ名が空のため終了します。" -ForegroundColor Red
        Pause
        exit 1
    }
}
if (-not $VaultName) {
    $VaultName = Read-Host "Key Vault 名 (テナント内一意)"
    if ([string]::IsNullOrWhiteSpace($VaultName)) {
        Write-Host "Key Vault 名が空のため終了します。" -ForegroundColor Red
        Pause
        exit 1
    }
}

# =============================================================================
# サインイン (Az + Microsoft.Graph) — 既存コンテキストがあれば再利用
# =============================================================================
Write-Host ""
Write-Host "Azure / Entra にサインインしています..." -ForegroundColor Cyan

# Az
$azCtx = Get-AzContext -ErrorAction SilentlyContinue
if (-not $azCtx -or $azCtx.Subscription.Id -ne $SubscriptionId) {
    try {
        Connect-AzAccount -Subscription $SubscriptionId -ErrorAction Stop | Out-Null
    } catch {
        Write-Host "Connect-AzAccount に失敗: $($_.Exception.Message)" -ForegroundColor Red
        Pause
        exit 1
    }
} else {
    Write-Host "  Az: 既存コンテキストを再利用 ($($azCtx.Account.Id))" -ForegroundColor DarkGray
}
Set-AzContext -SubscriptionId $SubscriptionId -ErrorAction Stop | Out-Null

# Microsoft.Graph
$mgScopes = @("Group.ReadWrite.All", "Directory.Read.All")
$mgCtx = $null
try { $mgCtx = Get-MgContext -ErrorAction SilentlyContinue } catch { $mgCtx = $null }
$mgScopesOk = $mgCtx -and ($mgScopes | ForEach-Object { $mgCtx.Scopes -contains $_ }) -notcontains $false
if (-not $mgCtx -or -not $mgScopesOk) {
    try {
        Connect-MgGraph -Scopes $mgScopes -NoWelcome -ErrorAction Stop | Out-Null
    } catch {
        Write-Host "Connect-MgGraph に失敗: $($_.Exception.Message)" -ForegroundColor Red
        Write-Host "  Group.ReadWrite.All の同意が組織で許可されているか確認してください。" -ForegroundColor Yellow
        Pause
        exit 1
    }
} else {
    Write-Host "  Microsoft.Graph: 既存セッションを再利用 ($($mgCtx.Account))" -ForegroundColor DarkGray
}

# =============================================================================
# リソースグループ
# =============================================================================
Write-Host ""
Write-Host "リソースグループ '$ResourceGroup' を確認しています..." -ForegroundColor Cyan
$rg = Get-AzResourceGroup -Name $ResourceGroup -ErrorAction SilentlyContinue
$rgAction = "reuse"
if (-not $rg) {
    Write-Host "  作成します ($Location)..." -ForegroundColor Green
    $rg = New-AzResourceGroup -Name $ResourceGroup -Location $Location -ErrorAction Stop
    $rgAction = "created"
} else {
    Write-Host "  既存を再利用 (Location: $($rg.Location))" -ForegroundColor DarkGray
}

# =============================================================================
# Key Vault (RBAC モード)
# =============================================================================
Write-Host ""
Write-Host "Key Vault '$VaultName' を確認しています..." -ForegroundColor Cyan
$vault = Get-AzKeyVault -VaultName $VaultName -ResourceGroupName $ResourceGroup -ErrorAction SilentlyContinue
$vaultAction = "reuse"
Write-Host ""
Write-Host "  注意: Purge Protection (パージ保護) を有効化します。" -ForegroundColor Yellow
Write-Host "  注意: Purge Protection は仕様上 *一度有効にすると無効化できません*。" -ForegroundColor Yellow
Write-Host "  注意: ただし誤って Vault/鍵を削除した際の永久消失を防ぐので、本セットアップでは意図して有効化します。" -ForegroundColor Yellow
Write-Host ""
if (-not $vault) {
    Write-Host "  作成します (RBAC モード, Purge Protection 有効, Location: $Location)..." -ForegroundColor Green
    # Az.KeyVault 6.x の New-AzKeyVault は許可モデルを *逆方向* スイッチ -DisableRbacAuthorization で扱う。
    # 既定 (省略 / :$false) = RBAC モード。アクセスポリシー モードにしたい場合のみ -DisableRbacAuthorization を付与する。
    # CredManager は RBAC 専用なので明示的に :$false を渡し、将来 cmdlet の既定値が変わっても影響を受けないようにする。
    # Soft-delete は既定で有効。-EnablePurgeProtection で purge protection を作成時に有効化。
    $vault = New-AzKeyVault -Name $VaultName -ResourceGroupName $ResourceGroup -Location $Location `
        -DisableRbacAuthorization:$false -EnablePurgeProtection -ErrorAction Stop
    $vaultAction = "created"
} else {
    Write-Host "  既存を再利用 (Uri: $($vault.VaultUri))" -ForegroundColor DarkGray

    # ---- 許可モデルを RBAC に明示保証 (Update-AzKeyVault の副作用で倒れていた事例あり) ----
    # CredManager は RBAC 専用設計。アクセスポリシー方式に倒れていれば必ず戻す。
    # また、後段の purge protection 更新でも必ず -DisableRbacAuthorization $false を併記し、
    # cmdlet 実装が許可モデルを既定値に戻してしまう既知の地雷を踏まないようにする。
    # 注意: Az.KeyVault 6.x の Update-AzKeyVault のパラメータ名は -DisableRbacAuthorization (Nullable<bool>)。
    # 旧名の -EnableRbacAuthorization は本バージョンには存在しない。
    if (-not $vault.EnableRbacAuthorization) {
        Write-Host "  ⚠ この Vault はアクセスポリシー モードです。RBAC モードに切替えます..." -ForegroundColor Yellow
        try {
            Update-AzKeyVault -VaultName $VaultName -ResourceGroupName $ResourceGroup `
                -DisableRbacAuthorization $false -ErrorAction Stop | Out-Null
            $vaultAction = "rbac-enabled"
            $vault = Get-AzKeyVault -VaultName $VaultName -ResourceGroupName $ResourceGroup -ErrorAction Stop
            Write-Host "  RBAC モードに切替えました。" -ForegroundColor Green
        } catch {
            Write-Host "  RBAC モードへの切替に失敗: $($_.Exception.Message)" -ForegroundColor Red
            Write-Host "    Owner / Contributor 相当の権限が必要です。手動で切替えてください。" -ForegroundColor Yellow
        }
    }

    # ---- Purge Protection を冪等に有効化 ----
    # 重要: Az.KeyVault 6.x の Update-AzKeyVault は「明示されていないプロパティ」を既定値で送信する
    # ことがあり、許可モデルがアクセスポリシー側に倒れる事例が発生した (本番事故の直接原因)。
    # この呼出しでは必ず -DisableRbacAuthorization $false を併記して RBAC を維持する。
    if (-not $vault.EnablePurgeProtection) {
        Write-Host "  既存 Vault に Purge Protection を有効化します (この変更は取り消せません)..." -ForegroundColor Yellow
        try {
            Update-AzKeyVault -VaultName $VaultName -ResourceGroupName $ResourceGroup `
                -EnablePurgeProtection `
                -DisableRbacAuthorization $false `
                -ErrorAction Stop | Out-Null
            $vaultAction = if ($vaultAction -eq "reuse") { "purge-protection-enabled" } else { $vaultAction }
            # vault オブジェクトを最新化
            $vault = Get-AzKeyVault -VaultName $VaultName -ResourceGroupName $ResourceGroup -ErrorAction Stop
        } catch {
            Write-Host "  Purge Protection の有効化に失敗: $($_.Exception.Message)" -ForegroundColor Red
            Write-Host "    続行はしますが、災害復旧の観点で再有効化を強くお勧めします。" -ForegroundColor Yellow
        }
    } else {
        Write-Host "  Purge Protection は既に有効です。" -ForegroundColor DarkGray
    }

    # ---- 最終確認: 許可モデルが RBAC で確定したか ----
    # ここに到達した時点で必ず EnableRbacAuthorization=$true であるべき。万一 false のままなら
    # 後段の鍵スコープ RBAC 割当てが効かなくなるため、明確に警告して継続。
    if (-not $vault.EnableRbacAuthorization) {
        Write-Host "  ⚠ 警告: 許可モデルが RBAC になっていません。後段のロール割当ては効きません。" -ForegroundColor Red
        Write-Host "    手動で許可モデルを RBAC (Azure RBAC) に切替えてから再実行してください。" -ForegroundColor Red
    }
}

# =============================================================================
# リトライヘルパ (RBAC 伝播待ち)
# =============================================================================
function Invoke-WithRetry {
    param([scriptblock]$Action, [int]$MaxAttempts = 8, [int]$SleepSec = 8, [string]$Label = "")
    for ($n = 1; $n -le $MaxAttempts; $n++) {
        try {
            return & $Action
        } catch {
            if ($n -eq $MaxAttempts) { throw }
            $msg = $_.Exception.Message
            Write-Host "    リトライ $n/$MaxAttempts ($Label) — $msg" -ForegroundColor DarkGray
            Start-Sleep -Seconds $SleepSec
        }
    }
}

# =============================================================================
# 区分ごとの処理: 鍵 → グループ → RBAC
# =============================================================================
$subId = $SubscriptionId
$results = @()

# ネイティブバックアップ blob の出力フォルダ (accessTiers.json と同じ場所)
$backupOutDir = [System.IO.Path]::GetDirectoryName([System.IO.Path]::GetFullPath($AccessTiersJsonOutputPath))
if (-not (Test-Path $backupOutDir)) {
    New-Item -ItemType Directory -Path $backupOutDir -Force | Out-Null
}
$backupDateTag = (Get-Date -Format "yyyyMMdd")
$nativeBackupUtc = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")

foreach ($tier in $tierDefs) {
    $disp = "$($tier.DisplayName)"
    $kn   = "$($tier.KeyName)"
    $gn   = "$($tier.GroupName)"
    $goid = if ($tier.PSObject.Properties.Match("GroupObjectId").Count -gt 0) { $tier.GroupObjectId } else { $null }

    Write-Host ""
    Write-Host "==== 区分: $disp ====" -ForegroundColor Cyan

    # ----- (a) Key Vault 鍵 -----
    $keyAction = "reuse"
    $existingKey = Get-AzKeyVaultKey -VaultName $VaultName -Name $kn -ErrorAction SilentlyContinue
    if (-not $existingKey) {
        Write-Host "  鍵 '$kn' を作成 (RSA-2048, Software)..." -ForegroundColor Green
        Add-AzKeyVaultKey -VaultName $VaultName -Name $kn -Destination Software -KeyType RSA -Size 2048 -ErrorAction Stop | Out-Null
        $keyAction = "created"
    } else {
        Write-Host "  鍵 '$kn' は既存を再利用" -ForegroundColor DarkGray
    }

    # ----- (a-2) Key Vault ネイティブバックアップ -----
    # 鍵の暗号化 blob を取得し、accessTiers.json と同じフォルダに保存する。
    # この blob は *同一サブスクリプション・同一地域の Key Vault にしか復元できない* 制約があるが、
    # Vault レベルでの誤削除/破損からの最後の砦になる。
    # ファイル名は ASCII (KeyName ベース) で固定。冪等性: 同日再実行は同名ファイルを上書き。
    $backupFileName = "keyvault-native-backup-$kn-$backupDateTag.blob"
    $backupFilePath = Join-Path $backupOutDir $backupFileName
    $backupAction = "skipped"
    try {
        if (Test-Path $backupFilePath) {
            # Backup-AzKeyVaultKey は既存ファイルがあると失敗するので、当日再実行時は事前削除
            Remove-Item $backupFilePath -Force -ErrorAction Stop
        }
        Backup-AzKeyVaultKey -VaultName $VaultName -Name $kn -OutputFile $backupFilePath -ErrorAction Stop | Out-Null
        Write-Host "  ネイティブバックアップ blob を保存: $backupFilePath" -ForegroundColor Green
        $backupAction = "saved"
    } catch {
        Write-Host "  ネイティブバックアップに失敗: $($_.Exception.Message)" -ForegroundColor Yellow
        Write-Host "    続行します。Backup-AzKeyVaultKey は鍵スコープの読み取り権限を要求します。" -ForegroundColor DarkGray
        $backupAction = "ERROR"
    }

    # ----- (b) Entra グループ -----
    $groupAction = "reuse"
    $resolvedGroupId = $null
    if ($goid) {
        # 既存 ObjectId 指定 → 存在検証
        try {
            $g = Get-MgGroup -GroupId $goid -ErrorAction Stop
            $resolvedGroupId = $g.Id
            Write-Host "  グループ ObjectId 再利用: $resolvedGroupId (displayName='$($g.DisplayName)')" -ForegroundColor DarkGray
        } catch {
            Write-Host "  指定 GroupObjectId が見つかりません: $goid — $($_.Exception.Message)" -ForegroundColor Red
            $results += [pscustomobject]@{ Tier=$disp; Key=$keyAction; Group="ERROR"; Rbac="-"; GroupObjectId="-" }
            continue
        }
    } else {
        # 名前で検索 → 無ければ作成
        $found = $null
        try {
            $found = Get-MgGroup -Filter "displayName eq '$gn'" -ConsistencyLevel eventual -CountVariable c -ErrorAction Stop | Select-Object -First 1
        } catch {
            $found = $null
        }
        if ($found) {
            $resolvedGroupId = $found.Id
            Write-Host "  グループ '$gn' は既存を再利用 (Id: $resolvedGroupId)" -ForegroundColor DarkGray
        } else {
            $nick = ($gn -replace '[^a-zA-Z0-9]', '').ToLowerInvariant()
            if ([string]::IsNullOrEmpty($nick)) { $nick = "credmgrtier" + (Get-Random -Maximum 9999) }
            Write-Host "  グループ '$gn' を作成 (mailNickname=$nick)..." -ForegroundColor Green
            $created = New-MgGroup -DisplayName $gn -MailEnabled:$false -MailNickname $nick -SecurityEnabled:$true -ErrorAction Stop
            $resolvedGroupId = $created.Id
            $groupAction = "created"
        }
    }

    # ----- (c) 鍵単位スコープの RBAC -----
    $keyScope = "/subscriptions/$subId/resourceGroups/$ResourceGroup/providers/Microsoft.KeyVault/vaults/$VaultName/keys/$kn"
    $rbacAction = "reuse"
    $existingRA = Get-AzRoleAssignment -Scope $keyScope -ObjectId $resolvedGroupId -RoleDefinitionName "Key Vault Crypto User" -ErrorAction SilentlyContinue
    if (-not $existingRA) {
        Write-Host "  ロール割当: 'Key Vault Crypto User' → $resolvedGroupId @ key scope" -ForegroundColor Green
        try {
            Invoke-WithRetry -Label "New-AzRoleAssignment" -MaxAttempts 8 -SleepSec 8 -Action {
                New-AzRoleAssignment -Scope $keyScope -ObjectId $resolvedGroupId -RoleDefinitionName "Key Vault Crypto User" -ErrorAction Stop | Out-Null
            }
            $rbacAction = "created"
        } catch {
            Write-Host "  ロール割当に失敗しました: $($_.Exception.Message)" -ForegroundColor Red
            Write-Host "    考えられる原因: 操作者に Owner / User Access Administrator 権限がない" -ForegroundColor Yellow
            $rbacAction = "ERROR"
        }
    } else {
        Write-Host "  ロール割当は既存を再利用" -ForegroundColor DarkGray
    }

    $results += [pscustomobject]@{
        Tier         = $disp
        KeyName      = $kn
        Key          = $keyAction
        GroupName    = $gn
        Group        = $groupAction
        GroupObjectId= $resolvedGroupId
        Rbac         = $rbacAction
        Backup       = $backupAction
        BackupFile   = if ($backupAction -eq "saved") { $backupFilePath } else { "-" }
    }

    # 出力 JSON 用に GroupObjectId を tier オブジェクトに反映
    $tier | Add-Member -NotePropertyName GroupObjectId -NotePropertyValue $resolvedGroupId -Force
}

# =============================================================================
# accessTiers.json 出力 (setup-sharepoint.ps1 / C# AccessTierService が読む形式)
# =============================================================================
Write-Host ""
Write-Host "accessTiers.json を出力しています..." -ForegroundColor Cyan
$outputObj = [ordered]@{
    "_comment" = "Generated by setup-access-tiers.ps1 on $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss'). Vault=$VaultName / RG=$ResourceGroup / SubId=$SubscriptionId."
    # NativeKeyBackupUtc は setup-sharepoint.ps1 によって CredManagerSetting に伝播される。
    # 「Key Vault バックアップ状態」画面 (EnvSettings) でこの値を表示する。
    NativeKeyBackupUtc = $nativeBackupUtc
    AccessTiers = @($tierDefs | ForEach-Object {
        [ordered]@{
            DisplayName        = "$($_.DisplayName)"
            KeyVaultKeyName    = "$($_.KeyName)"
            EntraGroupObjectId = "$($_.GroupObjectId)"
        }
    })
}
try {
    $json = $outputObj | ConvertTo-Json -Depth 5
    $resolvedOut = [System.IO.Path]::GetFullPath($AccessTiersJsonOutputPath)
    $utf8NoBom = New-Object System.Text.UTF8Encoding($false)
    [System.IO.File]::WriteAllText($resolvedOut, $json, $utf8NoBom)
    Write-Host "  生成: $resolvedOut" -ForegroundColor Green
} catch {
    Write-Host "accessTiers.json の出力に失敗: $($_.Exception.Message)" -ForegroundColor Red
    Pause
    exit 1
}

# =============================================================================
# 完了表示
# =============================================================================
Write-Host ""
Write-Host "===== 完了 =====" -ForegroundColor Green
Write-Host "  Resource Group        : $ResourceGroup ($rgAction)"
Write-Host "  Key Vault             : $VaultName ($vaultAction)"
Write-Host "  Vault URI             : $($vault.VaultUri)"
Write-Host "  Purge Protection      : $(if ($vault.EnablePurgeProtection) { 'Enabled (取り消し不可)' } else { 'NOT enabled' })"
Write-Host "  Soft Delete (days)    : $($vault.SoftDeleteRetentionInDays)"
Write-Host "  Native Backup (UTC)   : $nativeBackupUtc"
Write-Host "  Backup フォルダ       : $backupOutDir"
Write-Host "  accessTiers           : $AccessTiersJsonOutputPath"
Write-Host ""
$results | Format-Table -AutoSize | Out-String | Write-Host

# バックアップ blob の保管に関する強い案内
$savedBlobs = @($results | Where-Object { $_.Backup -eq "saved" })
if ($savedBlobs.Count -gt 0) {
    Write-Host "---- ネイティブバックアップ blob の取扱い (重要) ----" -ForegroundColor Yellow
    Write-Host "  以下のファイルは、各区分の Key Vault 鍵 (KEK) の暗号化バックアップです。" -ForegroundColor Yellow
    foreach ($b in $savedBlobs) {
        Write-Host "    - $($b.BackupFile)" -ForegroundColor Yellow
    }
    Write-Host "  必ず、accessTiers.json とは別の、安全な場所 (オフライン媒体・金庫等) に保管してください。" -ForegroundColor Yellow
    Write-Host "  制約: この blob は *同一サブスクリプション・同一地域 ($Location) の Key Vault にしか復元できません*。" -ForegroundColor Yellow
    Write-Host "        テナント/サブスクリプション/地域を越えた復旧は B1 のキー情報バックアップ (.cmkb) で行ってください。" -ForegroundColor Yellow
    Write-Host ""
}

# RBAC エラーがあったかどうかで終了コード判断
$hadErr = ($results | Where-Object { $_.Rbac -eq "ERROR" -or $_.Group -eq "ERROR" }).Count -gt 0
if ($hadErr) {
    Write-Host "一部の区分でエラーがありました。上の表を確認してください。" -ForegroundColor Yellow
    Pause
    exit 1
}

Write-Host "次のステップ:" -ForegroundColor Cyan
Write-Host "  1. 生成された accessTiers.json と Key Vault URI を控えてください。"
Write-Host "  2. setup-sharepoint.ps1 を同じフォルダで実行すると、この accessTiers.json が"
Write-Host "     自動で読み込まれ CredManagerSetting リストの AccessTier 行に投入されます。"
Write-Host "  3. 利用者を該当の Entra グループに追加してください。"
Pause
exit 0
