<?php

namespace App\Services\Intune;

use App\Models\ManagedEnvironment;
use App\Models\ManagedEnvironmentPermission;
use App\Services\Graph\GraphClientInterface;
use App\Services\Providers\MicrosoftGraphOptionsResolver;
use DateTimeInterface;
use Illuminate\Support\Carbon;

class ManagedEnvironmentPermissionService
{
    public function __construct(
        private readonly GraphClientInterface $graphClient,
        private readonly MicrosoftGraphOptionsResolver $graphOptionsResolver,
    ) {}

    /**
     * @return array<int, array{key:string,type:string,description:?string,features:array<int,string>}>
     */
    public function getRequiredPermissions(): array
    {
        return array_merge(
            config('intune_permissions.permissions', []),
            config('entra_permissions.permissions', []),
        );
    }

    /**
     * @return array<string, array{status:string,details:array<string,mixed>|null,last_checked_at:?\Illuminate\Support\Carbon}>
     */
    public function getGrantedPermissions(ManagedEnvironment $tenant): array
    {
        return ManagedEnvironmentPermission::query()
            ->where('managed_environment_id', $tenant->id)
            ->get()
            ->keyBy('permission_key')
            ->map(fn (ManagedEnvironmentPermission $permission) => [
                'status' => $permission->status,
                'details' => $permission->details,
                'last_checked_at' => $permission->last_checked_at,
            ])
            ->all();
    }

