<?php

namespace App\Services\Intune;

use App\Models\Policy;
use App\Models\PolicyVersion;
use App\Models\ManagedEnvironment;
use App\Services\Graph\AssignmentFetcher;
use App\Services\Graph\AssignmentFilterResolver;
use App\Services\Graph\GraphException;
use App\Services\Graph\GroupResolver;
use App\Services\Graph\ScopeTagResolver;
use App\Services\Providers\MicrosoftGraphOptionsResolver;
use App\Support\Baselines\PolicyVersionCapturePurpose;
use Carbon\CarbonImmutable;
use Illuminate\Database\QueryException;
use Illuminate\Database\UniqueConstraintViolationException;
use Illuminate\Support\Facades\DB;

class VersionService
{
    public function __construct(
        private readonly AuditLogger $auditLogger,
        private readonly PolicySnapshotService $snapshotService,
        private readonly PolicySnapshotRedactor $snapshotRedactor,
        private readonly AssignmentFetcher $assignmentFetcher,
        private readonly GroupResolver $groupResolver,
        private readonly AssignmentFilterResolver $assignmentFilterResolver,
        private readonly ScopeTagResolver $scopeTagResolver,
        private readonly MicrosoftGraphOptionsResolver $graphOptionsResolver,
    ) {}

    public function captureVersion(
        Policy $policy,
        array $payload,
        ?string $createdBy = null,
        array $metadata = [],
        ?array $assignments = null,
        ?array $scopeTags = null,
        PolicyVersionCapturePurpose $capturePurpose = PolicyVersionCapturePurpose::Backup,
        ?int $operationRunId = null,
        ?int $baselineProfileId = null,
    ): PolicyVersion {
        $policy->loadMissing('tenant');

        $workspaceId = is_numeric($policy->tenant?->workspace_id ?? null)
            ? (int) $policy->tenant?->workspace_id
            : 0;

        $protectedSnapshot = $this->snapshotRedactor->protect(
            workspaceId: $workspaceId,
            payload: $payload,
            assignments: $assignments,
            scopeTags: $scopeTags,
        );

        $version = null;
        $versionNumber = null;

        for ($attempt = 1; $attempt <= 3; $attempt++) {
            try {
                [$version, $versionNumber] = DB::transaction(function () use ($policy, $protectedSnapshot, $createdBy, $metadata, $capturePurpose, $operationRunId, $baselineProfileId): array {
                    // Serialize version number allocation per policy.
                    Policy::query()->whereKey($policy->getKey())->lockForUpdate()->first();

                    $versionNumber = $this->nextVersionNumber($policy);

                    $version = PolicyVersion::create([
                        'managed_environment_id' => $policy->managed_environment_id,
                        'policy_id' => $policy->id,
                        'version_number' => $versionNumber,
                        'policy_type' => $policy->policy_type,
                        'platform' => $policy->platform,
                        'created_by' => $createdBy,
                        'captured_at' => CarbonImmutable::now(),
                        'snapshot' => $protectedSnapshot->snapshot,
                        'metadata' => $metadata,
                        'assignments' => $protectedSnapshot->assignments,
                        'scope_tags' => $protectedSnapshot->scopeTags,
                        'assignments_hash' => $protectedSnapshot->assignments !== null ? hash('sha256', json_encode($protectedSnapshot->assignments, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)) : null,
                        'scope_tags_hash' => $protectedSnapshot->scopeTags !== null ? hash('sha256', json_encode($protectedSnapshot->scopeTags, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)) : null,
                        'secret_fingerprints' => $protectedSnapshot->secretFingerprints,
                        'redaction_version' => $protectedSnapshot->redactionVersion,
                        'capture_purpose' => $capturePurpose->value,
                        'operation_run_id' => $operationRunId,
                        'baseline_profile_id' => $baselineProfileId,
                    ]);

                    return [$version, $versionNumber];
                }, 3);

                break;
            } catch (QueryException $e) {
                if (! $this->isUniqueViolation($e) || $attempt === 3) {
                    throw $e;
                }

                usleep(50_000 * $attempt);
            }
        }

        if (! $version instanceof PolicyVersion || ! is_int($versionNumber)) {
            throw new \RuntimeException('Failed to capture policy version.');
        }

        $this->auditLogger->log(
            tenant: $policy->tenant,
            action: 'policy.versioned',
            context: [
                'metadata' => [
                    'policy_id' => $policy->id,
                    'version_number' => $versionNumber,
                ],
            ],
            actorEmail: $createdBy,
            resourceType: 'policy',
            resourceId: (string) $policy->id
        );

        return $version;
    }

