$items * @return array{entries: array>, mapping: array, failed: int, skipped: int} */ public function map(Tenant $tenant, Collection $items, bool $execute = false): array { $entries = []; $mapping = []; $failed = 0; $skipped = 0; if ($items->isEmpty()) { return [ 'entries' => $entries, 'mapping' => $mapping, 'failed' => $failed, 'skipped' => $skipped, ]; } $itemsByType = $items->groupBy('policy_type'); foreach ($itemsByType as $foundationType => $typeItems) { $foundationType = (string) $foundationType; $existingOutcome = $this->snapshotService->fetchAll($tenant, $foundationType); $existingFailures = $existingOutcome['failures'] ?? []; if (! empty($existingFailures)) { $reason = $existingFailures[0]['reason'] ?? 'Unable to list foundation resources.'; foreach ($typeItems as $item) { $entries[] = $this->failureEntry($foundationType, $item, $reason); $failed++; } continue; } $existing = $existingOutcome['items'] ?? []; $existingByName = $this->indexExistingByName($existing); $existingNames = array_keys($existingByName); foreach ($typeItems as $item) { $sourceId = $item->policy_identifier; $sourceName = $item->resolvedDisplayName(); if ($sourceName === '') { $entries[] = $this->skipEntry($foundationType, $sourceId, null, 'Missing display name.'); $skipped++; continue; } $normalizedName = strtolower($sourceName); $matches = $existingByName[$normalizedName] ?? []; if (count($matches) === 1) { $match = $matches[0]; $targetId = $match['source_id'] ?? null; $targetName = $match['display_name'] ?? null; if (is_string($targetId) && $targetId !== '') { $mapping[$sourceId] = $targetId; } $entries[] = $this->decisionEntry( $foundationType, $sourceId, $sourceName, 'mapped_existing', $targetId, $targetName, null ); continue; } $builtIn = $this->isBuiltInScopeTag($foundationType, $item); if ($builtIn) { $entries[] = $this->skipEntry( $foundationType, $sourceId, $sourceName, 'Built-in scope tag cannot be created.' ); $skipped++; continue; } $decision = count($matches) > 1 ? 'created_copy' : 'created'; $targetName = $decision === 'created_copy' ? $this->resolveCopyName($sourceName, $existingNames) : $sourceName; if (! $execute) { $entries[] = $this->decisionEntry( $foundationType, $sourceId, $sourceName, $decision, null, $targetName, count($matches) > 1 ? 'Multiple matches found by name.' : null ); continue; } $createOutcome = $this->createFoundation( tenant: $tenant, foundationType: $foundationType, item: $item, targetName: $targetName ); if ($createOutcome['success'] ?? false) { $targetId = $createOutcome['target_id'] ?? null; $createdName = $createOutcome['target_name'] ?? $targetName; if (is_string($targetId) && $targetId !== '') { $mapping[$sourceId] = $targetId; } $entries[] = $this->decisionEntry( $foundationType, $sourceId, $sourceName, $decision, $targetId, $createdName, null ); continue; } $entries[] = $this->failureEntry( $foundationType, $item, $createOutcome['reason'] ?? 'Failed to create foundation resource.' ); $failed++; } } return [ 'entries' => $entries, 'mapping' => $mapping, 'failed' => $failed, 'skipped' => $skipped, ]; } /** * @param array $existing * @return array> */ private function indexExistingByName(array $existing): array { $index = []; foreach ($existing as $item) { $name = $item['display_name'] ?? null; if (! is_string($name) || $name === '') { continue; } $index[strtolower($name)][] = [ 'source_id' => $item['source_id'], 'display_name' => $name, ]; } return $index; } private function resolveCopyName(string $baseName, array $existingNames): string { $suffix = ' (Copy)'; $candidate = $baseName.$suffix; $normalized = array_map('strtolower', $existingNames); if (! in_array(strtolower($candidate), $normalized, true)) { return $candidate; } $counter = 2; while (true) { $candidate = sprintf('%s (Copy %d)', $baseName, $counter); if (! in_array(strtolower($candidate), $normalized, true)) { return $candidate; } $counter++; } } private function isBuiltInScopeTag(string $foundationType, BackupItem $item): bool { if ($foundationType !== 'roleScopeTag') { return false; } if ($item->policy_identifier === '0') { return true; } $payload = is_array($item->payload) ? $item->payload : []; return (bool) ($payload['isBuiltIn'] ?? false); } /** * @return array{success: bool, target_id: ?string, target_name: ?string, reason: ?string} */ private function createFoundation( Tenant $tenant, string $foundationType, BackupItem $item, string $targetName ): array { $resource = $this->contracts->resourcePath($foundationType); if (! $resource) { return [ 'success' => false, 'target_id' => null, 'target_name' => null, 'reason' => 'Graph contract resource missing for foundation type.', ]; } $contract = $this->contracts->get($foundationType); $method = strtoupper((string) ($contract['create_method'] ?? 'POST')); $payload = $this->contracts->sanitizeUpdatePayload($foundationType, is_array($item->payload) ? $item->payload : []); $payload = $this->applyDisplayName($payload, $targetName); if ($payload === []) { return [ 'success' => false, 'target_id' => null, 'target_name' => null, 'reason' => 'Foundation payload could not be sanitized.', ]; } $response = $this->graphClient->request( $method, $resource, ['json' => $payload] + $tenant->graphOptions() ); if ($response->failed()) { return [ 'success' => false, 'target_id' => null, 'target_name' => null, 'reason' => $response->meta['error_message'] ?? 'Graph create failed.', ]; } $data = $response->data; $targetId = is_array($data) ? ($data['id'] ?? null) : null; $targetName = is_array($data) ? ($data['displayName'] ?? $data['name'] ?? $targetName) : $targetName; return [ 'success' => true, 'target_id' => is_string($targetId) ? $targetId : null, 'target_name' => is_string($targetName) ? $targetName : $targetName, 'reason' => null, ]; } /** * @param array $payload * @return array */ private function applyDisplayName(array $payload, string $targetName): array { if (array_key_exists('displayName', $payload)) { $payload['displayName'] = $targetName; return $payload; } if (array_key_exists('name', $payload)) { $payload['name'] = $targetName; return $payload; } $payload['displayName'] = $targetName; return $payload; } private function decisionEntry( string $foundationType, string $sourceId, string $sourceName, string $decision, ?string $targetId, ?string $targetName, ?string $reason ): array { return array_filter([ 'type' => $foundationType, 'sourceId' => $sourceId, 'sourceName' => $sourceName, 'decision' => $decision, 'targetId' => $targetId, 'targetName' => $targetName, 'reason' => $reason, ], static fn ($value) => $value !== null); } private function skipEntry( string $foundationType, string $sourceId, ?string $sourceName, string $reason ): array { return $this->decisionEntry( $foundationType, $sourceId, $sourceName ?? $sourceId, 'skipped', null, null, $reason ); } private function failureEntry(string $foundationType, BackupItem $item, string $reason): array { $sourceName = $item->resolvedDisplayName(); return $this->decisionEntry( $foundationType, $item->policy_identifier, $sourceName, 'failed', null, null, $reason ); } }