<?php

namespace App\Services\Intune;

use App\Models\Policy;
use App\Models\PolicyVersion;
use App\Models\ProviderConnection;
use App\Models\ManagedEnvironment;
use App\Services\Graph\GraphClientInterface;
use App\Services\Graph\GraphErrorMapper;
use App\Services\Graph\GraphLogger;
use App\Services\Providers\ProviderConnectionResolver;
use App\Services\Providers\ProviderGateway;
use App\Support\Audit\AuditActionId;
use App\Support\Providers\ProviderNextStepsRegistry;
use App\Support\Providers\ProviderReasonCodes;
use Illuminate\Support\Arr;
use RuntimeException;
use Throwable;

class PolicySyncService
{
    public function __construct(
        private readonly GraphClientInterface $graphClient,
        private readonly GraphLogger $graphLogger,
        private readonly ?ProviderConnectionResolver $providerConnections = null,
        private readonly ?ProviderGateway $providerGateway = null,
        private readonly ?ProviderNextStepsRegistry $nextStepsRegistry = null,
        private readonly ?AuditLogger $auditLogger = null,
    ) {}

    /**
     * Sync supported policies for a tenant from Microsoft Graph.
     *
     * @return array<int> IDs of policies synced or created
     */
    public function syncPolicies(ManagedEnvironment $tenant, ?array $supportedTypes = null): array
    {
        $result = $this->syncPoliciesWithReport($tenant, $supportedTypes);

        return $result['synced'];
    }

