<?php

namespace App\Services\Graph;

use Illuminate\Support\Facades\Log;

class AssignmentFetcher
{
    public function __construct(
        private readonly GraphClientInterface $graphClient,
        private readonly GraphContractRegistry $contracts,
    ) {}

    /**
     * Fetch policy assignments with fallback strategy.
     *
     * Primary: GET {assignments_list_path}
     * Fallback: GET {resource}?$expand=assignments&$filter=id eq '{id}'
     *
     * @return array Returns assignment array or empty array on failure
     */
    public function fetch(
        string $policyType,
        string $tenantId,
        string $policyId,
        array $options = [],
        bool $throwOnFailure = false,
        ?string $policyOdataType = null,
    ): array {
        if (! $this->supportsStandardAssignments($policyType, $policyOdataType)) {
            Log::debug('Standard assignment fetch is not applicable for policy type', [
                'managed_environment_id' => $tenantId,
                'policy_type' => $policyType,
                'policy_id' => $policyId,
            ]);

            return [];
        }

        $contract = $this->contracts->get($policyType);
        $listPathTemplate = $contract['assignments_list_path'] ?? null;
        $resource = $contract['resource'] ?? null;
        $requestOptions = array_merge($options, ['tenant' => $tenantId]);
        $context = [
            'managed_environment_id' => $tenantId,
            'policy_type' => $policyType,
            'policy_id' => $policyId,
        ];

        $primaryException = null;
        $assignments = [];
        $lastSuccessfulAssignments = null;

        // Try primary endpoint(s)
        $listPathTemplates = [];

        if ($policyType === 'appProtectionPolicy') {
            $derivedTemplate = $this->resolveAppProtectionAssignmentsListTemplate($policyOdataType);

            if ($derivedTemplate !== null) {
                $listPathTemplates[] = $derivedTemplate;
            }
        }

        if (is_string($listPathTemplate) && $listPathTemplate !== '' && ! in_array($listPathTemplate, $listPathTemplates, true)) {
            $listPathTemplates[] = $listPathTemplate;
        }

        foreach ($listPathTemplates as $template) {
            try {
                $assignments = $this->fetchPrimary(
                    $template,
                    $policyId,
                    $requestOptions,
                    $context,
                    $throwOnFailure
                );

                if ($assignments === null) {
                    continue;
                }

                $lastSuccessfulAssignments = $assignments;

                if (! empty($assignments)) {
                    Log::debug('Fetched assignments via primary endpoint', [
                        'managed_environment_id' => $tenantId,
                        'policy_type' => $policyType,
                        'policy_id' => $policyId,
                        'count' => count($assignments),
                    ]);

                    return $assignments;
                }

                if ($policyType !== 'appProtectionPolicy') {
                    // Empty is a valid outcome (policy not assigned). Do not attempt fallback.
                    return [];
                }
            } catch (GraphException $e) {
                $primaryException = $primaryException ?? $e;
            }
        }

        if ($lastSuccessfulAssignments !== null && $policyType === 'appProtectionPolicy') {
            Log::debug('Assignments fetched via primary endpoint(s)', [
                'managed_environment_id' => $tenantId,
                'policy_type' => $policyType,
                'policy_id' => $policyId,
                'count' => count($lastSuccessfulAssignments),
            ]);

            return $lastSuccessfulAssignments;
        }

        // Try fallback with $expand
        Log::debug('Primary endpoint returned empty, trying fallback', [
            'managed_environment_id' => $tenantId,
            'policy_type' => $policyType,
            'policy_id' => $policyId,
        ]);

        if (! is_string($resource) || $resource === '') {
            Log::debug('Assignments resource not configured for policy type', [
                'managed_environment_id' => $tenantId,
                'policy_type' => $policyType,
                'policy_id' => $policyId,
            ]);

            if ($throwOnFailure && $primaryException) {
                Log::warning('Failed to fetch assignments', [
                    'managed_environment_id' => $tenantId,
                    'policy_type' => $policyType,
                    'policy_id' => $policyId,
                    'error' => $primaryException->getMessage(),
                    'context' => $primaryException->context,
                ]);

                throw $primaryException;
            }

            return [];
        }

        if ($policyType === 'appProtectionPolicy') {
            if ($throwOnFailure && $primaryException) {
                throw $primaryException;
            }

            return $assignments;
        }

        $fallbackException = null;

        try {
            $assignments = $this->fetchWithExpand(
                $resource,
                $policyId,
                $requestOptions,
                $context,
                $throwOnFailure
            );
        } catch (GraphException $e) {
            $fallbackException = $e;
        }

        if (! empty($assignments)) {
            Log::debug('Fetched assignments via fallback endpoint', [
                'managed_environment_id' => $tenantId,
                'policy_type' => $policyType,
                'policy_id' => $policyId,
                'count' => count($assignments),
            ]);

            return $assignments;
        }

        // Both methods returned empty
        Log::debug('No assignments found for policy', [
            'managed_environment_id' => $tenantId,
            'policy_type' => $policyType,
            'policy_id' => $policyId,
        ]);

        if ($throwOnFailure && ($fallbackException || $primaryException)) {
            $exception = $fallbackException ?? $primaryException;

            Log::warning('Failed to fetch assignments', [
                'managed_environment_id' => $tenantId,
                'policy_type' => $policyType,
                'policy_id' => $policyId,
                'error' => $exception->getMessage(),
                'context' => $exception->context,
            ]);

            throw $exception;
        }

        return [];
    }