    /**
     * @param  array<string, array{status:string,details?:array<string,mixed>|null}|string>|null  $grantedStatuses
     * @param  bool  $persist  Persist comparison results to managed_environment_permissions
     * @param  bool  $liveCheck  If true, fetch actual permissions from Graph API
     * @param  bool  $useConfiguredStub  Include configured stub permissions when no live check is used
     * @param  array{tenant?:string|null,client_id?:string|null,client_secret?:string|null,client_request_id?:string|null}|null  $graphOptions
     * @return array{
     *   overall_status:string,
     *   permissions:array<int,array{key:string,type:string,description:?string,features:array<int,string>,status:string,details:array<string,mixed>|null}>,
     *   last_refreshed_at:?string,
     *   live_check?: array{attempted:bool,succeeded:bool,http_status:?int,reason_code:?string}
     * }
     */
    public function compare(
        ManagedEnvironment $tenant,
        ?array $grantedStatuses = null,
        bool $persist = true,
        bool $liveCheck = false,
        bool $useConfiguredStub = true,
        ?array $graphOptions = null,
    ): array {
        $required = $this->getRequiredPermissions();
        $liveCheckMeta = [
            'attempted' => false,
            'succeeded' => false,
            'http_status' => null,
            'reason_code' => null,
        ];

        $liveCheckFailed = false;
        $liveCheckDetails = null;

        // If liveCheck is requested, fetch actual permissions from Graph
        if ($liveCheck && $grantedStatuses === null) {
            $liveCheckMeta['attempted'] = true;

            if (! is_array($graphOptions)) {
                $graphOptions = $this->graphOptionsResolver->resolveForTenant($tenant);
            }

            $appId = null;

            if (is_string($graphOptions['client_id'] ?? null) && $graphOptions['client_id'] !== '') {
                $appId = (string) $graphOptions['client_id'];
            }

            if ($appId !== null) {
                $liveCheckMeta['app_id'] = $appId;
            }

            $grantedStatuses = $this->fetchLivePermissions($tenant, $graphOptions);

            if (isset($grantedStatuses['__error'])) {
                $liveCheckFailed = true;
                $liveCheckError = is_array($grantedStatuses['__error'] ?? null) ? $grantedStatuses['__error'] : null;
                $liveCheckDetails = is_array($liveCheckError['details'] ?? null)
                    ? $liveCheckError['details']
                    : (is_array($liveCheckError) ? $liveCheckError : null);

                $httpStatus = $liveCheckDetails['status'] ?? null;
                $liveCheckMeta['http_status'] = is_int($httpStatus) ? $httpStatus : null;
                $liveCheckMeta['reason_code'] = $this->deriveLiveCheckReasonCode(
                    $liveCheckMeta['http_status'],
                    is_array($liveCheckDetails) ? $liveCheckDetails : null,
                );

                unset($grantedStatuses['__error']);
                $grantedStatuses = null;
            } else {
                $observedCount = is_array($grantedStatuses) ? count($grantedStatuses) : 0;
                $liveCheckMeta['observed_permissions_count'] = $observedCount;

                if ($observedCount === 0) {
                    // Enterprise-safe: if the live refresh produced an empty inventory, treat it as non-fresh.
                    // This prevents false "missing" findings due to partial/misconfigured verification context.
                    $liveCheckMeta['succeeded'] = false;
                    $liveCheckMeta['reason_code'] = 'permissions_inventory_empty';
                    $grantedStatuses = null;
                } else {
                    $liveCheckMeta['succeeded'] = true;
                    $liveCheckMeta['reason_code'] = 'ok';
                }
            }
        }

        $storedStatuses = $this->getGrantedPermissions($tenant);

        if (! $useConfiguredStub) {
            $storedStatuses = $this->dropConfiguredStatuses($storedStatuses);
        }

        $granted = $this->normalizeGrantedStatuses(
            $grantedStatuses ?? array_replace_recursive(
                $useConfiguredStub && ! $liveCheck ? $this->configuredGrantedStatuses() : [],
                $storedStatuses
            )
        );
        $results = [];
        $hasMissing = false;
        $hasErrors = false;
        $checkedAt = now();
        $tenantWorkspaceId = $this->resolveTenantWorkspaceId($tenant);

        $canPersist = $persist && $tenantWorkspaceId !== null;

        if ($canPersist && $liveCheckMeta['attempted'] === true && $liveCheckMeta['succeeded'] === false) {
            // Enterprise-safe: never overwrite stored inventory when we could not refresh it.
            // When the failure is a deterministic misconfiguration (e.g. permission denied), persist an "error" snapshot
            // only if we have no stored inventory yet, so the UI can explain the failure.
            $reasonCode = is_string($liveCheckMeta['reason_code'] ?? null)
                ? (string) $liveCheckMeta['reason_code']
                : null;

            $shouldPersistErrorSnapshot = in_array($reasonCode, [
                'authentication_failed',
                'permission_denied',
            ], true);

            if (! $shouldPersistErrorSnapshot) {
                $canPersist = false;
            } else {
                $hasStoredStatuses = ManagedEnvironmentPermission::query()
                    ->where('managed_environment_id', $tenant->id)
                    ->exists();

                $canPersist = ! $hasStoredStatuses;
            }
        }

        foreach ($required as $permission) {
            $key = $permission['key'];
            $status = $liveCheckFailed
                ? 'error'
                : ($granted[$key]['status'] ?? 'missing');

            $details = $liveCheckFailed
                ? array_filter([
                    'source' => 'graph_api',
                    'status' => $liveCheckMeta['http_status'],
                    'reason_code' => $liveCheckMeta['reason_code'],
                    'message' => is_array($liveCheckDetails) ? ($liveCheckDetails['message'] ?? null) : null,
                ], fn (mixed $value): bool => $value !== null)
                : ($granted[$key]['details'] ?? null);

            if ($canPersist) {
                ManagedEnvironmentPermission::updateOrCreate(
                    [
                        'managed_environment_id' => $tenant->id,
                        'permission_key' => $key,
                        'workspace_id' => $tenantWorkspaceId,
                    ],
                    [
                        'status' => $status,
                        'details' => $details,
                        'last_checked_at' => $checkedAt,
                    ]
                );
            }

            $results[] = [
                'key' => $key,
                'type' => $permission['type'] ?? 'application',
                'description' => $permission['description'] ?? null,
                'features' => $permission['features'] ?? [],
                'status' => $status,
                'details' => $details,
            ];

            $hasMissing = $hasMissing || $status === 'missing';
            $hasErrors = $hasErrors || $status === 'error';
        }

        $overall = match (true) {
            $hasErrors => 'error',
            $hasMissing => 'missing',
            default => 'granted',
        };

        $payload = [
            'overall_status' => $overall,
            'permissions' => $results,
            'last_refreshed_at' => $this->lastRefreshedAtIso($tenant),
        ];

        if ($liveCheckMeta['attempted'] === true) {
            $payload['live_check'] = $liveCheckMeta;
        }

        return $payload;
    }

    /**
     * @param  array<string, mixed>|null  $details
     */
    private function deriveLiveCheckReasonCode(?int $httpStatus, ?array $details = null): string
    {
        if (is_array($details) && is_string($details['reason_code'] ?? null)) {
            return (string) $details['reason_code'];
        }

        return match (true) {
            $httpStatus === 401 => 'authentication_failed',
            $httpStatus === 403 => 'permission_denied',
            $httpStatus === 408 => 'dependency_unreachable',
            $httpStatus === 429 => 'throttled',
            is_int($httpStatus) && $httpStatus >= 500 => 'dependency_unreachable',
            is_int($httpStatus) && $httpStatus >= 400 => 'unknown_error',
            default => is_array($details) && is_string($details['message'] ?? null) ? 'dependency_unreachable' : 'unknown_error',
        };
    }

    /**
     * @param  array<string, array{status:string,details?:array<string,mixed>|null}|string>  $granted
     * @return array<string, array{status:string,details:array<string,mixed>|null}>
     */
    private function normalizeGrantedStatuses(array $granted): array
    {
        $normalized = [];

        foreach ($granted as $key => $value) {
            if (is_string($value)) {
                $normalized[$key] = ['status' => $value, 'details' => null];

                continue;
            }

            $normalized[$key] = [
                'status' => $value['status'] ?? 'missing',
                'details' => $value['details'] ?? null,
            ];
        }

        return $normalized;
    }

