<?php

namespace App\Services\Drift;

use App\Models\PolicyVersion;
use App\Models\ManagedEnvironment;
use App\Services\Directory\EntraGroupLabelResolver;
use App\Services\Drift\Normalizers\AssignmentsNormalizer;
use App\Services\Drift\Normalizers\ScopeTagsNormalizer;
use App\Services\Drift\Normalizers\SettingsNormalizer;
use App\Services\Intune\VersionDiff;
use App\Support\References\ReferencePresentationVariant;
use App\Support\References\ResolvedReferencePresenter;
use App\Support\References\Resolvers\AssignmentTargetReferenceResolver;

class DriftFindingDiffBuilder
{
    public function __construct(
        private readonly SettingsNormalizer $settingsNormalizer,
        private readonly AssignmentsNormalizer $assignmentsNormalizer,
        private readonly ScopeTagsNormalizer $scopeTagsNormalizer,
        private readonly VersionDiff $versionDiff,
        private readonly EntraGroupLabelResolver $groupLabelResolver,
        private readonly AssignmentTargetReferenceResolver $assignmentTargetReferenceResolver,
        private readonly ResolvedReferencePresenter $resolvedReferencePresenter,
    ) {}

    /**
     * @return array<string, mixed>
     */
    public function buildSettingsDiff(?PolicyVersion $baselineVersion, ?PolicyVersion $currentVersion): array
    {
        $policyType = $currentVersion?->policy_type ?? $baselineVersion?->policy_type ?? '';
        $platform = $currentVersion?->platform ?? $baselineVersion?->platform;

        $from = $baselineVersion
            ? $this->settingsNormalizer->normalizeForDiff(is_array($baselineVersion->snapshot) ? $baselineVersion->snapshot : [], (string) $policyType, $platform)
            : [];

        $to = $currentVersion
            ? $this->settingsNormalizer->normalizeForDiff(is_array($currentVersion->snapshot) ? $currentVersion->snapshot : [], (string) $policyType, $platform)
            : [];

        $result = $this->versionDiff->compare($from, $to);
        $result['policy_type'] = $policyType;

        $protectedChanges = $this->protectedPointerChanges($baselineVersion, $currentVersion, 'snapshot');

        if ($protectedChanges !== []) {
            $existingChanged = is_array($result['changed'] ?? null) ? $result['changed'] : [];

            foreach ($protectedChanges as $pointer) {
                $existingChanged['Protected > '.$pointer.' (value changed)'] = [
                    'from' => '[REDACTED]',
                    'to' => '[REDACTED]',
                ];
            }

            $result['changed'] = $existingChanged;
            $result['summary']['changed'] = count($existingChanged);
            $result['summary']['message'] = sprintf(
                '%d added, %d removed, %d changed (%d protected value change%s)',
                count(is_array($result['added'] ?? null) ? $result['added'] : []),
                count(is_array($result['removed'] ?? null) ? $result['removed'] : []),
                count($existingChanged),
                count($protectedChanges),
                count($protectedChanges) === 1 ? '' : 's',
            );
        }

        return $result;
    }

