<?php

namespace App\Services\Intune;

use App\Models\BackupItem;
use App\Models\BackupSet;
use App\Models\Policy;
use App\Models\PolicyVersion;
use App\Models\ManagedEnvironment;
use App\Services\Graph\GroupResolver;
use App\Services\Providers\MicrosoftGraphOptionsResolver;
use Carbon\CarbonImmutable;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;

class RestoreRiskChecker
{
    public function __construct(
        private readonly GroupResolver $groupResolver,
        private readonly ConfigurationPolicyTemplateResolver $templateResolver,
        private readonly MicrosoftGraphOptionsResolver $graphOptionsResolver,
    ) {}

    /**
     * @param  array<int>|null  $selectedItemIds
     * @param  array<string, string|null>  $groupMapping
     * @return array{summary: array{blocking: int, warning: int, safe: int, has_blockers: bool}, results: array<int, array{code: string, severity: string, title: string, message: string, meta: array<string, mixed>}>}
     */
    public function check(ManagedEnvironment $tenant, BackupSet $backupSet, ?array $selectedItemIds = null, array $groupMapping = []): array
    {
        if ($backupSet->managed_environment_id !== $tenant->id) {
            throw new \InvalidArgumentException('Backup set does not belong to the provided tenant.');
        }

        $items = $this->loadItems($backupSet, $selectedItemIds);

        $policyItems = $items
            ->reject(fn (BackupItem $item): bool => $item->isFoundation())
            ->values();

        $results = [];

        $results[] = $this->checkOrphanedGroups($tenant, $policyItems, $groupMapping);
        $results[] = $this->checkMetadataOnlySnapshots($policyItems);
        $results[] = $this->checkPreviewOnlyPolicies($items);
        $results[] = $this->checkEndpointSecurityTemplates($tenant, $policyItems);
        $results[] = $this->checkMissingPolicies($tenant, $policyItems);
        $results[] = $this->checkStalePolicies($tenant, $policyItems);
        $results[] = $this->checkMissingScopeTagsInScope($items, $policyItems, $selectedItemIds !== null);

        $results = array_values(array_filter($results));

        $summary = [
            'blocking' => 0,
            'warning' => 0,
            'safe' => 0,
            'has_blockers' => false,
        ];

        foreach ($results as $result) {
            $severity = $result['severity'] ?? 'safe';

            if (! in_array($severity, ['blocking', 'warning', 'safe'], true)) {
                $severity = 'safe';
            }

            $summary[$severity]++;
        }

        $summary['has_blockers'] = $summary['blocking'] > 0;

        return [
            'summary' => $summary,
            'results' => $results,
        ];
    }

    /**
     * @param  array<int>|null  $selectedItemIds
     * @return Collection<int, BackupItem>
     */
    private function loadItems(BackupSet $backupSet, ?array $selectedItemIds): Collection
    {
        $query = $backupSet->items()->getQuery();

        if ($selectedItemIds !== null) {
            $query->whereIn('id', $selectedItemIds);
        }

        return $query->orderBy('id')->get();
    }

    /**
     * @param  Collection<int, BackupItem>  $policyItems
     * @param  array<string, string|null>  $groupMapping
     * @return array{code: string, severity: string, title: string, message: string, meta: array<string, mixed>}|null
     */
    private function checkOrphanedGroups(ManagedEnvironment $tenant, Collection $policyItems, array $groupMapping): ?array
    {
        [$groupIds, $sourceNames] = $this->extractGroupIds($policyItems);

        if ($groupIds === []) {
            return [
                'code' => 'assignment_groups',
                'severity' => 'safe',
                'title' => 'Assignments',
                'message' => 'No group-based assignments detected.',
                'meta' => [
                    'group_count' => 0,
                ],
            ];
        }

        $graphOptions = $this->graphOptionsResolver->resolveForTenant($tenant);
        $tenantIdentifier = (string) ($graphOptions['tenant'] ?? '');
        $resolved = $this->groupResolver->resolveGroupIds($groupIds, $tenantIdentifier, $graphOptions);

        $orphaned = [];

        foreach ($groupIds as $groupId) {
            $group = $resolved[$groupId] ?? null;

            if (! is_array($group) || ! ($group['orphaned'] ?? false)) {
                continue;
            }

            $orphaned[] = [
                'id' => $groupId,
                'label' => $this->formatGroupLabel($sourceNames[$groupId] ?? null, $groupId),
            ];
        }

        if ($orphaned === []) {
            return [
                'code' => 'assignment_groups',
                'severity' => 'safe',
                'title' => 'Assignments',
                'message' => sprintf('%d group assignment targets resolved.', count($groupIds)),
                'meta' => [
                    'group_count' => count($groupIds),
                    'orphaned_count' => 0,
                ],
            ];
        }

        $unmapped = [];
        $mapped = [];
        $skipped = [];

        foreach ($orphaned as $group) {
            $groupId = $group['id'];
            $mapping = $groupMapping[$groupId] ?? null;

            if (! is_string($mapping) || $mapping === '') {
                $unmapped[] = $group;

                continue;
            }

            if ($mapping === 'SKIP') {
                $skipped[] = $group;

                continue;
            }

            $mapped[] = $group + [
                'mapped_to' => $mapping,
            ];
        }

        $severity = $unmapped !== [] ? 'blocking' : 'warning';

        $message = $unmapped !== []
            ? sprintf('%d group assignment targets are missing in the tenant and require mapping (or skip).', count($unmapped))
            : sprintf('%d group assignment targets are missing in the tenant (mapped/skipped).', count($orphaned));

        return [
            'code' => 'assignment_groups',
            'severity' => $severity,
            'title' => 'Assignments',
            'message' => $message,
            'meta' => [
                'group_count' => count($groupIds),
                'orphaned_count' => count($orphaned),
                'unmapped' => $unmapped,
                'mapped' => $mapped,
                'skipped' => $skipped,
            ],
        ];
    }