    /**
     * Sync supported policies for a tenant from Microsoft Graph.
     *
     * @param  array<int, array{type: string, platform?: string|null, filter?: string|null}>|null  $supportedTypes
     * @return array{synced: array<int>, failures: array<int, array{policy_type: string, status: int|null, errors: array, meta: array}>}
     */
    public function syncPoliciesWithReport(ManagedEnvironment $tenant, ?array $supportedTypes = null): array
    {
        if (! $tenant->isActive()) {
            throw new \RuntimeException('ManagedEnvironment is archived or inactive.');
        }

        $types = $supportedTypes ?? config('tenantpilot.supported_policy_types', []);
        $synced = [];
        $failures = [];
        $successfulPolicyTypes = [];
        $observedExternalIdsByPolicyType = [];

        $resolution = $this->providerConnections()->resolveDefault($tenant, 'microsoft');

        if (! $resolution->resolved || ! $resolution->connection instanceof ProviderConnection) {
            return [
                'synced' => [],
                'failures' => $this->blockedFailuresForTypes($tenant, $types, $resolution->effectiveReasonCode(), $resolution->message, $resolution->connection),
            ];
        }

        $connection = $resolution->connection;
        $tenantIdentifier = (string) $connection->entra_tenant_id;

        foreach ($types as $typeConfig) {
            $policyType = $typeConfig['type'];
            $platform = $typeConfig['platform'] ?? null;
            $filter = $typeConfig['filter'] ?? null;

            $this->graphLogger->logRequest('list_policies', [
                'tenant' => $tenantIdentifier,
                'policy_type' => $policyType,
                'platform' => $platform,
                'filter' => $filter,
            ]);

            try {
                $response = $this->providerGateway()->listPolicies($connection, $policyType, [
                    'platform' => $platform,
                    'filter' => $filter,
                ]);
            } catch (Throwable $throwable) {
                throw GraphErrorMapper::fromThrowable($throwable, [
                    'policy_type' => $policyType,
                    'managed_environment_id' => $tenant->id,
                    'tenant_identifier' => $tenantIdentifier,
                    'provider_connection_id' => (int) $connection->getKey(),
                ]);
            }

            $this->graphLogger->logResponse('list_policies', $response, [
                'policy_type' => $policyType,
                'managed_environment_id' => $tenant->id,
                'tenant' => $tenantIdentifier,
            ]);

            if ($response->failed()) {
                $failures[] = [
                    'policy_type' => $policyType,
                    'status' => $response->status,
                    'errors' => $response->errors,
                    'meta' => $response->meta,
                ];

                continue;
            }

            $successfulPolicyTypes[$policyType] = true;
            $observedExternalIdsByPolicyType[$policyType] ??= [];

            foreach ($response->data as $policyData) {
                $externalId = $policyData['id'] ?? $policyData['external_id'] ?? null;

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

                $externalId = (string) $externalId;

                $canonicalPolicyType = $this->resolveCanonicalPolicyType($policyType, $policyData);

                if ($canonicalPolicyType !== $policyType) {
                    continue;
                }

                if ($policyType === 'appProtectionPolicy') {
                    $odataType = $policyData['@odata.type'] ?? $policyData['@OData.Type'] ?? null;

                    if (is_string($odataType) && strtolower($odataType) === '#microsoft.graph.targetedmanagedappconfiguration') {
                        continue;
                    }
                }

                $observedExternalIdsByPolicyType[$policyType][] = $externalId;
                $displayName = $policyData['displayName'] ?? $policyData['name'] ?? 'Unnamed policy';
                $policyPlatform = $platform ?? ($policyData['platform'] ?? null);

                $this->reclassifyEnrollmentConfigurationPoliciesIfNeeded(
                    tenant: $tenant,
                    externalId: $externalId,
                    policyType: $policyType,
                );

                $this->reclassifyConfigurationPoliciesIfNeeded(
                    tenant: $tenant,
                    externalId: $externalId,
                    policyType: $policyType,
                );

                $policy = Policy::query()->firstOrNew([
                    'managed_environment_id' => $tenant->id,
                    'external_id' => $externalId,
                    'policy_type' => $policyType,
                ]);
                $wasProviderMissing = $policy->exists && $policy->missing_from_provider_at !== null;

                $policy->forceFill([
                    'workspace_id' => $tenant->workspace_id,
                    'display_name' => $displayName,
                    'platform' => $policyPlatform,
                    'last_synced_at' => now(),
                    'missing_from_provider_at' => null,
                    'metadata' => Arr::except($policyData, ['id', 'external_id', 'displayName', 'name', 'platform']),
                ])->save();

                if ($wasProviderMissing) {
                    $this->auditProviderPresenceTransition(
                        tenant: $tenant,
                        policy: $policy,
                        action: AuditActionId::PolicyProviderMissingCleared,
                    );
                }

                $synced[] = $policy->id;
            }
        }

        $this->markProviderMissingPolicies(
            tenant: $tenant,
            policyTypes: array_keys($successfulPolicyTypes),
            observedExternalIdsByPolicyType: $observedExternalIdsByPolicyType,
        );

        return [
            'synced' => $synced,
            'failures' => $failures,
        ];
    }

    private function resolveCanonicalPolicyType(string $policyType, array $policyData): string
    {
        if (in_array($policyType, ['settingsCatalogPolicy', 'endpointSecurityPolicy', 'securityBaselinePolicy'], true)) {
            return $this->resolveConfigurationPolicyType($policyData);
        }

        $enrollmentConfigurationTypes = [
            'enrollmentRestriction',
            'windowsEnrollmentStatusPage',
            'deviceEnrollmentLimitConfiguration',
            'deviceEnrollmentPlatformRestrictionsConfiguration',
            'deviceEnrollmentNotificationConfiguration',
        ];

        if (! in_array($policyType, $enrollmentConfigurationTypes, true)) {
            return $policyType;
        }

        if ($this->isEnrollmentStatusPageItem($policyData)) {
            return 'windowsEnrollmentStatusPage';
        }

        if ($this->isEnrollmentNotificationItem($policyData)) {
            return 'deviceEnrollmentNotificationConfiguration';
        }

        if ($this->isEnrollmentLimitItem($policyData)) {
            return 'deviceEnrollmentLimitConfiguration';
        }

        if ($this->isEnrollmentPlatformRestrictionsItem($policyData)) {
            return 'deviceEnrollmentPlatformRestrictionsConfiguration';
        }

        return 'enrollmentRestriction';
    }