    public function captureFoundationVersion(
        Policy $policy,
        array $payload,
        ?string $createdBy = null,
        array $metadata = [],
        PolicyVersionCapturePurpose $capturePurpose = PolicyVersionCapturePurpose::Backup,
        ?int $operationRunId = null,
        ?int $baselineProfileId = null,
    ): PolicyVersion {
        $policy->loadMissing('tenant');

        $workspaceId = is_numeric($policy->tenant?->workspace_id ?? null)
            ? (int) $policy->tenant?->workspace_id
            : 0;

        $protectedSnapshot = $this->snapshotRedactor->protect(
            workspaceId: $workspaceId,
            payload: $payload,
            assignments: null,
            scopeTags: null,
        );

        $snapshotHash = $this->snapshotContractHash(
            snapshot: $protectedSnapshot->snapshot,
            snapshotFingerprints: $protectedSnapshot->secretFingerprints['snapshot'],
            redactionVersion: $protectedSnapshot->redactionVersion,
        );

        $existingVersion = PolicyVersion::query()
            ->where('policy_id', $policy->getKey())
            ->where('capture_purpose', $capturePurpose->value)
            ->when(
                $capturePurpose !== PolicyVersionCapturePurpose::Backup && $baselineProfileId !== null,
                fn ($query) => $query->where('baseline_profile_id', $baselineProfileId),
            )
            ->get()
            ->first(function (PolicyVersion $version) use ($snapshotHash): bool {
                return $this->snapshotContractHash(
                    snapshot: is_array($version->snapshot) ? $version->snapshot : [],
                    snapshotFingerprints: $this->fingerprintBucket($version, 'snapshot'),
                    redactionVersion: is_numeric($version->redaction_version) ? (int) $version->redaction_version : null,
                ) === $snapshotHash;
            });

        if ($existingVersion instanceof PolicyVersion) {
            return $existingVersion;
        }

        return $this->captureVersion(
            policy: $policy,
            payload: $payload,
            createdBy: $createdBy,
            metadata: array_merge(
                ['capture_source' => 'foundation_capture'],
                $metadata,
            ),
            assignments: null,
            scopeTags: null,
            capturePurpose: $capturePurpose,
            operationRunId: $operationRunId,
            baselineProfileId: $baselineProfileId,
        );
    }

    private function isUniqueViolation(QueryException $exception): bool
    {
        if ($exception instanceof UniqueConstraintViolationException) {
            return true;
        }

        $sqlState = $exception->getCode();

        if (is_string($sqlState) && in_array($sqlState, ['23505', '23000'], true)) {
            return true;
        }

        $errorInfoState = $exception->errorInfo[0] ?? null;

        return is_string($errorInfoState) && in_array($errorInfoState, ['23505', '23000'], true);
    }