    /**
     * @param  Collection<int, BackupItem>  $items
     * @return array{code: string, severity: string, title: string, message: string, meta: array<string, mixed>}|null
     */
    private function checkPreviewOnlyPolicies(Collection $items): ?array
    {
        $byType = [];

        foreach ($items as $item) {
            $restoreMode = $this->resolveRestoreMode($item->policy_type);

            if ($restoreMode !== 'preview-only') {
                continue;
            }

            $label = $this->resolveTypeLabel($item->policy_type);
            $byType[$label] ??= 0;
            $byType[$label]++;
        }

        if ($byType === []) {
            return [
                'code' => 'preview_only',
                'severity' => 'safe',
                'title' => 'Preview-only types',
                'message' => 'No preview-only policy types detected.',
                'meta' => [
                    'count' => 0,
                ],
            ];
        }

        return [
            'code' => 'preview_only',
            'severity' => 'warning',
            'title' => 'Preview-only types',
            'message' => 'Some selected items are preview-only and will never execute.',
            'meta' => [
                'count' => array_sum($byType),
                'types' => $byType,
            ],
        ];
    }

    /**
     * Validate that Endpoint Security policy templates referenced by snapshots exist in the tenant.
     *
     * @param  Collection<int, BackupItem>  $policyItems
     * @return array{code: string, severity: string, title: string, message: string, meta: array<string, mixed>}|null
     */
    private function checkEndpointSecurityTemplates(ManagedEnvironment $tenant, Collection $policyItems): ?array
    {
        $issues = [];
        $hasRestoreEnabled = false;
        $graphOptions = $this->graphOptionsResolver->resolveForTenant($tenant);

        foreach ($policyItems as $item) {
            if ($item->policy_type !== 'endpointSecurityPolicy') {
                continue;
            }

            $restoreMode = $this->resolveRestoreMode($item->policy_type);

            if ($restoreMode !== 'preview-only') {
                $hasRestoreEnabled = true;
            }

            $payload = is_array($item->payload) ? $item->payload : [];
            $templateReference = $payload['templateReference'] ?? null;

            if (is_string($templateReference)) {
                $decoded = json_decode($templateReference, true);
                $templateReference = is_array($decoded) ? $decoded : null;
            }

            if (! is_array($templateReference)) {
                $issues[] = [
                    'backup_item_id' => $item->id,
                    'policy_identifier' => $item->policy_identifier,
                    'label' => $item->resolvedDisplayName(),
                    'reason' => 'Missing templateReference in snapshot.',
                ];

                continue;
            }

            $outcome = $this->templateResolver->resolveTemplateReference($tenant, $templateReference, $graphOptions);

            if (! ($outcome['success'] ?? false)) {
                $issues[] = [
                    'backup_item_id' => $item->id,
                    'policy_identifier' => $item->policy_identifier,
                    'label' => $item->resolvedDisplayName(),
                    'template_id' => $templateReference['templateId'] ?? null,
                    'template_family' => $templateReference['templateFamily'] ?? null,
                    'reason' => $outcome['reason'] ?? 'Template could not be resolved in the tenant.',
                ];
            }
        }

        if ($issues === []) {
            return [
                'code' => 'endpoint_security_templates',
                'severity' => 'safe',
                'title' => 'Endpoint security templates',
                'message' => 'All referenced Endpoint Security templates are available.',
                'meta' => [
                    'count' => 0,
                ],
            ];
        }

        $severity = $hasRestoreEnabled ? 'blocking' : 'warning';
        $message = $hasRestoreEnabled
            ? 'Some Endpoint Security templates are missing or cannot be resolved in the tenant.'
            : 'Some Endpoint Security templates are missing or cannot be resolved (execution is preview-only).';

        return [
            'code' => 'endpoint_security_templates',
            'severity' => $severity,
            'title' => 'Endpoint security templates',
            'message' => $message,
            'meta' => [
                'count' => count($issues),
                'items' => $this->truncateList($issues, 10),
            ],
        ];
    }