    private function resolveConfigurationPolicyType(array $policyData): string
    {
        if ($this->isSecurityBaselineConfigurationPolicy($policyData)) {
            return 'securityBaselinePolicy';
        }

        if ($this->isEndpointSecurityConfigurationPolicy($policyData)) {
            return 'endpointSecurityPolicy';
        }

        return 'settingsCatalogPolicy';
    }

    private function isEndpointSecurityConfigurationPolicy(array $policyData): bool
    {
        $technologies = $policyData['technologies'] ?? null;

        if (is_string($technologies)) {
            if (strcasecmp(trim($technologies), 'endpointSecurity') === 0) {
                return true;
            }
        }

        if (is_array($technologies)) {
            foreach ($technologies as $technology) {
                if (is_string($technology) && strcasecmp(trim($technology), 'endpointSecurity') === 0) {
                    return true;
                }
            }
        }

        $templateReference = $policyData['templateReference'] ?? null;

        if (! is_array($templateReference)) {
            return false;
        }

        $templateFamily = $templateReference['templateFamily'] ?? null;

        return is_string($templateFamily) && str_starts_with(strtolower(trim($templateFamily)), 'endpointsecurity');
    }

    private function isSecurityBaselineConfigurationPolicy(array $policyData): bool
    {
        $templateReference = $policyData['templateReference'] ?? null;

        if (! is_array($templateReference)) {
            return false;
        }

        $templateFamily = $templateReference['templateFamily'] ?? null;

        return is_string($templateFamily) && strcasecmp(trim($templateFamily), 'securityBaseline') === 0;
    }

    private function isEnrollmentStatusPageItem(array $policyData): bool
    {
        $odataType = $policyData['@odata.type'] ?? $policyData['@OData.Type'] ?? null;
        $configurationType = $policyData['deviceEnrollmentConfigurationType'] ?? null;

        return (is_string($odataType) && strcasecmp($odataType, '#microsoft.graph.windows10EnrollmentCompletionPageConfiguration') === 0)
            || (is_string($configurationType) && $configurationType === 'windows10EnrollmentCompletionPageConfiguration');
    }

    private function isEnrollmentLimitItem(array $policyData): bool
    {
        $odataType = $policyData['@odata.type'] ?? $policyData['@OData.Type'] ?? null;
        $configurationType = $policyData['deviceEnrollmentConfigurationType'] ?? null;

        return (is_string($odataType) && strcasecmp($odataType, '#microsoft.graph.deviceEnrollmentLimitConfiguration') === 0)
            || (is_string($configurationType) && strcasecmp($configurationType, 'deviceEnrollmentLimitConfiguration') === 0);
    }

    private function isEnrollmentPlatformRestrictionsItem(array $policyData): bool
    {
        $odataType = $policyData['@odata.type'] ?? $policyData['@OData.Type'] ?? null;
        $configurationType = $policyData['deviceEnrollmentConfigurationType'] ?? null;

        if (is_string($odataType) && $odataType !== '') {
            $odataTypeKey = strtolower($odataType);

            if (in_array($odataTypeKey, [
                '#microsoft.graph.deviceenrollmentplatformrestrictionconfiguration',
                '#microsoft.graph.deviceenrollmentplatformrestrictionsconfiguration',
            ], true)) {
                return true;
            }
        }

        if (is_string($configurationType) && $configurationType !== '') {
            $configurationTypeKey = strtolower($configurationType);

            if (in_array($configurationTypeKey, [
                'deviceenrollmentplatformrestrictionconfiguration',
                'deviceenrollmentplatformrestrictionsconfiguration',
            ], true)) {
                return true;
            }
        }

        return false;
    }