    /**
     * @return array<string, mixed>
     */
    public function buildAssignmentsDiff(ManagedEnvironment $tenant, ?PolicyVersion $baselineVersion, ?PolicyVersion $currentVersion, int $limit = 200): array
    {
        $baseline = $baselineVersion ? $this->assignmentsNormalizer->normalizeForDiff($baselineVersion->assignments) : [];
        $current = $currentVersion ? $this->assignmentsNormalizer->normalizeForDiff($currentVersion->assignments) : [];

        $baselineMap = [];
        foreach ($baseline as $row) {
            $baselineMap[$row['key']] = $row;
        }

        $currentMap = [];
        foreach ($current as $row) {
            $currentMap[$row['key']] = $row;
        }

        $allKeys = array_values(array_unique(array_merge(array_keys($baselineMap), array_keys($currentMap))));
        sort($allKeys);

        $added = [];
        $removed = [];
        $changed = [];

        foreach ($allKeys as $key) {
            $from = $baselineMap[$key] ?? null;
            $to = $currentMap[$key] ?? null;

            if ($from === null && is_array($to)) {
                $added[] = $to;

                continue;
            }

            if ($to === null && is_array($from)) {
                $removed[] = $from;

                continue;
            }

            if (! is_array($from) || ! is_array($to)) {
                continue;
            }

            $diffFields = [
                'filter_type',
                'filter_id',
                'intent',
                'mode',
            ];

            $fieldChanges = [];

            foreach ($diffFields as $field) {
                $fromValue = $from[$field] ?? null;
                $toValue = $to[$field] ?? null;

                if ($fromValue !== $toValue) {
                    $fieldChanges[$field] = [
                        'from' => $fromValue,
                        'to' => $toValue,
                    ];
                }
            }

            if ($fieldChanges !== []) {
                $changed[] = [
                    'key' => $key,
                    'include_exclude' => $to['include_exclude'],
                    'target_type' => $to['target_type'],
                    'target_id' => $to['target_id'],
                    'from' => $from,
                    'to' => $to,
                    'changes' => $fieldChanges,
                ];
            }
        }

        $truncated = false;

        $total = count($added) + count($removed) + count($changed);
        if ($total > $limit) {
            $truncated = true;

            $budget = $limit;

            $changed = array_slice($changed, 0, min(count($changed), $budget));
            $budget -= count($changed);

            $added = array_slice($added, 0, min(count($added), $budget));
            $budget -= count($added);

            $removed = array_slice($removed, 0, min(count($removed), $budget));
        }

        $groupDescriptions = $this->groupDescriptionsForDiff($tenant, $added, $removed, $changed);

        $decorateAssignment = function (array $row) use ($groupDescriptions, $tenant): array {
            $row['target_reference'] = $this->targetReference($tenant, $row, $groupDescriptions);
            $row['target_label'] = (string) data_get($row, 'target_reference.primaryLabel', 'Unknown target');

            return $row;
        };

        $decorateChanged = function (array $row) use ($decorateAssignment): array {
            $row['from'] = is_array($row['from'] ?? null) ? $decorateAssignment($row['from']) : $row['from'];
            $row['to'] = is_array($row['to'] ?? null) ? $decorateAssignment($row['to']) : $row['to'];
            $row['target_label'] = is_array($row['to'] ?? null) ? ($row['to']['target_label'] ?? null) : null;

            return $row;
        };

        return [
            'summary' => [
                'added' => count($added),
                'removed' => count($removed),
                'changed' => count($changed),
                'message' => $this->assignmentSummaryMessage($baselineVersion, $currentVersion, count($added), count($removed), count($changed)),
                'truncated' => $truncated,
                'limit' => $limit,
            ],
            'added' => array_map($decorateAssignment, $added),
            'removed' => array_map($decorateAssignment, $removed),
            'changed' => array_map($decorateChanged, $changed),
        ];
    }

    /**
     * @return array<string, mixed>
     */
    public function buildScopeTagsDiff(?PolicyVersion $baselineVersion, ?PolicyVersion $currentVersion): array
    {
        $baselineIds = $baselineVersion ? ($this->scopeTagsNormalizer->normalizeIdsForHash($baselineVersion->scope_tags) ?? []) : [];
        $currentIds = $currentVersion ? ($this->scopeTagsNormalizer->normalizeIdsForHash($currentVersion->scope_tags) ?? []) : [];

        $baselineLabels = $baselineVersion ? $this->scopeTagsNormalizer->labelsById($baselineVersion->scope_tags) : [];
        $currentLabels = $currentVersion ? $this->scopeTagsNormalizer->labelsById($currentVersion->scope_tags) : [];

        $baselineSet = array_fill_keys($baselineIds, true);
        $currentSet = array_fill_keys($currentIds, true);

        $addedIds = array_values(array_diff($currentIds, $baselineIds));
        $removedIds = array_values(array_diff($baselineIds, $currentIds));

        sort($addedIds);
        sort($removedIds);

        $decorate = static function (array $ids, array $labels): array {
            $rows = [];

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

                $rows[] = [
                    'id' => $id,
                    'name' => $labels[$id] ?? ($id === '0' ? 'Default' : $id),
                ];
            }

            return $rows;
        };

        $protectedChanges = $this->protectedPointerChanges($baselineVersion, $currentVersion, 'scope_tags');