    public function supportsStandardAssignments(string $policyType, ?string $policyOdataType = null): bool
    {
        $contract = $this->contracts->get($policyType);
        $listPathTemplate = $contract['assignments_list_path'] ?? null;

        if (is_string($listPathTemplate) && $listPathTemplate !== '') {
            return true;
        }

        if ($policyType === 'appProtectionPolicy' && $this->resolveAppProtectionAssignmentsListTemplate($policyOdataType) !== null) {
            return true;
        }

        $allowedExpand = $contract['allowed_expand'] ?? [];

        return is_array($allowedExpand) && in_array('assignments', $allowedExpand, true);
    }

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

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

        return "/deviceAppManagement/{$entitySet}/{id}/assignments";
    }

    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,
        };
    }

    /**
     * Fetch assignments using primary endpoint.
     */
    private function fetchPrimary(
        ?string $listPathTemplate,
        string $policyId,
        array $options,
        array $context,
        bool $throwOnFailure
    ): ?array {
        if (! is_string($listPathTemplate) || $listPathTemplate === '') {
            return null;
        }

        $path = $this->resolvePath($listPathTemplate, $policyId);

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

        $response = $this->graphClient->request('GET', $path, $options);

        if ($response->failed()) {
            $this->logAssignmentFailure('primary', $response, $context + ['path' => $path]);

            if ($throwOnFailure) {
                throw new GraphException(
                    $this->resolveErrorMessage($response),
                    $response->status,
                    $context + ['path' => $path]
                );
            }

            return null;
        }

        return $response->data['value'] ?? [];
    }

    /**
     * Fetch assignments using $expand fallback.
     */
    private function fetchWithExpand(
        string $resource,
        string $policyId,
        array $options,
        array $context,
        bool $throwOnFailure
    ): array {
        $path = $resource;
        $params = [
            '$expand' => 'assignments',
            '$filter' => "id eq '{$policyId}'",
        ];

        $response = $this->graphClient->request('GET', $path, array_merge($options, [
            'query' => $params,
        ]));

        if ($response->failed()) {
            $this->logAssignmentFailure('fallback', $response, $context + ['path' => $path]);

            if ($throwOnFailure) {
                throw new GraphException(
                    $this->resolveErrorMessage($response),
                    $response->status,
                    $context + ['path' => $path]
                );
            }

            return [];
        }

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

        if (empty($policies)) {
            return [];
        }

        return $policies[0]['assignments'] ?? [];
    }

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

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

    private function resolveErrorMessage(GraphResponse $response): string
    {
        $error = $response->errors[0] ?? null;

        if (is_array($error)) {
            if (isset($error['message']) && is_string($error['message'])) {
                return $error['message'];
            }

            return json_encode($error, JSON_UNESCAPED_SLASHES) ?: 'Graph request failed';
        }

        if (is_string($error) && $error !== '') {
            return $error;
        }

        return 'Graph request failed';
    }

    private function logAssignmentFailure(string $stage, GraphResponse $response, array $context): void
    {
        Log::warning('Assignment fetch failed', $context + [
            'stage' => $stage,
            'status' => $response->status,
            'errors' => $response->errors,
        ]);
    }
}
