<?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 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 AssignmentFetcher $assignmentFetcher,
        private readonly GroupResolver $groupResolver,
        private readonly AssignmentFilterResolver $assignmentFilterResolver,
        private readonly ScopeTagResolver $scopeTagResolver,
    ) {}

    /**
     * Capture policy snapshot with optional assignments/scope tags.
     *
     * @return array ['version' => PolicyVersion, 'captured' => array]
     */
    public function capture(
        Policy $policy,
        Tenant $tenant,
        bool $includeAssignments = false,
        bool $includeScopeTags = false,
        ?string $createdBy = null,
        array $metadata = []
    ): array {
        $graphOptions = $tenant->graphOptions();
        $tenantIdentifier = $graphOptions['tenant'] ?? $tenant->graphTenantId() ?? (string) $tenant->getKey();

        // 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();

                Log::warning('Failed to fetch assignments during capture', [
                    'tenant_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);
        }

        // 4. Check if PolicyVersion with same snapshot already exists
        $snapshotHash = hash('sha256', json_encode($payload));

        // Find existing version by comparing snapshot content (database-agnostic)
        $existingVersion = PolicyVersion::where('policy_id', $policy->id)
            ->get()
            ->first(function ($version) use ($snapshotHash) {
                return hash('sha256', json_encode($version->snapshot)) === $snapshotHash;
            });

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

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

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

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

                Log::info('Backfilled existing PolicyVersion with capture data', [
                    'tenant_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' => $payload,
                        'assignments' => $assignments,
                        'scope_tags' => $scopeTags,
                        'metadata' => $captureMetadata,
                    ],
                ];
            }

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

            return [
                'version' => $existingVersion,
                'captured' => [
                    'payload' => $payload,
                    'assignments' => $assignments,
                    'scope_tags' => $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,
        );

        Log::info('Policy captured via orchestrator', [
            'tenant_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' => $payload,
                'assignments' => $assignments,
                'scope_tags' => $scopeTags,
                'metadata' => $captureMetadata,
            ],
        ];
    }

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

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

                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) {
            $updates['assignments'] = $assignments;
            $updates['assignments_hash'] = $assignments ? hash('sha256', json_encode($assignments)) : null;
        }

        if ($includeScopeTags && $version->scope_tags === null) {
            $updates['scope_tags'] = $scopeTags;
            $updates['scope_tags_hash'] = $scopeTags ? hash('sha256', json_encode($scopeTags)) : 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(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,
        ];
    }
}
