<?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 Illuminate\Support\Facades\Log;

/**
 * Orchestrates policy capture with assignments and scope tags.
 *
 * Ensures PolicyVersion is the source of truth, with BackupItem as restore copy.
 */
class PolicyCaptureOrchestrator
{
    public function __construct(
        private readonly VersionService $versionService,
        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,
    ) {}

    /**
     * Capture policy snapshot with optional assignments/scope tags.
     *
     * @return array ['version' => PolicyVersion, 'captured' => array]
     */
    public function capture(
        Policy $policy,
        ManagedEnvironment $tenant,
        bool $includeAssignments = false,
        bool $includeScopeTags = false,
        ?string $createdBy = null,
        array $metadata = [],
        PolicyVersionCapturePurpose $capturePurpose = PolicyVersionCapturePurpose::Backup,
        ?int $operationRunId = null,
        ?int $baselineProfileId = null,
    ): array {
        $graphOptions = $this->graphOptionsResolver->resolveForTenant($tenant);
        $tenantIdentifier = (string) ($graphOptions['tenant'] ?? '');

        // 1. Fetch policy snapshot
        $snapshot = $this->snapshotService->fetch($tenant, $policy, $createdBy);

        if (isset($snapshot['failure'])) {
            return [
                'failure' => $snapshot['failure'],
            ];
        }

        $payload = $snapshot['payload'];
        $assignments = null;
        $scopeTags = null;
        $captureMetadata = is_array($snapshot['metadata'] ?? null) ? $snapshot['metadata'] : [];

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

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

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

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

                    if (! empty($groupIds)) {
                        $resolvedGroups = $this->groupResolver->resolveGroupIds($groupIds, $tenantIdentifier, $graphOptions);
                        $captureMetadata['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) {
                $captureMetadata['assignments_fetch_failed'] = true;
                $captureMetadata['assignments_fetch_error'] = $e->getMessage();
                $captureMetadata['assignments_fetch_error_code'] = $e instanceof GraphException
                    ? ($e->status ?? null)
                    : (is_numeric($e->getCode()) ? (int) $e->getCode() : null);

                Log::warning('Failed to fetch assignments during capture', [
                    'managed_environment_id' => $tenant->id,
                    'policy_id' => $policy->id,
                    'error' => $e->getMessage(),
                ]);
            }
        }

        // 3. Fetch scope tags if requested
        if ($includeScopeTags) {
            $scopeTagIds = $payload['roleScopeTagIds'] ?? ['0'];
            $scopeTags = $this->resolveScopeTags($tenant, $scopeTagIds);
        }

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

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

        $existingVersion = PolicyVersion::query()
            ->where('policy_id', $policy->id)
            ->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 ($protectedSnapshot, $snapshotHash) {
                return $this->matchesExistingVersionSnapshot(
                    version: $version,
                    snapshot: $protectedSnapshot->snapshot,
                    snapshotHash: $snapshotHash,
                );
            });

        if ($existingVersion) {
            $updates = [];

            if ($includeAssignments && $existingVersion->assignments === null) {
                $updates['assignments'] = $protectedSnapshot->assignments;
                $updates['assignments_hash'] = $protectedSnapshot->assignments
                    ? hash('sha256', json_encode($protectedSnapshot->assignments, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE))
                    : null;
            }

            if ($includeScopeTags && $existingVersion->scope_tags === null) {
                $updates['scope_tags'] = $protectedSnapshot->scopeTags;
                $updates['scope_tags_hash'] = $protectedSnapshot->scopeTags
                    ? hash('sha256', json_encode($protectedSnapshot->scopeTags, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE))
                    : null;
            }

            if ($updates !== []) {
                $secretFingerprints = is_array($existingVersion->secret_fingerprints)
                    ? $existingVersion->secret_fingerprints
                    : ProtectedSnapshotResult::emptyFingerprints();

                if (array_key_exists('assignments', $updates)) {
                    $secretFingerprints['assignments'] = $protectedSnapshot->secretFingerprints['assignments'];
                }

                if (array_key_exists('scope_tags', $updates)) {
                    $secretFingerprints['scope_tags'] = $protectedSnapshot->secretFingerprints['scope_tags'];
                }

                $updates['secret_fingerprints'] = $secretFingerprints;
            }

            if (! empty($updates)) {
                $existingVersion->update($updates);

                Log::info('Backfilled existing PolicyVersion with capture data', [
                    'managed_environment_id' => $tenant->id,
                    'policy_id' => $policy->id,
                    'version_id' => $existingVersion->id,
                    'version_number' => $existingVersion->version_number,
                    'assignments_backfilled' => array_key_exists('assignments', $updates),
                    'scope_tags_backfilled' => array_key_exists('scope_tags', $updates),
                ]);

                return [
                    'version' => $existingVersion->fresh(),
                    'captured' => [
                        'payload' => $protectedSnapshot->snapshot,
                        'assignments' => $protectedSnapshot->assignments,
                        'scope_tags' => $protectedSnapshot->scopeTags,
                        'metadata' => $captureMetadata,
                    ],
                ];
            }

            Log::info('Reusing existing PolicyVersion', [
                'managed_environment_id' => $tenant->id,
                'policy_id' => $policy->id,
                'version_id' => $existingVersion->id,
                'version_number' => $existingVersion->version_number,
            ]);

            return [
                'version' => $existingVersion,
                'captured' => [
                    'payload' => $protectedSnapshot->snapshot,
                    'assignments' => $protectedSnapshot->assignments,
                    'scope_tags' => $protectedSnapshot->scopeTags,
                    'metadata' => $captureMetadata,
                ],
            ];
        }