    private function isEnrollmentNotificationItem(array $policyData): bool
    {
        $odataType = $policyData['@odata.type'] ?? $policyData['@OData.Type'] ?? null;
        $configurationType = $policyData['deviceEnrollmentConfigurationType'] ?? null;

        if (is_string($odataType) && strcasecmp($odataType, '#microsoft.graph.deviceEnrollmentNotificationConfiguration') === 0) {
            return true;
        }

        if (! is_string($configurationType) || $configurationType === '') {
            return false;
        }

        return in_array(strtolower($configurationType), [
            'enrollmentnotificationsconfiguration',
            'deviceenrollmentnotificationconfiguration',
        ], true);
    }

    private function reclassifyEnrollmentConfigurationPoliciesIfNeeded(ManagedEnvironment $tenant, string $externalId, string $policyType): void
    {
        $enrollmentTypes = [
            'enrollmentRestriction',
            'windowsEnrollmentStatusPage',
            'deviceEnrollmentLimitConfiguration',
            'deviceEnrollmentPlatformRestrictionsConfiguration',
            'deviceEnrollmentNotificationConfiguration',
        ];

        if (! in_array($policyType, $enrollmentTypes, true)) {
            return;
        }

        $existingCorrect = Policy::query()
            ->where('managed_environment_id', $tenant->id)
            ->where('external_id', $externalId)
            ->where('policy_type', $policyType)
            ->first();

        if ($existingCorrect) {
            $this->markSiblingPoliciesProviderMissing(
                tenant: $tenant,
                externalId: $externalId,
                policyTypes: $enrollmentTypes,
                exceptPolicyType: $policyType,
            );

            return;
        }

        $existingWrong = Policy::query()
            ->where('managed_environment_id', $tenant->id)
            ->where('external_id', $externalId)
            ->whereIn('policy_type', $enrollmentTypes)
            ->where('policy_type', '!=', $policyType)
            ->first();

        if (! $existingWrong) {
            return;
        }

        $wasProviderMissing = $existingWrong->missing_from_provider_at !== null;

        $existingWrong->forceFill([
            'policy_type' => $policyType,
            'missing_from_provider_at' => null,
        ])->save();

        if ($wasProviderMissing) {
            $this->auditProviderPresenceTransition(
                tenant: $tenant,
                policy: $existingWrong,
                action: AuditActionId::PolicyProviderMissingCleared,
            );
        }

        PolicyVersion::query()
            ->where('policy_id', $existingWrong->id)
            ->update(['policy_type' => $policyType]);
    }

    private function reclassifyConfigurationPoliciesIfNeeded(ManagedEnvironment $tenant, string $externalId, string $policyType): void
    {
        $configurationTypes = ['settingsCatalogPolicy', 'endpointSecurityPolicy', 'securityBaselinePolicy'];

        if (! in_array($policyType, $configurationTypes, true)) {
            return;
        }

        $existingCorrect = Policy::query()
            ->where('managed_environment_id', $tenant->id)
            ->where('external_id', $externalId)
            ->where('policy_type', $policyType)
            ->first();

        if ($existingCorrect) {
            $this->markSiblingPoliciesProviderMissing(
                tenant: $tenant,
                externalId: $externalId,
                policyTypes: $configurationTypes,
                exceptPolicyType: $policyType,
            );

            return;
        }

        $existingWrong = Policy::query()
            ->where('managed_environment_id', $tenant->id)
            ->where('external_id', $externalId)
            ->whereIn('policy_type', $configurationTypes)
            ->where('policy_type', '!=', $policyType)
            ->first();

        if (! $existingWrong) {
            return;
        }

        $wasProviderMissing = $existingWrong->missing_from_provider_at !== null;

        $existingWrong->forceFill([
            'policy_type' => $policyType,
            'missing_from_provider_at' => null,
        ])->save();

        if ($wasProviderMissing) {
            $this->auditProviderPresenceTransition(
                tenant: $tenant,
                policy: $existingWrong,
                action: AuditActionId::PolicyProviderMissingCleared,
            );
        }

        PolicyVersion::query()
            ->where('policy_id', $existingWrong->id)
            ->update(['policy_type' => $policyType]);
    }