    /**
     * @param  array<string, array{status:string,details:array<string,mixed>|null,last_checked_at:?\Illuminate\Support\Carbon}>  $granted
     * @return array<string, array{status:string,details:array<string,mixed>|null,last_checked_at:?\Illuminate\Support\Carbon}>
     */
    private function dropConfiguredStatuses(array $granted): array
    {
        foreach ($granted as $key => $value) {
            $source = $value['details']['source'] ?? null;

            if ($source === 'configured') {
                unset($granted[$key]);
            }
        }

        return $granted;
    }

    /**
     * @return array<string, array{status:string,details:array<string,mixed>|null}>
     */
    public function configuredGrantedStatuses(): array
    {
        $configured = $this->configuredGrantedKeys();
        $normalized = [];

        foreach ($configured as $key) {
            $normalized[$key] = [
                'status' => 'granted',
                'details' => ['source' => 'configured'],
            ];
        }

        return $normalized;
    }

    /**
     * @return array<int, string>
     */
    private function configuredGrantedKeys(): array
    {
        $env = env('INTUNE_GRANTED_PERMISSIONS');

        if (is_string($env) && filled($env)) {
            return collect(explode(',', $env))
                ->map(fn (string $key) => trim($key))
                ->filter()
                ->values()
                ->all();
        }

        return config('intune_permissions.granted_stub', []);
    }

    private function resolveTenantWorkspaceId(ManagedEnvironment $tenant): ?int
    {
        $workspaceId = $tenant->getAttribute('workspace_id');

        if (is_numeric($workspaceId)) {
            return (int) $workspaceId;
        }

        if (! $tenant->exists) {
            return null;
        }

        $workspaceId = ManagedEnvironment::query()
            ->whereKey($tenant->getKey())
            ->value('workspace_id');

        if (! is_numeric($workspaceId)) {
            return null;
        }

        return (int) $workspaceId;
    }

    /**
     * Fetch actual granted permissions from Graph API.
     *
     * @return array<string, array{status:string,details:array<string,mixed>|null}>
     */
    private function fetchLivePermissions(ManagedEnvironment $tenant, ?array $graphOptions = null): array
    {
        try {
            $graphOptions ??= $this->graphOptionsResolver->resolveForTenant($tenant);

            $response = $this->graphClient->getServicePrincipalPermissions(
                $graphOptions
            );

            if (! $response->success) {
                return [
                    '__error' => [
                        'status' => 'error',
                        'details' => [
                            'source' => 'graph_api',
                            'status' => $response->status,
                            'errors' => $response->errors,
                        ],
                    ],
                ];
            }

            $grantedPermissions = $response->data['permissions'] ?? [];
            $diagnostics = is_array($response->data['diagnostics'] ?? null) ? $response->data['diagnostics'] : null;
            $assignmentsTotal = is_array($diagnostics) ? (int) ($diagnostics['assignments_total'] ?? 0) : 0;
            $mappedTotal = is_array($diagnostics) ? (int) ($diagnostics['mapped_total'] ?? 0) : null;

            if ($assignmentsTotal > 0 && $mappedTotal === 0) {
                return [
                    '__error' => [
                        'status' => 'error',
                        'details' => [
                            'source' => 'graph_api',
                            'status' => $response->status,
                            'reason_code' => 'permission_mapping_failed',
                            'message' => 'Graph returned app role assignments, but the system could not map them to permission values.',
                            'diagnostics' => $diagnostics,
                        ],
                    ],
                ];
            }

            $normalized = [];

            foreach ($grantedPermissions as $permission) {
                $normalized[$permission] = [
                    'status' => 'granted',
                    'details' => ['source' => 'graph_api', 'checked_at' => now()->toIso8601String()],
                ];
            }

            return $normalized;
        } catch (\Throwable $e) {
            // Log error but don't fail - fall back to config
            \Log::warning('Failed to fetch live permissions from Graph', [
                'managed_environment_id' => $tenant->id,
                'error' => $e->getMessage(),
            ]);

            return [
                '__error' => [
                    'status' => 'error',
                    'details' => [
                        'source' => 'graph_api',
                        'message' => $e->getMessage(),
                    ],
                ],
            ];
        }
    }

    private function lastRefreshedAtIso(ManagedEnvironment $tenant): ?string
    {
        $lastCheckedAt = ManagedEnvironmentPermission::query()
            ->where('managed_environment_id', (int) $tenant->getKey())
            ->max('last_checked_at');

        if ($lastCheckedAt instanceof DateTimeInterface) {
            return Carbon::instance($lastCheckedAt)->toIso8601String();
        }

        if (is_string($lastCheckedAt) && $lastCheckedAt !== '') {
            try {
                return Carbon::parse($lastCheckedAt)->toIso8601String();
            } catch (\Throwable) {
                return null;
            }
        }

        return null;
    }
}
