<?php

namespace App\Services;

use App\Models\RestoreRun;
use App\Models\ManagedEnvironment;
use App\Services\Graph\AssignmentFilterResolver;
use App\Services\Graph\GraphClientInterface;
use App\Services\Graph\GraphContractRegistry;
use App\Services\Graph\GraphLogger;
use App\Services\Graph\GraphResponse;
use App\Services\Providers\MicrosoftGraphOptionsResolver;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Log;

class AssignmentRestoreService
{
    public function __construct(
        private readonly GraphClientInterface $graphClient,
        private readonly GraphContractRegistry $contracts,
        private readonly GraphLogger $graphLogger,
        private readonly AssignmentFilterResolver $assignmentFilterResolver,
        private readonly MicrosoftGraphOptionsResolver $graphOptionsResolver,
    ) {}

    /**
     * @param  array<int, array<string, mixed>>  $assignments
     * @param  array<string, string>  $groupMapping
     * @param  array<string, array<string, string>>  $foundationMapping
     * @return array{outcomes: array<int, array<string, mixed>>, summary: array{success:int,failed:int,skipped:int}}
     */
    public function restore(
        ManagedEnvironment $tenant,
        string $policyType,
        string $policyId,
        array $assignments,
        array $groupMapping,
        array $foundationMapping = [],
        ?RestoreRun $restoreRun = null,
        ?string $actorEmail = null,
        ?string $actorName = null,
        ?string $policyOdataType = null,
    ): array {
        $outcomes = [];
        $summary = [
            'success' => 0,
            'failed' => 0,
            'skipped' => 0,
        ];

        if ($assignments === []) {
            return [
                'outcomes' => $outcomes,
                'summary' => $summary,
            ];
        }

        $contract = $this->contracts->get($policyType);
        $createPath = $this->resolvePath($contract['assignments_create_path'] ?? null, $policyId);
        $createMethod = strtoupper((string) ($contract['assignments_create_method'] ?? 'POST'));

        if ($policyType === 'appProtectionPolicy') {
            $derivedAssignPath = $this->resolveAppProtectionAssignmentsCreatePath($policyId, $policyOdataType);

            if ($derivedAssignPath !== null) {
                $createPath = $derivedAssignPath;
            }
        }

        $usesAssignAction = is_string($createPath) && str_ends_with($createPath, '/assign');
        $assignmentsPayloadKey = $contract['assignments_payload_key'] ?? 'assignments';

        if (! is_string($assignmentsPayloadKey) || $assignmentsPayloadKey === '') {
            $assignmentsPayloadKey = 'assignments';
        }
        $listPath = $this->resolvePath($contract['assignments_list_path'] ?? null, $policyId);
        $deletePathTemplate = $contract['assignments_delete_path'] ?? null;

        if (! $createPath || (! $usesAssignAction && (! $listPath || ! $deletePathTemplate))) {
            $outcomes[] = $this->failureOutcome(null, 'Assignments endpoints are not configured for this policy type.');
            $summary['failed']++;

            return [
                'outcomes' => $outcomes,
                'summary' => $summary,
            ];
        }

        $graphOptions = $this->graphOptionsResolver->resolveForTenant($tenant);
        $tenantIdentifier = (string) ($graphOptions['tenant'] ?? '');

        $context = [
            'tenant' => $tenantIdentifier,
            'policy_id' => $policyId,
            'policy_type' => $policyType,
            'restore_run_id' => $restoreRun?->id,
        ];

        $preparedAssignments = [];
        $preparedMeta = [];

        $assignmentFilterMapping = $foundationMapping['assignmentFilter'] ?? [];

        if ($assignmentFilterMapping === []) {
            $filterIds = $this->extractAssignmentFilterIds($assignments);

            if ($filterIds !== []) {
                $resolvedFilters = $this->assignmentFilterResolver->resolve($filterIds, $tenant);

                foreach ($resolvedFilters as $filter) {
                    $filterId = $filter['id'] ?? null;

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

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

            $target = $assignment['target'] ?? [];
            $filterId = $assignment['deviceAndAppManagementAssignmentFilterId']
                ?? ($target['deviceAndAppManagementAssignmentFilterId'] ?? null);
            $filterLocation = array_key_exists('deviceAndAppManagementAssignmentFilterId', $assignment) ? 'root' : 'target';

            if (! is_string($filterId) && ! is_int($filterId)) {
                $filterId = null;
            }

            if (is_string($filterId) && $filterId === '') {
                $filterId = null;
            }

            if ($filterId !== null) {
                if ($assignmentFilterMapping === []) {
                    $outcomes[] = $this->skipOutcome($assignment, null, null, 'Assignment filter mapping is unavailable.');
                    $summary['skipped']++;

                    continue;
                }

                $mappedFilterId = $assignmentFilterMapping[$filterId] ?? null;

                if ($mappedFilterId === null) {
                    $outcomes[] = $this->skipOutcome(
                        $assignment,
                        null,
                        null,
                        'Assignment filter mapping missing for filter ID.'
                    );
                    $summary['skipped']++;

                    continue;
                }

                if ($filterLocation === 'root') {
                    $assignment['deviceAndAppManagementAssignmentFilterId'] = $mappedFilterId;
                } else {
                    $target['deviceAndAppManagementAssignmentFilterId'] = $mappedFilterId;
                    $assignment['target'] = $target;
                }
            }

            $groupId = $assignment['target']['groupId'] ?? null;
            $mappedGroupId = $groupId && isset($groupMapping[$groupId]) ? $groupMapping[$groupId] : null;

            if ($mappedGroupId === 'SKIP') {
                $outcomes[] = $this->skipOutcome($assignment, $groupId, $mappedGroupId);
                $summary['skipped']++;

                continue;
            }

            $assignmentToRestore = $this->applyGroupMapping($assignment, $mappedGroupId);
            $assignmentToRestore = $this->sanitizeAssignment($assignmentToRestore);

            $preparedAssignments[] = $assignmentToRestore;
            $preparedMeta[] = [
                'assignment' => $assignment,
                'group_id' => $groupId,
                'mapped_group_id' => $mappedGroupId,
            ];
        }

        if ($preparedAssignments === []) {
            return [
                'outcomes' => $outcomes,
                'summary' => $summary,
            ];
        }

        if ($usesAssignAction) {
            $this->graphLogger->logRequest('restore_assignments_assign', $context + [
                'method' => $createMethod,
                'endpoint' => $createPath,
                'assignments' => count($preparedAssignments),
            ]);

            $assignResponse = $this->graphClient->request($createMethod, $createPath, [
                'json' => [$assignmentsPayloadKey => $preparedAssignments],
            ] + $graphOptions);

            $this->graphLogger->logResponse('restore_assignments_assign', $assignResponse, $context + [
                'method' => $createMethod,
                'endpoint' => $createPath,
                'assignments' => count($preparedAssignments),
            ]);

            if ($assignResponse->successful()) {
                foreach ($preparedMeta as $meta) {
                    $outcomes[] = $this->successOutcome(
                        $meta['assignment'],
                        $meta['group_id'],
                        $meta['mapped_group_id']
                    );
                    $summary['success']++;
                }
            } else {
                $reason = $assignResponse->meta['error_message'] ?? 'Graph assign failed';

                if ($preparedMeta === []) {
                    $outcomes[] = $this->failureOutcome(null, $reason, null, null, $assignResponse);
                    $summary['failed']++;
                }

                foreach ($preparedMeta as $meta) {
                    $outcomes[] = $this->failureOutcome(
                        $meta['assignment'],
                        $reason,
                        $meta['group_id'],
                        $meta['mapped_group_id'],
                        $assignResponse
                    );
                    $summary['failed']++;
                }
            }

            return [
                'outcomes' => $outcomes,
                'summary' => $summary,
            ];
        }

        $this->graphLogger->logRequest('restore_assignments_list', $context + [
            'method' => 'GET',
            'endpoint' => $listPath,
        ]);

        $response = $this->graphClient->request('GET', $listPath, $graphOptions);

        $this->graphLogger->logResponse('restore_assignments_list', $response, $context + [
            'method' => 'GET',
            'endpoint' => $listPath,
        ]);

        $existingAssignments = $response->data['value'] ?? [];

        foreach ($existingAssignments as $existing) {
            $assignmentId = $existing['id'] ?? null;

            if (! is_string($assignmentId) || $assignmentId === '') {
                continue;
            }

            $deletePath = $this->resolvePath($deletePathTemplate, $policyId, $assignmentId);

            if (! $deletePath) {
                continue;
            }

            $this->graphLogger->logRequest('restore_assignments_delete', $context + [
                'method' => 'DELETE',
                'endpoint' => $deletePath,
                'assignment_id' => $assignmentId,
            ]);

            $deleteResponse = $this->graphClient->request('DELETE', $deletePath, $graphOptions);

            $this->graphLogger->logResponse('restore_assignments_delete', $deleteResponse, $context + [
                'method' => 'DELETE',
                'endpoint' => $deletePath,
                'assignment_id' => $assignmentId,
            ]);

            if ($deleteResponse->failed()) {
                Log::warning('Failed to delete existing assignment during restore', $context + [
                    'assignment_id' => $assignmentId,
                    'graph_error_message' => $deleteResponse->meta['error_message'] ?? null,
                    'graph_error_code' => $deleteResponse->meta['error_code'] ?? null,
                ]);
            }
        }

        foreach ($preparedMeta as $index => $meta) {
            $assignmentToRestore = $preparedAssignments[$index] ?? null;

            if (! is_array($assignmentToRestore)) {
                continue;
            }

            $this->graphLogger->logRequest('restore_assignments_create', $context + [
                'method' => $createMethod,
                'endpoint' => $createPath,
                'group_id' => $meta['group_id'],
                'mapped_group_id' => $meta['mapped_group_id'],
            ]);

            $createResponse = $this->graphClient->request($createMethod, $createPath, [
                'json' => $assignmentToRestore,
            ] + $graphOptions);

            $this->graphLogger->logResponse('restore_assignments_create', $createResponse, $context + [
                'method' => $createMethod,
                'endpoint' => $createPath,
                'group_id' => $meta['group_id'],
                'mapped_group_id' => $meta['mapped_group_id'],
            ]);

            if ($createResponse->successful()) {
                $outcomes[] = $this->successOutcome($meta['assignment'], $meta['group_id'], $meta['mapped_group_id']);
                $summary['success']++;
            } else {
                $outcomes[] = $this->failureOutcome(
                    $meta['assignment'],
                    $createResponse->meta['error_message'] ?? 'Graph create failed',
                    $meta['group_id'],
                    $meta['mapped_group_id'],
                    $createResponse
                );
                $summary['failed']++;
            }

            usleep(100000);
        }

        return [
            'outcomes' => $outcomes,
            'summary' => $summary,
        ];
    }

    private function resolveAppProtectionAssignmentsCreatePath(string $policyId, ?string $odataType): ?string
    {
        $entitySet = $this->resolveAppProtectionEntitySet($odataType);

        if ($entitySet === null) {
            return null;
        }

        return $this->resolvePath("/deviceAppManagement/{$entitySet}/{id}/assign", $policyId);
    }

    private function resolveAppProtectionEntitySet(?string $odataType): ?string
    {
        if (! is_string($odataType) || $odataType === '') {
            return null;
        }

        return match (strtolower($odataType)) {
            '#microsoft.graph.androidmanagedappprotection' => 'androidManagedAppProtections',
            '#microsoft.graph.iosmanagedappprotection' => 'iosManagedAppProtections',
            '#microsoft.graph.windowsinformationprotectionpolicy' => 'windowsInformationProtectionPolicies',
            '#microsoft.graph.mdmwindowsinformationprotectionpolicy' => 'mdmWindowsInformationProtectionPolicies',
            '#microsoft.graph.targetedmanagedappprotection' => 'targetedManagedAppProtections',
            default => null,
        };
    }

    private function resolvePath(?string $template, string $policyId, ?string $assignmentId = null): ?string
    {
        if (! is_string($template) || $template === '') {
            return null;
        }

        $path = str_replace('{id}', urlencode($policyId), $template);

        if ($assignmentId !== null) {
            $path = str_replace('{assignmentId}', urlencode($assignmentId), $path);
        }

        return $path;
    }

    /**
     * @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) || is_int($filterId)) {
                $filterId = (string) $filterId;

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

        return array_values(array_unique($filterIds));
    }

    private function applyGroupMapping(array $assignment, ?string $mappedGroupId): array
    {
        if (! $mappedGroupId) {
            return $assignment;
        }

        $target = $assignment['target'] ?? [];
        $odataType = $target['@odata.type'] ?? '';

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

        return $assignment;
    }

    private function sanitizeAssignment(array $assignment): array
    {
        $assignment = Arr::except($assignment, ['id']);
        $target = $assignment['target'] ?? [];

        unset(
            $target['group_display_name'],
            $target['group_orphaned'],
            $target['assignment_filter_name']
        );

        $assignment['target'] = $target;

        return $assignment;
    }

    private function successOutcome(array $assignment, ?string $groupId, ?string $mappedGroupId): array
    {
        return [
            'status' => 'success',
            'assignment' => $this->sanitizeAssignment($assignment),
            'group_id' => $groupId,
            'mapped_group_id' => $mappedGroupId,
        ];
    }

    private function skipOutcome(
        array $assignment,
        ?string $groupId,
        ?string $mappedGroupId,
        ?string $reason = null
    ): array {
        return [
            'status' => 'skipped',
            'assignment' => $this->sanitizeAssignment($assignment),
            'group_id' => $groupId,
            'mapped_group_id' => $mappedGroupId,
            'reason' => $reason,
        ];
    }

    private function failureOutcome(
        ?array $assignment,
        string $reason,
        ?string $groupId = null,
        ?string $mappedGroupId = null,
        ?GraphResponse $response = null
    ): array {
        return array_filter([
            'status' => 'failed',
            'assignment' => $assignment ? $this->sanitizeAssignment($assignment) : null,
            'group_id' => $groupId,
            'mapped_group_id' => $mappedGroupId,
            'reason' => $reason,
            'graph_error_message' => $response?->meta['error_message'] ?? null,
            'graph_error_code' => $response?->meta['error_code'] ?? null,
            'graph_request_id' => $response?->meta['request_id'] ?? null,
            'graph_client_request_id' => $response?->meta['client_request_id'] ?? null,
        ], static fn ($value) => $value !== null);
    }
}