    /**
     * Detect snapshots that were captured as metadata-only.
     *
     * These snapshots cannot be safely restored because they do not contain the
     * complete settings payload.
     *
     * @param  Collection<int, BackupItem>  $policyItems
     * @return array{code: string, severity: string, title: string, message: string, meta: array<string, mixed>}|null
     */
    private function checkMetadataOnlySnapshots(Collection $policyItems): ?array
    {
        $affected = [];
        $hasRestoreEnabled = false;

        foreach ($policyItems as $item) {
            if (! $this->isMetadataOnlySnapshot($item)) {
                continue;
            }

            $restoreMode = $this->resolveRestoreMode($item->policy_type);
            if ($restoreMode !== 'preview-only') {
                $hasRestoreEnabled = true;
            }

            $affected[] = [
                'backup_item_id' => $item->id,
                'policy_identifier' => $item->policy_identifier,
                'policy_type' => $item->policy_type,
                'label' => $item->resolvedDisplayName(),
                'restore_mode' => $restoreMode,
            ];
        }

        if ($affected === []) {
            return [
                'code' => 'metadata_only',
                'severity' => 'safe',
                'title' => 'Snapshot completeness',
                'message' => 'No metadata-only snapshots detected.',
                'meta' => [
                    'count' => 0,
                ],
            ];
        }

        $severity = $hasRestoreEnabled ? 'blocking' : 'warning';
        $message = $hasRestoreEnabled
            ? 'Some selected items were captured as metadata-only. Restore cannot execute until Graph works again.'
            : 'Some selected items were captured as metadata-only. Execution is preview-only, but payload completeness is limited.';

        return [
            'code' => 'metadata_only',
            'severity' => $severity,
            'title' => 'Snapshot completeness',
            'message' => $message,
            'meta' => [
                'count' => count($affected),
                'items' => $this->truncateList($affected, 10),
            ],
        ];
    }

    private function isMetadataOnlySnapshot(BackupItem $item): bool
    {
        $metadata = is_array($item->metadata) ? $item->metadata : [];

        $source = $metadata['source'] ?? null;
        $snapshotSource = $metadata['snapshot_source'] ?? null;

        if ($source === 'metadata_only' || $snapshotSource === 'metadata_only') {
            return true;
        }

        $warnings = $metadata['warnings'] ?? null;
        if (is_array($warnings)) {
            foreach ($warnings as $warning) {
                if (is_string($warning) && Str::contains(Str::lower($warning), 'metadata only')) {
                    return true;
                }
            }
        }

        return false;
    }

    /**
     * @param  Collection<int, BackupItem>  $policyItems
     * @return array{code: string, severity: string, title: string, message: string, meta: array<string, mixed>}|null
     */
    private function checkMissingPolicies(ManagedEnvironment $tenant, Collection $policyItems): ?array
    {
        $pairs = [];

        foreach ($policyItems as $item) {
            $identifier = $item->policy_identifier;
            $type = $item->policy_type;

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

            $pairs[] = [
                'identifier' => $identifier,
                'type' => $type,
                'label' => $item->resolvedDisplayName(),
            ];
        }

        if ($pairs === []) {
            return [
                'code' => 'missing_policies',
                'severity' => 'safe',
                'title' => 'Target policies',
                'message' => 'No policy identifiers available to verify.',
                'meta' => [
                    'missing_count' => 0,
                ],
            ];
        }

        $identifiers = array_values(array_unique(array_column($pairs, 'identifier')));
        $types = array_values(array_unique(array_column($pairs, 'type')));

        $existing = Policy::query()
            ->where('managed_environment_id', $tenant->id)
            ->whereIn('external_id', $identifiers)
            ->whereIn('policy_type', $types)
            ->get(['id', 'external_id', 'policy_type'])
            ->mapWithKeys(fn (Policy $policy) => [$this->policyKey($policy->policy_type, $policy->external_id) => $policy->id])
            ->all();

        $missing = [];

        foreach ($pairs as $pair) {
            $key = $this->policyKey($pair['type'], $pair['identifier']);

            if (array_key_exists($key, $existing)) {
                continue;
            }

            $missing[] = [
                'type' => $pair['type'],
                'identifier' => $pair['identifier'],
                'label' => $pair['label'],
            ];
        }

        $missing = array_values(collect($missing)->unique(fn (array $row) => $this->policyKey($row['type'], $row['identifier']))->all());

        if ($missing === []) {
            return [
                'code' => 'missing_policies',
                'severity' => 'safe',
                'title' => 'Target policies',
                'message' => 'All policies exist in the tenant (restore will update).',
                'meta' => [
                    'missing_count' => 0,
                ],
            ];
        }

        return [
            'code' => 'missing_policies',
            'severity' => 'warning',
            'title' => 'Target policies',
            'message' => sprintf('%d policies do not exist in the tenant and will be created.', count($missing)),
            'meta' => [
                'missing_count' => count($missing),
                'missing' => $this->truncateList($missing, 10),
            ],
        ];
    }