    public function captureFromGraph(
        ManagedEnvironment $tenant,
        Policy $policy,
        ?string $createdBy = null,
        array $metadata = [],
        bool $includeAssignments = true,
        bool $includeScopeTags = true,
        PolicyVersionCapturePurpose $capturePurpose = PolicyVersionCapturePurpose::Backup,
        ?int $operationRunId = null,
        ?int $baselineProfileId = null,
    ): PolicyVersion {
        $graphOptions = $this->graphOptionsResolver->resolveForTenant($tenant);
        $tenantIdentifier = (string) ($graphOptions['tenant'] ?? '');

        $snapshot = $this->snapshotService->fetch($tenant, $policy, $createdBy);

        if (isset($snapshot['failure'])) {
            $reason = $snapshot['failure']['reason'] ?? 'Unable to fetch policy snapshot';

            throw new \RuntimeException($reason);
        }

        $payload = $snapshot['payload'];
        $snapshotMetadata = is_array($snapshot['metadata'] ?? null) ? $snapshot['metadata'] : [];
        $snapshotWarnings = is_array($snapshot['warnings'] ?? null) ? $snapshot['warnings'] : [];
        $assignments = null;
        $scopeTags = null;
        $assignmentMetadata = [];

        if ($includeAssignments) {
            try {
                $rawAssignments = $this->assignmentFetcher->fetch(
                    $policy->policy_type,
                    $tenantIdentifier,
                    $policy->external_id,
                    $graphOptions,
                    true,
                    $payload['@odata.type'] ?? null,
                );
                $assignmentMetadata['assignments_fetched'] = true;
                $assignmentMetadata['assignments_count'] = count($rawAssignments);

                if (! empty($rawAssignments)) {
                    $resolvedGroups = [];

                    // Resolve groups
                    $groupIds = collect($rawAssignments)
                        ->pluck('target.groupId')
                        ->filter()
                        ->unique()
                        ->values()
                        ->toArray();

                    if (! empty($groupIds)) {
                        $resolvedGroups = $this->groupResolver->resolveGroupIds($groupIds, $tenantIdentifier, $graphOptions);
                    }

                    $assignmentMetadata['has_orphaned_assignments'] = collect($resolvedGroups)
                        ->contains(fn (array $group) => $group['orphaned'] ?? false);

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

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

                    $assignments = $this->enrichAssignments($rawAssignments, $resolvedGroups, $filterNames);
                }
            } catch (\Throwable $e) {
                $assignmentMetadata['assignments_fetch_failed'] = true;
                $assignmentMetadata['assignments_fetch_error'] = $e->getMessage();
                $assignmentMetadata['assignments_fetch_error_code'] = $e instanceof GraphException
                    ? ($e->status ?? null)
                    : (is_numeric($e->getCode()) ? (int) $e->getCode() : null);
            }
        }

        if ($includeScopeTags) {
            $scopeTagIds = $payload['roleScopeTagIds'] ?? ['0'];
            $scopeTags = $this->resolveScopeTags($tenant, $scopeTagIds);
        }

        $metadata = array_merge(
            $snapshotMetadata,
            ['capture_source' => 'version_capture'],
            $metadata,
            $assignmentMetadata,
        );

        if ($snapshotWarnings !== []) {
            $existingWarnings = is_array($metadata['warnings'] ?? null) ? $metadata['warnings'] : [];
            $metadata['warnings'] = array_values(array_unique(array_merge($existingWarnings, $snapshotWarnings)));
        }

        return $this->captureVersion(
            policy: $policy,
            payload: $payload,
            createdBy: $createdBy,
            metadata: $metadata,
            assignments: $assignments,
            scopeTags: $scopeTags,
            capturePurpose: $capturePurpose,
            operationRunId: $operationRunId,
            baselineProfileId: $baselineProfileId,
        );
    }

    /**
     * @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));
    }

    /**
     * @param  array<int, string>  $scopeTagIds
     * @return array{ids:array<int, string>,names:array<int, string>}
     */
    private function resolveScopeTags(ManagedEnvironment $tenant, array $scopeTagIds): array
    {
        $scopeTags = $this->scopeTagResolver->resolve($scopeTagIds, $tenant);

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

        return [
            'ids' => $scopeTagIds,
            'names' => $names,
        ];
    }

    /**
     * @param  array<string, mixed>  $snapshot
     * @param  array<string, string>  $snapshotFingerprints
     */
    private function snapshotContractHash(array $snapshot, array $snapshotFingerprints, ?int $redactionVersion): string
    {
        return hash(
            'sha256',
            json_encode(
                $this->normalizeHashValue([
                    'snapshot' => $snapshot,
                    'secret_fingerprints' => $snapshotFingerprints,
                    'redaction_version' => $redactionVersion,
                ]),
                JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE,
            ),
        );
    }

    /**
     * @return array<string, string>
     */
    private function fingerprintBucket(PolicyVersion $version, string $bucket): array
    {
        $fingerprints = is_array($version->secret_fingerprints) ? $version->secret_fingerprints : [];
        $bucketFingerprints = $fingerprints[$bucket] ?? [];

        return is_array($bucketFingerprints) ? $bucketFingerprints : [];
    }

    private function normalizeHashValue(mixed $value): mixed
    {
        if (! is_array($value)) {
            return $value;
        }

        if (array_is_list($value)) {
            return array_map(fn (mixed $item): mixed => $this->normalizeHashValue($item), $value);
        }

        ksort($value);

        foreach ($value as $key => $item) {
            $value[$key] = $this->normalizeHashValue($item);
        }

        return $value;
    }

    private function nextVersionNumber(Policy $policy): int
    {
        $current = PolicyVersion::query()
            ->where('policy_id', $policy->id)
            ->max('version_number');

        return (int) ($current ?? 0) + 1;
    }
}
