<?php

namespace App\Services;

use App\Models\BackupItem;
use App\Models\ManagedEnvironment;
use App\Services\Graph\AssignmentFetcher;
use App\Services\Graph\AssignmentFilterResolver;
use App\Services\Graph\GroupResolver;
use App\Services\Graph\ScopeTagResolver;
use App\Services\Providers\MicrosoftGraphOptionsResolver;
use App\Support\OpsUx\RunFailureSanitizer;
use App\Support\Providers\ProviderReasonCodes;
use Illuminate\Support\Facades\Log;

class AssignmentBackupService
{
    public function __construct(
        private readonly AssignmentFetcher $assignmentFetcher,
        private readonly GroupResolver $groupResolver,
        private readonly AssignmentFilterResolver $assignmentFilterResolver,
        private readonly ScopeTagResolver $scopeTagResolver,
        private readonly MicrosoftGraphOptionsResolver $graphOptionsResolver,
        private readonly OperationRunService $operationRunService,
    ) {}

    /**
     * Enrich a backup item with assignments and scope tag metadata.
     *
     * @param  BackupItem  $backupItem  The backup item to enrich
     * @param  ManagedEnvironment  $tenant  ManagedEnvironment model with credentials
     * @param  string  $policyType  Policy type key (e.g. deviceConfiguration)
     * @param  string  $policyId  Policy ID (external_id from Graph)
     * @param  array  $policyPayload  Full policy payload from Graph
     * @param  bool  $includeAssignments  Whether to fetch and include assignments
     * @return BackupItem Updated backup item with assignments and metadata
     */
    public function enrichWithAssignments(
        BackupItem $backupItem,
        ManagedEnvironment $tenant,
        string $policyType,
        string $policyId,
        array $policyPayload,
        bool $includeAssignments = false
    ): BackupItem {
        // Extract scope tags from payload (always available in policy)
        $scopeTagIds = $policyPayload['roleScopeTagIds'] ?? ['0'];
        $scopeTagNames = $this->resolveScopeTagNames($scopeTagIds, $tenant);

        $metadata = $backupItem->metadata ?? [];
        $metadata['scope_tag_ids'] = $scopeTagIds;
        $metadata['scope_tag_names'] = $scopeTagNames;

        // Only fetch assignments if explicitly requested
        if (! $includeAssignments) {
            $metadata['assignment_count'] = 0;
            $backupItem->update([
                'assignments' => null,
                'metadata' => $metadata,
            ]);

            return $backupItem->refresh();
        }

        if (! $this->assignmentFetcher->supportsStandardAssignments($policyType, $policyPayload['@odata.type'] ?? null)) {
            unset(
                $metadata['assignments_fetch_error'],
                $metadata['assignments_fetch_error_code'],
            );

            $metadata['assignment_count'] = 0;
            $metadata['assignments_fetch_failed'] = false;
            $metadata['assignments_not_applicable'] = true;
            $metadata['assignment_capture_reason'] = 'separate_role_assignments';
            $metadata['has_orphaned_assignments'] = false;

            $backupItem->update([
                'assignments' => [],
                'metadata' => $metadata,
            ]);

            $this->recordFetchOperationRun($backupItem, $tenant, $metadata);

            return $backupItem->refresh();
        }

        // Fetch assignments from Graph API
        $graphOptions = $this->graphOptionsResolver->resolveForTenant($tenant);
        $tenantId = (string) ($graphOptions['tenant'] ?? '');
        $assignments = $this->assignmentFetcher->fetch(
            $policyType,
            $tenantId,
            $policyId,
            $graphOptions,
            false,
            $policyPayload['@odata.type'] ?? null,
        );

        if (empty($assignments)) {
            // No assignments or fetch failed
            $metadata['assignment_count'] = 0;
            $metadata['assignments_fetch_failed'] = true;
            $metadata['has_orphaned_assignments'] = false;

            $backupItem->update([
                'assignments' => [], // Return empty array instead of null
                'metadata' => $metadata,
            ]);

            $this->recordFetchOperationRun($backupItem, $tenant, $metadata);

            Log::warning('No assignments fetched for policy', [
                'managed_environment_id' => $tenantId,
                'policy_id' => $policyId,
                'backup_item_id' => $backupItem->id,
            ]);

            return $backupItem->refresh();
        }

        // Extract group IDs and resolve for orphan detection
        $groupIds = $this->extractGroupIds($assignments);
        $resolvedGroups = [];
        $hasOrphanedGroups = false;

        if (! empty($groupIds)) {
            $resolvedGroups = $this->groupResolver->resolveGroupIds($groupIds, $tenantId, $graphOptions);
            $hasOrphanedGroups = collect($resolvedGroups)
                ->contains(fn (array $group) => $group['orphaned'] ?? false);
        }

        $filterIds = $this->extractAssignmentFilterIds($assignments);

        $filters = $this->assignmentFilterResolver->resolve($filterIds, $tenant);
        $filterNames = collect($filters)
            ->pluck('displayName', 'id')
            ->all();

        $assignments = $this->enrichAssignments($assignments, $resolvedGroups, $filterNames);

        // Update backup item with assignments and metadata
        $metadata['assignment_count'] = count($assignments);
        $metadata['assignments_fetch_failed'] = false;
        $metadata['has_orphaned_assignments'] = $hasOrphanedGroups;

        $backupItem->update([
            'assignments' => $assignments,
            'metadata' => $metadata,
        ]);

        $this->recordFetchOperationRun($backupItem, $tenant, $metadata);

        Log::info('Assignments enriched for backup item', [
            'managed_environment_id' => $tenantId,
            'policy_id' => $policyId,
            'backup_item_id' => $backupItem->id,
            'assignment_count' => count($assignments),
            'has_orphaned' => $hasOrphanedGroups,
        ]);

        return $backupItem->refresh();
    }