    /**
     * @param  array<int, string>  $policyTypes
     */
    private function markSiblingPoliciesProviderMissing(ManagedEnvironment $tenant, string $externalId, array $policyTypes, string $exceptPolicyType): void
    {
        $timestamp = now();

        Policy::query()
            ->where('managed_environment_id', $tenant->id)
            ->where('external_id', $externalId)
            ->whereIn('policy_type', $policyTypes)
            ->where('policy_type', '!=', $exceptPolicyType)
            ->whereNull('missing_from_provider_at')
            ->get()
            ->each(function (Policy $policy) use ($tenant, $timestamp): void {
                $policy->forceFill(['missing_from_provider_at' => $timestamp])->save();

                $this->auditProviderPresenceTransition(
                    tenant: $tenant,
                    policy: $policy,
                    action: AuditActionId::PolicyProviderMissingDetected,
                    transitionAt: $timestamp,
                );
            });
    }

    /**
     * @param  array<int, string>  $policyTypes
     * @param  array<string, array<int, string>>  $observedExternalIdsByPolicyType
     */
    private function markProviderMissingPolicies(ManagedEnvironment $tenant, array $policyTypes, array $observedExternalIdsByPolicyType): void
    {
        foreach ($policyTypes as $policyType) {
            if (! is_string($policyType) || $policyType === '') {
                continue;
            }

            $observedExternalIds = array_values(array_unique(array_filter(
                array_map('strval', $observedExternalIdsByPolicyType[$policyType] ?? []),
                static fn (string $externalId): bool => $externalId !== '',
            )));

            $query = Policy::query()
                ->where('managed_environment_id', $tenant->id)
                ->where('policy_type', $policyType)
                ->whereNull('missing_from_provider_at');

            if ($observedExternalIds !== []) {
                $query->whereNotIn('external_id', $observedExternalIds);
            }

            $timestamp = now();

            $query->get()
                ->each(function (Policy $policy) use ($tenant, $timestamp): void {
                    $policy->forceFill(['missing_from_provider_at' => $timestamp])->save();

                    $this->auditProviderPresenceTransition(
                        tenant: $tenant,
                        policy: $policy,
                        action: AuditActionId::PolicyProviderMissingDetected,
                        transitionAt: $timestamp,
                    );
                });
        }
    }

    private function auditProviderPresenceTransition(
        ManagedEnvironment $tenant,
        Policy $policy,
        AuditActionId $action,
        mixed $transitionAt = null,
    ): void {
        $transitionAt ??= now();

        $this->auditLogger()->log(
            tenant: $tenant,
            action: $action,
            context: [
                'metadata' => [
                    'policy_id' => (int) $policy->getKey(),
                    'external_id' => (string) $policy->external_id,
                    'policy_type' => (string) $policy->policy_type,
                    'transition_at' => method_exists($transitionAt, 'toIso8601String')
                        ? $transitionAt->toIso8601String()
                        : (string) $transitionAt,
                    'source' => 'policy_sync',
                ],
            ],
            resourceType: 'policy',
            resourceId: (string) $policy->getKey(),
            targetLabel: (string) $policy->display_name,
            status: 'success',
        );
    }

    private function auditLogger(): AuditLogger
    {
        return $this->auditLogger ?? app(AuditLogger::class);
    }

