<?php

namespace App\Services\Intune;

use App\Models\BackupItem;
use App\Models\BackupSet;
use App\Models\PolicyVersion;
use App\Models\ManagedEnvironment;
use Carbon\CarbonImmutable;
use Illuminate\Support\Collection;

class RestoreDiffGenerator
{
    public function __construct(
        private readonly PolicyNormalizer $policyNormalizer,
        private readonly VersionDiff $versionDiff,
    ) {}

    /**
     * @param  array<int>|null  $selectedItemIds
     * @return array{summary: array<string, mixed>, diffs: array<int, array<string, mixed>>}
     */
    public function generate(ManagedEnvironment $tenant, BackupSet $backupSet, ?array $selectedItemIds = null): array
    {
        if ($backupSet->managed_environment_id !== $tenant->id) {
            throw new \InvalidArgumentException('Backup set does not belong to the provided tenant.');
        }

        if ($selectedItemIds === []) {
            $selectedItemIds = null;
        }

        $items = $this->loadItems($backupSet, $selectedItemIds);
        $policyItems = $items
            ->reject(fn (BackupItem $item): bool => $item->isFoundation())
            ->values();

        $policyIds = $policyItems
            ->pluck('policy_id')
            ->filter()
            ->unique()
            ->values()
            ->all();

        $latestVersions = $this->latestVersionsByPolicyId($tenant, $policyIds);

        $maxDetailedDiffs = 25;
        $maxEntriesPerSection = 200;

        $policiesChanged = 0;
        $assignmentsChanged = 0;
        $scopeTagsChanged = 0;

        $diffs = [];
        $diffsOmitted = 0;

        foreach ($policyItems as $index => $item) {
            $policyId = $item->policy_id ? (int) $item->policy_id : null;
            $currentVersion = $policyId ? ($latestVersions[$policyId] ?? null) : null;

            $currentSnapshot = is_array($currentVersion?->snapshot) ? $currentVersion->snapshot : [];
            $backupSnapshot = is_array($item->payload) ? $item->payload : [];

            $policyType = (string) ($item->policy_type ?? '');
            $platform = $item->platform;

            $from = $this->policyNormalizer->flattenForDiff($currentSnapshot, $policyType, $platform);
            $to = $this->policyNormalizer->flattenForDiff($backupSnapshot, $policyType, $platform);

            $diff = $this->versionDiff->compare($from, $to);
            $summary = $diff['summary'] ?? ['added' => 0, 'removed' => 0, 'changed' => 0];

            $hasPolicyChanges = ((int) ($summary['added'] ?? 0) + (int) ($summary['removed'] ?? 0) + (int) ($summary['changed'] ?? 0)) > 0;

            if ($hasPolicyChanges) {
                $policiesChanged++;
            }

            $assignmentDiff = $this->assignmentsChanged($item->assignments, $currentVersion?->assignments);
            if ($assignmentDiff) {
                $assignmentsChanged++;
            }

            $scopeTagDiff = $this->scopeTagsChanged($item, $currentVersion);
            if ($scopeTagDiff) {
                $scopeTagsChanged++;
            }

            $diffEntry = [
                'backup_item_id' => $item->id,
                'display_name' => $item->resolvedDisplayName(),
                'policy_identifier' => $item->policy_identifier,
                'policy_type' => $policyType,
                'platform' => $platform,
                'action' => $currentVersion ? 'update' : 'create',
                'diff' => [
                    'summary' => $summary,
                    'added' => [],
                    'removed' => [],
                    'changed' => [],
                ],
                'assignments_changed' => $assignmentDiff,
                'scope_tags_changed' => $scopeTagDiff,
                'diff_omitted' => false,
                'diff_truncated' => false,
            ];

            if ($index >= $maxDetailedDiffs) {
                $diffEntry['diff_omitted'] = true;
                $diffEntry['diff_truncated'] = true;
                $diffEntry['diff'] = [
                    'summary' => $summary,
                ];
                $diffsOmitted++;
                $diffs[] = $diffEntry;

                continue;
            }

            $added = is_array($diff['added'] ?? null) ? $diff['added'] : [];
            $removed = is_array($diff['removed'] ?? null) ? $diff['removed'] : [];
            $changed = is_array($diff['changed'] ?? null) ? $diff['changed'] : [];

            $diffEntry['diff_truncated'] = count($added) > $maxEntriesPerSection
                || count($removed) > $maxEntriesPerSection
                || count($changed) > $maxEntriesPerSection;

            $diffEntry['diff'] = [
                'summary' => $summary,
                'added' => array_slice($added, 0, $maxEntriesPerSection, true),
                'removed' => array_slice($removed, 0, $maxEntriesPerSection, true),
                'changed' => array_slice($changed, 0, $maxEntriesPerSection, true),
            ];

            $diffs[] = $diffEntry;
        }

        return [
            'summary' => [
                'generated_at' => CarbonImmutable::now()->toIso8601String(),
                'policies_total' => $policyItems->count(),
                'policies_changed' => $policiesChanged,
                'assignments_changed' => $assignmentsChanged,
                'scope_tags_changed' => $scopeTagsChanged,
                'diffs_detailed' => min($policyItems->count(), $maxDetailedDiffs),
                'diffs_omitted' => $diffsOmitted,
                'limits' => [
                    'max_detailed_diffs' => $maxDetailedDiffs,
                    'max_entries_per_section' => $maxEntriesPerSection,
                ],
            ],
            'diffs' => $diffs,
        ];
    }