        return [
            'summary' => [
                'added' => count($addedIds),
                'removed' => count($removedIds),
                'changed' => 0,
                'message' => $protectedChanges === []
                    ? sprintf('%d added, %d removed', count($addedIds), count($removedIds))
                    : sprintf(
                        '%d added, %d removed (%d protected value change%s)',
                        count($addedIds),
                        count($removedIds),
                        count($protectedChanges),
                        count($protectedChanges) === 1 ? '' : 's',
                    ),
                'baseline_count' => count($baselineSet),
                'current_count' => count($currentSet),
            ],
            'added' => $decorate($addedIds, $currentLabels),
            'removed' => $decorate($removedIds, $baselineLabels),
            'baseline' => $decorate($baselineIds, $baselineLabels),
            'current' => $decorate($currentIds, $currentLabels),
            'changed' => [],
        ];
    }

    private function assignmentSummaryMessage(?PolicyVersion $baselineVersion, ?PolicyVersion $currentVersion, int $added, int $removed, int $changed): string
    {
        $protectedChanges = $this->protectedPointerChanges($baselineVersion, $currentVersion, 'assignments');

        if ($protectedChanges === []) {
            return sprintf('%d added, %d removed, %d changed', $added, $removed, $changed);
        }

        return sprintf(
            '%d added, %d removed, %d changed (%d protected value change%s)',
            $added,
            $removed,
            $changed,
            count($protectedChanges),
            count($protectedChanges) === 1 ? '' : 's',
        );
    }

    /**
     * @return array<int, string>
     */
    private function protectedPointerChanges(?PolicyVersion $baselineVersion, ?PolicyVersion $currentVersion, string $bucket): array
    {
        $baseline = $this->fingerprintBucket($baselineVersion, $bucket);
        $current = $this->fingerprintBucket($currentVersion, $bucket);

        $pointers = array_values(array_unique(array_merge(array_keys($baseline), array_keys($current))));
        sort($pointers);

        return array_values(array_filter($pointers, static function (string $pointer) use ($baseline, $current): bool {
            return ($baseline[$pointer] ?? null) !== ($current[$pointer] ?? null);
        }));
    }

    /**
     * @return array<string, string>
     */
    private function fingerprintBucket(?PolicyVersion $version, string $bucket): array
    {
        if (! $version instanceof PolicyVersion || ! is_array($version->secret_fingerprints)) {
            return [];
        }

        $bucketFingerprints = $version->secret_fingerprints[$bucket] ?? [];

        return is_array($bucketFingerprints) ? $bucketFingerprints : [];
    }

    /**
     * @param  array<int, array<string, mixed>>  $added
     * @param  array<int, array<string, mixed>>  $removed
     * @param  array<int, array<string, mixed>>  $changed
     * @return array<string, array{display_name: ?string, resolved: bool}>
     */
    private function groupDescriptionsForDiff(ManagedEnvironment $tenant, array $added, array $removed, array $changed): array
    {
        $groupIds = [];

        foreach ([$added, $removed] as $items) {
            foreach ($items as $row) {
                $targetType = $row['target_type'] ?? null;
                $targetId = $row['target_id'] ?? null;

                if (! is_string($targetType) || ! is_string($targetId)) {
                    continue;
                }

                if (! str_contains($targetType, 'groupassignmenttarget')) {
                    continue;
                }

                $groupIds[] = $targetId;
            }
        }

        foreach ($changed as $row) {
            $targetType = $row['target_type'] ?? null;
            $targetId = $row['target_id'] ?? null;

            if (! is_string($targetType) || ! is_string($targetId)) {
                continue;
            }

            if (! str_contains($targetType, 'groupassignmenttarget')) {
                continue;
            }

            $groupIds[] = $targetId;
        }

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

        if ($groupIds === []) {
            return [];
        }

        return $this->groupLabelResolver->describeMany($tenant, $groupIds);
    }

    /**
     * @param  array<string, mixed>  $assignment
     * @param  array<string, array{display_name: ?string, resolved: bool}>  $groupDescriptions
     * @return array<string, mixed>
     */
    private function targetReference(ManagedEnvironment $tenant, array $assignment, array $groupDescriptions): array
    {
        $targetType = is_string($assignment['target_type'] ?? null) ? (string) $assignment['target_type'] : '';
        $targetId = is_string($assignment['target_id'] ?? null) ? (string) $assignment['target_id'] : '';

        return $this->resolvedReferencePresenter->present(
            $this->assignmentTargetReferenceResolver->resolve([], [
                'managed_environment_id' => (int) $tenant->getKey(),
                'target_type' => $targetType,
                'target_id' => $targetId,
                'group_descriptions' => $groupDescriptions,
            ]),
            ReferencePresentationVariant::Compact,
        );
    }
}
