<?php

namespace App\Services\Intune;

use App\Models\Policy;
use App\Models\PolicyVersion;
use App\Models\Tenant;
use App\Services\Graph\AssignmentFetcher;
use App\Services\Graph\AssignmentFilterResolver;
use App\Services\Graph\GroupResolver;
use App\Services\Graph\ScopeTagResolver;
use Carbon\CarbonImmutable;

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

    public function captureVersion(
        Policy $policy,
        array $payload,
        ?string $createdBy = null,
        array $metadata = [],
        ?array $assignments = null,
        ?array $scopeTags = null,
    ): PolicyVersion {
        $versionNumber = $this->nextVersionNumber($policy);

        $version = PolicyVersion::create([
            'tenant_id' => $policy->tenant_id,
            'policy_id' => $policy->id,
            'version_number' => $versionNumber,
            'policy_type' => $policy->policy_type,
            'platform' => $policy->platform,
            'created_by' => $createdBy,
            'captured_at' => CarbonImmutable::now(),
            'snapshot' => $payload,
            'metadata' => $metadata,
            'assignments' => $assignments,
            'scope_tags' => $scopeTags,
            'assignments_hash' => $assignments ? hash('sha256', json_encode($assignments)) : null,
            'scope_tags_hash' => $scopeTags ? hash('sha256', json_encode($scopeTags)) : null,
        ]);

        $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 captureFromGraph(
        Tenant $tenant,
        Policy $policy,
        ?string $createdBy = null,
        array $metadata = [],
        bool $includeAssignments = true,
        bool $includeScopeTags = true,
    ): PolicyVersion {
        $graphOptions = $tenant->graphOptions();
        $tenantIdentifier = $graphOptions['tenant'] ?? $tenant->graphTenantId() ?? (string) $tenant->getKey();

        $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();
            }
        }

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

    /**
     * @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(Tenant $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,
        ];
    }

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

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