    /**
     * @param  array<string, mixed>  $captureMetadata
     */
    public function recordFetchOperationRun(BackupItem $backupItem, ManagedEnvironment $tenant, array $captureMetadata = []): void
    {
        $run = $this->operationRunService->ensureRunWithIdentity(
            tenant: $tenant,
            type: 'assignments.fetch',
            identityInputs: [
                'backup_item_id' => (int) $backupItem->getKey(),
            ],
            context: [
                'backup_set_id' => (int) $backupItem->backup_set_id,
                'backup_item_id' => (int) $backupItem->getKey(),
                'policy_id' => is_numeric($backupItem->policy_id) ? (int) $backupItem->policy_id : null,
                'policy_identifier' => (string) $backupItem->policy_identifier,
            ],
        );

        if ($run->status === 'completed') {
            return;
        }

        $this->operationRunService->updateRun($run, 'running');

        $fetchFailed = (bool) ($captureMetadata['assignments_fetch_failed'] ?? false);

        $reasonCandidate = $captureMetadata['assignments_fetch_error_code']
            ?? $captureMetadata['assignments_fetch_error']
            ?? ProviderReasonCodes::UnknownError;

        $reasonCode = RunFailureSanitizer::normalizeReasonCode(
            $this->normalizeReasonCandidate($reasonCandidate)
        );

        $this->operationRunService->updateRun(
            $run,
            status: 'completed',
            outcome: $fetchFailed ? 'failed' : 'succeeded',
            summaryCounts: [
                'total' => 1,
                'processed' => $fetchFailed ? 0 : 1,
                'failed' => $fetchFailed ? 1 : 0,
            ],
            failures: $fetchFailed
                ? [[
                    'code' => 'assignments.fetch_failed',
                    'reason_code' => $reasonCode,
                    'message' => (string) ($captureMetadata['assignments_fetch_error'] ?? 'Assignments fetch failed'),
                ]]
                : [],
        );
    }