    /**
     * @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  array<int, int>  $policyIds
     * @return array<int, PolicyVersion>
     */
    private function latestVersionsByPolicyId(ManagedEnvironment $tenant, array $policyIds): array
    {
        if ($policyIds === []) {
            return [];
        }

        $latestVersionsQuery = PolicyVersion::query()
            ->where('managed_environment_id', $tenant->id)
            ->whereIn('policy_id', $policyIds)
            ->selectRaw('policy_id, max(version_number) as version_number')
            ->groupBy('policy_id');

        return PolicyVersion::query()
            ->where('managed_environment_id', $tenant->id)
            ->joinSub($latestVersionsQuery, 'latest_versions', function ($join): void {
                $join->on('policy_versions.policy_id', '=', 'latest_versions.policy_id')
                    ->on('policy_versions.version_number', '=', 'latest_versions.version_number');
            })
            ->get()
            ->keyBy('policy_id')
            ->all();
    }

    private function assignmentsChanged(?array $backupAssignments, ?array $currentAssignments): bool
    {
        $backup = $this->normalizeAssignments($backupAssignments);
        $current = $this->normalizeAssignments($currentAssignments);

        return $backup !== $current;
    }

    private function scopeTagsChanged(BackupItem $backupItem, ?PolicyVersion $currentVersion): bool
    {
        $backupIds = $backupItem->scope_tag_ids;
        $backupIds = is_array($backupIds) ? $backupIds : [];
        $backupIds = array_values(array_filter($backupIds, fn (mixed $id): bool => is_string($id) && $id !== '' && $id !== '0'));
        sort($backupIds);

        $scopeTags = $currentVersion?->scope_tags;
        $currentIds = is_array($scopeTags) ? ($scopeTags['ids'] ?? []) : [];
        $currentIds = is_array($currentIds) ? $currentIds : [];
        $currentIds = array_values(array_filter($currentIds, fn (mixed $id): bool => is_string($id) && $id !== '' && $id !== '0'));
        sort($currentIds);

        return $backupIds !== $currentIds;
    }

    /**
     * @return array<int, array<string, mixed>>
     */
    private function normalizeAssignments(?array $assignments): array
    {
        $assignments = is_array($assignments) ? $assignments : [];

        $normalized = [];

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

            $normalized[] = $assignment;
        }

        usort($normalized, function (array $a, array $b): int {
            $left = json_encode($a, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?: '';
            $right = json_encode($b, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?: '';

            return $left <=> $right;
        });

        return $normalized;
    }
}