    /**
     * Re-fetch a single policy from Graph and update local metadata.
     */
    public function syncPolicy(ManagedEnvironment $tenant, Policy $policy): void
    {
        if (! $tenant->isActive()) {
            throw new RuntimeException('ManagedEnvironment is archived or inactive.');
        }

        $resolution = $this->providerConnections()->resolveDefault($tenant, 'microsoft');

        if (! $resolution->resolved || ! $resolution->connection instanceof ProviderConnection) {
            $reasonCode = $resolution->effectiveReasonCode();
            $reasonMessage = $resolution->message ?? 'Provider connection is not configured.';

            throw new RuntimeException(sprintf('[%s] %s', $reasonCode, $reasonMessage));
        }

        $connection = $resolution->connection;
        $tenantIdentifier = (string) $connection->entra_tenant_id;

        $this->graphLogger->logRequest('get_policy', [
            'tenant' => $tenantIdentifier,
            'policy_type' => $policy->policy_type,
            'policy_id' => $policy->external_id,
            'platform' => $policy->platform,
        ]);

        try {
            $response = $this->providerGateway()->getPolicy(
                connection: $connection,
                policyType: $policy->policy_type,
                policyId: $policy->external_id,
                options: [
                    'platform' => $policy->platform,
                ],
            );
        } catch (Throwable $throwable) {
            throw GraphErrorMapper::fromThrowable($throwable, [
                'policy_type' => $policy->policy_type,
                'policy_id' => $policy->external_id,
                'managed_environment_id' => $tenant->id,
                'tenant_identifier' => $tenantIdentifier,
                'provider_connection_id' => (int) $connection->getKey(),
            ]);
        }

        $this->graphLogger->logResponse('get_policy', $response, [
            'managed_environment_id' => $tenant->id,
            'tenant' => $tenantIdentifier,
            'policy_type' => $policy->policy_type,
            'policy_id' => $policy->external_id,
        ]);

        if ($response->failed()) {
            $message = $response->errors[0]['message'] ?? $response->data['error']['message'] ?? 'Graph request failed.';

            throw new RuntimeException($message);
        }

        $payload = $response->data['payload'] ?? $response->data;

        if (! is_array($payload)) {
            throw new RuntimeException('Invalid Graph response payload.');
        }

        $displayName = $payload['displayName'] ?? $payload['name'] ?? $policy->display_name;
        $platform = $payload['platform'] ?? $policy->platform;
        $wasProviderMissing = $policy->missing_from_provider_at !== null;

        $policy->forceFill([
            'display_name' => $displayName,
            'platform' => $platform,
            'last_synced_at' => now(),
            'missing_from_provider_at' => null,
            'metadata' => Arr::except($payload, ['id', 'external_id', 'displayName', 'name', 'platform']),
        ])->save();

        if ($wasProviderMissing) {
            $this->auditProviderPresenceTransition(
                tenant: $tenant,
                policy: $policy,
                action: AuditActionId::PolicyProviderMissingCleared,
            );
        }
    }

    /**
     * @param  array<int, array{type: string, platform?: string|null, filter?: string|null}>  $types
     * @return array<int, array{policy_type: string, status: int|null, errors: array, meta: array}>
     */
    private function blockedFailuresForTypes(
        ManagedEnvironment $tenant,
        array $types,
        string $reasonCode,
        ?string $reasonMessage = null,
        ?ProviderConnection $connection = null,
    ): array {
        $knownReasonCode = ProviderReasonCodes::isKnown($reasonCode)
            ? $reasonCode
            : ProviderReasonCodes::UnknownError;

        $message = sprintf(
            '[%s] %s',
            $knownReasonCode,
            $reasonMessage ?? 'Provider connection is not configured.',
        );

        $nextSteps = $this->nextStepsRegistry()->forReason($tenant, $knownReasonCode, $connection);
        $failures = [];

        foreach ($types as $typeConfig) {
            if (! is_array($typeConfig)) {
                continue;
            }

            $policyType = $typeConfig['type'] ?? null;

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

            $failures[] = [
                'policy_type' => $policyType,
                'status' => null,
                'errors' => [['message' => $message]],
                'meta' => [
                    'reason_code' => $knownReasonCode,
                    'next_steps' => $nextSteps,
                ],
            ];
        }

        return $failures;
    }

    private function providerConnections(): ProviderConnectionResolver
    {
        return $this->providerConnections ?? app(ProviderConnectionResolver::class);
    }

    private function providerGateway(): ProviderGateway
    {
        return $this->providerGateway ?? app(ProviderGateway::class);
    }

    private function nextStepsRegistry(): ProviderNextStepsRegistry
    {
        return $this->nextStepsRegistry ?? app(ProviderNextStepsRegistry::class);
    }
}