    /**
     * Resolve scope tag IDs to display names.
     */
    private function resolveScopeTagNames(array $scopeTagIds, ManagedEnvironment $tenant): array
    {
        $scopeTags = $this->scopeTagResolver->resolve($scopeTagIds, $tenant);

        $names = [];
        foreach ($scopeTagIds as $id) {
            $scopeTag = collect($scopeTags)->firstWhere('id', $id);
            $names[] = $scopeTag['displayName'] ?? "Unknown (ID: {$id})";
        }

        return $names;
    }

    /**
     * Extract group IDs from assignment array.
     */
    private function extractGroupIds(array $assignments): array
    {
        $groupIds = [];

        foreach ($assignments as $assignment) {
            $target = $assignment['target'] ?? [];
            $odataType = $target['@odata.type'] ?? '';

            if (in_array($odataType, [
                '#microsoft.graph.groupAssignmentTarget',
                '#microsoft.graph.exclusionGroupAssignmentTarget',
            ], true) && isset($target['groupId'])) {
                $groupIds[] = $target['groupId'];
            }
        }

        return array_unique($groupIds);
    }

    /**
     * @param  array<int, array<string, mixed>>  $assignments
     * @param  array<string, array{id:string,displayName:?string,orphaned:bool}>  $groups
     * @param  array<string, string>  $filterNames
     * @return array<int, array<string, mixed>>
     */
    private function enrichAssignments(array $assignments, array $groups, array $filterNames): array
    {
        return array_map(function (array $assignment) use ($groups, $filterNames): array {
            $target = $assignment['target'] ?? [];
            $groupId = $target['groupId'] ?? null;

            if ($groupId && isset($groups[$groupId])) {
                $target['group_display_name'] = $groups[$groupId]['displayName'] ?? null;
                $target['group_orphaned'] = $groups[$groupId]['orphaned'] ?? false;
            }

            $filterId = $assignment['deviceAndAppManagementAssignmentFilterId']
                ?? ($target['deviceAndAppManagementAssignmentFilterId'] ?? null);
            $filterType = $assignment['deviceAndAppManagementAssignmentFilterType']
                ?? ($target['deviceAndAppManagementAssignmentFilterType'] ?? null);

            if ($filterId) {
                $target['deviceAndAppManagementAssignmentFilterId'] = $target['deviceAndAppManagementAssignmentFilterId'] ?? $filterId;

                if ($filterType) {
                    $target['deviceAndAppManagementAssignmentFilterType'] = $target['deviceAndAppManagementAssignmentFilterType'] ?? $filterType;
                }

                if (isset($filterNames[$filterId])) {
                    $target['assignment_filter_name'] = $filterNames[$filterId];
                }
            }

            $assignment['target'] = $target;

            return $assignment;
        }, $assignments);
    }

    /**
     * @param  array<int, array<string, mixed>>  $assignments
     * @return array<int, string>
     */
    private function extractAssignmentFilterIds(array $assignments): array
    {
        $filterIds = [];

        foreach ($assignments as $assignment) {
            if (! is_array($assignment)) {
                continue;
            }

            $filterId = $assignment['deviceAndAppManagementAssignmentFilterId']
                ?? ($assignment['target']['deviceAndAppManagementAssignmentFilterId'] ?? null);

            if (is_string($filterId) && $filterId !== '') {
                $filterIds[] = $filterId;
            }
        }

        return array_values(array_unique($filterIds));
    }

    private function normalizeReasonCandidate(mixed $candidate): string
    {
        if (! is_string($candidate) && ! is_numeric($candidate)) {
            return ProviderReasonCodes::UnknownError;
        }

        $raw = trim((string) $candidate);

        if ($raw === '') {
            return ProviderReasonCodes::UnknownError;
        }

        $raw = preg_replace('/(?<!^)[A-Z]/', '_$0', $raw) ?? $raw;
        $raw = strtolower($raw);
        $raw = str_replace([' ', '-', '.', '/'], '_', $raw);
        $raw = preg_replace('/_+/', '_', $raw) ?? $raw;

        return trim($raw, '_');
    }
}