        // 5. Create new PolicyVersion with all captured data
        $metadata = array_merge(
            ['capture_source' => 'orchestrated_capture'],
            $metadata,
            $captureMetadata,
        );

        $version = $this->versionService->captureVersion(
            policy: $policy,
            payload: $payload,
            createdBy: $createdBy,
            metadata: $metadata,
            assignments: $assignments,
            scopeTags: $scopeTags,
            capturePurpose: $capturePurpose,
            operationRunId: $operationRunId,
            baselineProfileId: $baselineProfileId,
        );

        Log::info('Policy captured via orchestrator', [
            'managed_environment_id' => $tenant->id,
            'policy_id' => $policy->id,
            'version_id' => $version->id,
            'version_number' => $version->version_number,
            'has_assignments' => ! is_null($assignments),
            'has_scope_tags' => ! is_null($scopeTags),
        ]);

        return [
            'version' => $version,
            'captured' => [
                'payload' => $protectedSnapshot->snapshot,
                'assignments' => $protectedSnapshot->assignments,
                'scope_tags' => $protectedSnapshot->scopeTags,
                'metadata' => $captureMetadata,
            ],
        ];
    }

    /**
     * Ensure existing PolicyVersion has assignments if missing.
     */
    public function ensureVersionHasAssignments(
        PolicyVersion $version,
        ManagedEnvironment $tenant,
        Policy $policy,
        bool $includeAssignments = false,
        bool $includeScopeTags = false
    ): PolicyVersion {
        $graphOptions = $this->graphOptionsResolver->resolveForTenant($tenant);
        $tenantIdentifier = (string) ($graphOptions['tenant'] ?? '');

        if ($version->assignments !== null && $version->scope_tags !== null) {
            Log::debug('Version already has assignments, skipping', [
                'version_id' => $version->id,
            ]);

            return $version;
        }

        // Only fetch if requested
        if (! $includeAssignments && ! $includeScopeTags) {
            return $version;
        }

        $assignments = null;
        $scopeTags = null;
        $metadata = $version->metadata ?? [];

        if ($includeAssignments && $version->assignments === null) {
            try {
                $rawAssignments = $this->assignmentFetcher->fetch(
                    $policy->policy_type,
                    $tenantIdentifier,
                    $policy->external_id,
                    $graphOptions,
                    true,
                    is_array($version->snapshot) ? ($version->snapshot['@odata.type'] ?? null) : null,
                );
                $metadata['assignments_fetched'] = true;
                $metadata['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);
                        $metadata['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) {
                $metadata['assignments_fetch_failed'] = true;
                $metadata['assignments_fetch_error'] = $e->getMessage();
                $metadata['assignments_fetch_error_code'] = $e instanceof GraphException
                    ? ($e->status ?? null)
                    : (is_numeric($e->getCode()) ? (int) $e->getCode() : null);

                Log::warning('Failed to backfill assignments for version', [
                    'version_id' => $version->id,
                    'error' => $e->getMessage(),
                ]);
            }
        }

        // Fetch scope tags
        if ($includeScopeTags && $version->scope_tags === null) {
            $scopeTagIds = $version->snapshot['roleScopeTagIds'] ?? ['0'];
            $scopeTags = $this->resolveScopeTags($tenant, $scopeTagIds);
        }

        $updates = [];

        if ($includeAssignments && $version->assignments === null) {
            $redactedAssignments = $this->snapshotRedactor->redactAssignments($assignments);

            $updates['assignments'] = $redactedAssignments;
            $updates['assignments_hash'] = $redactedAssignments
                ? hash('sha256', json_encode($redactedAssignments, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE))
                : null;
        }

        if ($includeScopeTags && $version->scope_tags === null) {
            $redactedScopeTags = $this->snapshotRedactor->redactScopeTags($scopeTags);

            $updates['scope_tags'] = $redactedScopeTags;
            $updates['scope_tags_hash'] = $redactedScopeTags
                ? hash('sha256', json_encode($redactedScopeTags, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE))
                : null;
        }

        if (! empty($updates)) {
            $updates['metadata'] = $metadata;
            $version->update($updates);
        }

        Log::info('Version backfilled with capture data', [
            'version_id' => $version->id,
            'has_assignments' => ! is_null($assignments),
            'has_scope_tags' => ! is_null($scopeTags),
        ]);

        return $version->refresh();
    }

    /**
     * @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 matchesExistingVersionSnapshot(PolicyVersion $version, array $snapshot, string $snapshotHash): bool
    {
        $currentHash = $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,
        );

        if ($currentHash === $snapshotHash) {
            return true;
        }

        $hasLegacySnapshotContract = $this->fingerprintBucket($version, 'snapshot') === []
            && ! is_numeric($version->redaction_version);

        if (! $hasLegacySnapshotContract) {
            return false;
        }

        return $this->normalizeHashValue(is_array($version->snapshot) ? $version->snapshot : [])
            === $this->normalizeHashValue($snapshot);
    }

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