    /**
     * @param  Collection<int, BackupItem>  $policyItems
     * @return array{code: string, severity: string, title: string, message: string, meta: array<string, mixed>}|null
     */
    private function checkStalePolicies(ManagedEnvironment $tenant, Collection $policyItems): ?array
    {
        $itemsByPolicyId = [];

        foreach ($policyItems as $item) {
            if (! $item->policy_id) {
                continue;
            }

            $capturedAt = $item->captured_at;

            if (! $capturedAt) {
                continue;
            }

            $itemsByPolicyId[$item->policy_id][] = [
                'backup_item_id' => $item->id,
                'captured_at' => $capturedAt,
                'label' => $item->resolvedDisplayName(),
            ];
        }

        if ($itemsByPolicyId === []) {
            return [
                'code' => 'stale_policies',
                'severity' => 'safe',
                'title' => 'Staleness',
                'message' => 'No captured timestamps available to evaluate staleness.',
                'meta' => [
                    'stale_count' => 0,
                ],
            ];
        }

        $latestVersions = PolicyVersion::query()
            ->where('managed_environment_id', $tenant->id)
            ->whereIn('policy_id', array_keys($itemsByPolicyId))
            ->selectRaw('policy_id, max(captured_at) as latest_captured_at')
            ->groupBy('policy_id')
            ->get()
            ->mapWithKeys(function (PolicyVersion $version) {
                $latestCapturedAt = $version->getAttribute('latest_captured_at');

                if (is_string($latestCapturedAt) && $latestCapturedAt !== '') {
                    $latestCapturedAt = CarbonImmutable::parse($latestCapturedAt);
                } else {
                    $latestCapturedAt = null;
                }

                return [
                    (int) $version->policy_id => $latestCapturedAt,
                ];
            })
            ->all();

        $stale = [];

        foreach ($itemsByPolicyId as $policyId => $policyItems) {
            $latestCapturedAt = $latestVersions[(int) $policyId] ?? null;

            if (! $latestCapturedAt) {
                continue;
            }

            foreach ($policyItems as $policyItem) {
                if ($latestCapturedAt->greaterThan($policyItem['captured_at'])) {
                    $stale[] = [
                        'backup_item_id' => $policyItem['backup_item_id'],
                        'label' => $policyItem['label'],
                        'snapshot_captured_at' => $policyItem['captured_at']->toIso8601String(),
                        'latest_captured_at' => $latestCapturedAt->toIso8601String(),
                    ];
                }
            }
        }

        if ($stale === []) {
            return [
                'code' => 'stale_policies',
                'severity' => 'safe',
                'title' => 'Staleness',
                'message' => 'No newer versions detected since the snapshot.',
                'meta' => [
                    'stale_count' => 0,
                ],
            ];
        }

        return [
            'code' => 'stale_policies',
            'severity' => 'warning',
            'title' => 'Staleness',
            'message' => sprintf('%d policies have newer versions in the tenant than this snapshot.', count($stale)),
            'meta' => [
                'stale_count' => count($stale),
                'stale' => $this->truncateList($stale, 10),
            ],
        ];
    }

    /**
     * @param  Collection<int, BackupItem>  $items
     * @param  Collection<int, BackupItem>  $policyItems
     * @return array{code: string, severity: string, title: string, message: string, meta: array<string, mixed>}|null
     */
    private function checkMissingScopeTagsInScope(Collection $items, Collection $policyItems, bool $isSelectedScope): ?array
    {
        if (! $isSelectedScope) {
            return [
                'code' => 'scope_tags_in_scope',
                'severity' => 'safe',
                'title' => 'Scope tags',
                'message' => 'Scope includes all items; foundations are available if present in the backup set.',
                'meta' => [
                    'missing_scope_tags' => false,
                ],
            ];
        }

        $selectedScopeTagCount = $items->where('policy_type', 'roleScopeTag')->count();

        $scopeTagIds = [];

        foreach ($policyItems as $item) {
            $ids = $item->scope_tag_ids;

            if (! is_array($ids)) {
                continue;
            }

            foreach ($ids as $id) {
                if (! is_string($id) || $id === '' || $id === '0') {
                    continue;
                }

                $scopeTagIds[] = $id;
            }
        }

        $scopeTagIds = array_values(array_unique($scopeTagIds));

        if ($scopeTagIds === [] || $selectedScopeTagCount > 0) {
            return [
                'code' => 'scope_tags_in_scope',
                'severity' => 'safe',
                'title' => 'Scope tags',
                'message' => 'Scope tags look OK for the selected items.',
                'meta' => [
                    'missing_scope_tags' => false,
                    'referenced_scope_tags' => count($scopeTagIds),
                    'selected_scope_tag_items' => $selectedScopeTagCount,
                ],
            ];
        }

        return [
            'code' => 'scope_tags_in_scope',
            'severity' => 'warning',
            'title' => 'Scope tags',
            'message' => 'Policies reference scope tags, but scope tags are not included in the selected restore scope.',
            'meta' => [
                'missing_scope_tags' => true,
                'referenced_scope_tags' => count($scopeTagIds),
                'selected_scope_tag_items' => 0,
            ],
        ];
    }

    /**
     * @param  Collection<int, BackupItem>  $policyItems
     * @return array{0: array<int, string>, 1: array<string, string>}
     */
    private function extractGroupIds(Collection $policyItems): array
    {
        $groupIds = [];
        $sourceNames = [];

        foreach ($policyItems as $item) {
            if (! is_array($item->assignments) || $item->assignments === []) {
                continue;
            }

            foreach ($item->assignments as $assignment) {
                if (! is_array($assignment)) {
                    continue;
                }

                $target = $assignment['target'] ?? [];
                $odataType = $target['@odata.type'] ?? '';

                if (! in_array($odataType, [
                    '#microsoft.graph.groupAssignmentTarget',
                    '#microsoft.graph.exclusionGroupAssignmentTarget',
                ], true)) {
                    continue;
                }

                $groupId = $target['groupId'] ?? null;

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

                $groupIds[] = $groupId;

                $displayName = $target['group_display_name'] ?? null;

                if (is_string($displayName) && $displayName !== '') {
                    $sourceNames[$groupId] = $displayName;
                }
            }
        }

        $groupIds = array_values(array_unique($groupIds));

        return [$groupIds, $sourceNames];
    }

    private function formatGroupLabel(?string $name, string $id): string
    {
        $parts = [];

        if (is_string($name) && $name !== '') {
            $parts[] = $name;
        }

        $parts[] = Str::limit($id, 24, '...');

        return implode(' • ', $parts);
    }

    private function policyKey(string $type, string $identifier): string
    {
        return $type.'|'.$identifier;
    }

    /**
     * @return array<string, mixed>
     */
    private function resolveTypeMeta(?string $type): array
    {
        if (! is_string($type) || $type === '') {
            return [];
        }

        $types = array_merge(
            config('tenantpilot.supported_policy_types', []),
            config('tenantpilot.foundation_types', [])
        );

        foreach ($types as $typeConfig) {
            if (($typeConfig['type'] ?? null) === $type) {
                return is_array($typeConfig) ? $typeConfig : [];
            }
        }

        return [];
    }

    private function resolveRestoreMode(?string $policyType): string
    {
        $meta = $this->resolveTypeMeta($policyType);

        if ($meta === []) {
            return 'preview-only';
        }

        $restore = $meta['restore'] ?? 'enabled';

        if (! is_string($restore) || $restore === '') {
            return 'enabled';
        }

        return $restore;
    }

    private function resolveTypeLabel(?string $policyType): string
    {
        $meta = $this->resolveTypeMeta($policyType);

        return (string) ($meta['label'] ?? $policyType ?? 'Unknown');
    }

    /**
     * @param  array<int, array<string, mixed>>  $items
     * @return array<int, array<string, mixed>>
     */
    private function truncateList(array $items, int $limit): array
    {
        if (count($items) <= $limit) {
            return $items;
        }

        return array_slice($items, 0, $limit);
    }
}
