<?php

namespace App\Services\Intune;

use DateInterval;
use Illuminate\Support\Str;

class AppProtectionPolicyNormalizer implements PolicyTypeNormalizer
{
    public function __construct(
        private readonly DefaultPolicyNormalizer $defaultNormalizer,
    ) {}

    public function supports(string $policyType): bool
    {
        return $policyType === 'appProtectionPolicy';
    }

    /**
     * @return array{status: string, settings: array<int, array<string, mixed>>, settings_table?: array<string, mixed>, warnings: array<int, string>}
     */
    public function normalize(?array $snapshot, string $policyType, ?string $platform = null): array
    {
        $snapshot = $snapshot ?? [];
        $normalized = $this->defaultNormalizer->normalize($snapshot, $policyType, $platform);

        if ($snapshot === []) {
            return $normalized;
        }

        $normalized['settings'] = array_values(array_filter(
            $normalized['settings'],
            fn (array $block) => strtolower((string) ($block['title'] ?? '')) !== 'general'
        ));

        foreach ($this->buildBlocks($snapshot) as $block) {
            $normalized['settings'][] = $block;
        }

        return $normalized;
    }

    /**
     * @return array<string, mixed>
     */
    public function flattenForDiff(?array $snapshot, string $policyType, ?string $platform = null): array
    {
        $snapshot = $snapshot ?? [];
        $normalized = $this->normalize($snapshot, $policyType, $platform);

        return $this->defaultNormalizer->flattenNormalizedForDiff($normalized);
    }

    /**
     * @return array<int, array<string, mixed>>
     */
    private function buildBlocks(array $snapshot): array
    {
        $blocks = [];
        $groups = $this->groupedFields();
        $usedKeys = [];

        foreach ($groups as $title => $group) {
            $rows = $this->buildRows($snapshot, $group['keys'], $group['labels'] ?? []);

            if ($rows === []) {
                continue;
            }

            if ($title === 'Basics') {
                $platformLabel = $this->platformLabelFromOdataType($snapshot['@odata.type'] ?? null);

                if ($platformLabel !== null) {
                    array_unshift($rows, [
                        'path' => '@odata.type',
                        'label' => 'Platform',
                        'value' => $platformLabel,
                    ]);
                }
            }

            $blocks[] = [
                'type' => 'table',
                'title' => $title,
                'rows' => $rows,
            ];

            $usedKeys = array_merge($usedKeys, $group['keys']);
        }

        $additionalRows = $this->buildAdditionalRows($snapshot, $usedKeys);

        if ($additionalRows !== []) {
            $blocks[] = [
                'type' => 'table',
                'title' => 'Additional Settings',
                'rows' => $additionalRows,
            ];
        }

        return $blocks;
    }

    /**
     * @return array{keys: array<int, string>, labels?: array<string, string>}
     */
    private function groupedFields(): array
    {
        return [
            'Basics' => [
                'keys' => [
                    'displayName',
                    'description',
                    'appGroupType',
                    'isAssigned',
                    'deployedAppCount',
                ],
                'labels' => [
                    'displayName' => 'Name',
                    'description' => 'Description',
                    'appGroupType' => 'App group type',
                    'isAssigned' => 'Assigned',
                    'deployedAppCount' => 'Deployed app count',
                ],
            ],
            'Data Protection' => [
                'keys' => [
                    'dataBackupBlocked',
                    'printBlocked',
                    'saveAsBlocked',
                    'screenCaptureBlocked',
                    'allowedInboundDataTransferSources',
                    'allowedOutboundDataTransferDestinations',
                    'allowedDataIngestionLocations',
                    'allowedOutboundClipboardSharingLevel',
                ],
                'labels' => [
                    'dataBackupBlocked' => 'Prevent backups',
                    'printBlocked' => 'Printing org data',
                    'saveAsBlocked' => 'Save copies of org data',
                    'screenCaptureBlocked' => 'Screen capture',
                    'allowedInboundDataTransferSources' => 'Receive data from other apps',
                    'allowedOutboundDataTransferDestinations' => 'Send org data to other apps',
                    'allowedDataIngestionLocations' => 'Allow users to open data from selected services',
                    'allowedOutboundClipboardSharingLevel' => 'Restrict cut, copy, and paste',
                ],
            ],
            'Access Requirements' => [
                'keys' => [
                    'pinRequired',
                    'pinCharacterSet',
                    'minimumPinLength',
                    'simplePinBlocked',
                    'maximumPinRetries',
                    'fingerprintAndBiometricEnabled',
                    'pinRequiredInsteadOfBiometricTimeout',
                    'periodOnlineBeforeAccessCheck',
                    'periodOfflineBeforeAccessCheck',
                ],
                'labels' => [
                    'pinRequired' => 'PIN for access',
                    'pinCharacterSet' => 'PIN type',
                    'minimumPinLength' => 'Minimum PIN length',
                    'simplePinBlocked' => 'Block simple PIN',
                    'maximumPinRetries' => 'Max PIN attempts',
                    'fingerprintAndBiometricEnabled' => 'Biometrics instead of PIN',
                    'pinRequiredInsteadOfBiometricTimeout' => 'Override biometrics with PIN after timeout',
                    'periodOnlineBeforeAccessCheck' => 'Recheck access requirements after',
                    'periodOfflineBeforeAccessCheck' => 'Offline grace period (block access)',
                ],
            ],
            'Conditional Launch' => [
                'keys' => [
                    'periodOfflineBeforeWipeIsEnforced',
                    'appActionIfMaximumPinRetriesExceeded',
                    'appActionIfDeviceLockNotSet',
                    'appActionIfDeviceComplianceRequired',
                    'maximumAllowedDeviceThreatLevel',
                    'mobileThreatDefenseRemediationAction',
                ],
                'labels' => [
                    'periodOfflineBeforeWipeIsEnforced' => 'Offline grace period (wipe data)',
                    'appActionIfMaximumPinRetriesExceeded' => 'Action if max PIN retries exceeded',
                    'appActionIfDeviceLockNotSet' => 'Action if device lock not set',
                    'appActionIfDeviceComplianceRequired' => 'Action if device compliance required',
                    'maximumAllowedDeviceThreatLevel' => 'Maximum allowed device threat level',
                    'mobileThreatDefenseRemediationAction' => 'Threat defense remediation action',
                ],
            ],
        ];
    }

    /**
     * @param  array<string, mixed>  $labels
     * @return array<int, array<string, mixed>>
     */
    private function buildRows(array $snapshot, array $keys, array $labels = []): array
    {
        $rows = [];

        foreach ($keys as $key) {
            if (! array_key_exists($key, $snapshot)) {
                continue;
            }

            $rows[] = [
                'path' => $key,
                'label' => $labels[$key] ?? Str::headline($key),
                'value' => $this->formatValue($key, $snapshot[$key]),
            ];
        }

        return $rows;
    }

    /**
     * @param  array<int, string>  $usedKeys
     * @return array<int, array<string, mixed>>
     */
    private function buildAdditionalRows(array $snapshot, array $usedKeys): array
    {
        $ignoredKeys = array_merge($this->ignoredKeys(), $usedKeys);
        $rows = [];

        foreach ($snapshot as $key => $value) {
            if (! is_string($key)) {
                continue;
            }

            if (in_array($key, $ignoredKeys, true)) {
                continue;
            }

            $rows[] = [
                'path' => $key,
                'label' => Str::headline($key),
                'value' => $this->formatValue($key, $value),
            ];
        }

        return $rows;
    }

    /**
     * @return array<int, string>
     */
    private function ignoredKeys(): array
    {
        return [
            '@odata.context',
            '@odata.type',
            'id',
            'version',
            'createdDateTime',
            'lastModifiedDateTime',
            'supportsScopeTags',
            'roleScopeTagIds',
            'assignments',
            'createdBy',
            'lastModifiedBy',
            'omaSettings',
            'settings',
            'settingsDelta',
        ];
    }

    private function formatValue(string $key, mixed $value): mixed
    {
        if (is_bool($value)) {
            $normalized = strtolower($key);

            if (str_ends_with($normalized, 'blocked')) {
                return $value ? 'Blocked' : 'Allowed';
            }

            if (str_ends_with($normalized, 'required')) {
                return $value ? 'Required' : 'Not required';
            }

            if (str_contains($normalized, 'enabled')) {
                return $value ? 'Enabled' : 'Disabled';
            }

            return $value ? 'Yes' : 'No';
        }

        if (is_array($value)) {
            if ($value === []) {
                return 'None';
            }

            if (array_is_list($value) && $this->isScalarList($value)) {
                return implode(', ', array_map(fn (mixed $item) => $this->formatScalarListItem($item), $value));
            }

            return json_encode($value, JSON_PRETTY_PRINT);
        }

        if (is_string($value)) {
            $duration = $this->formatDuration($value);

            if ($duration !== null) {
                return $duration;
            }

            if ($value === '') {
                return null;
            }

            if (preg_match('/^[a-zA-Z][a-zA-Z0-9]*$/', $value) === 1) {
                return Str::headline($value);
            }

            return $value;
        }

        return $value;
    }

    /**
     * @param  array<int, mixed>  $value
     */
    private function isScalarList(array $value): bool
    {
        foreach ($value as $item) {
            if (! is_string($item) && ! is_int($item) && ! is_float($item)) {
                return false;
            }
        }

        return true;
    }

    private function formatScalarListItem(mixed $value): string
    {
        if (is_int($value) || is_float($value)) {
            return (string) $value;
        }

        if (! is_string($value)) {
            return '';
        }

        if (preg_match('/^[a-zA-Z][a-zA-Z0-9]*$/', $value) === 1) {
            return Str::headline($value);
        }

        return $value;
    }

    private function formatDuration(string $value): ?string
    {
        if (! preg_match('/^P[T0-9]/i', $value)) {
            return null;
        }

        try {
            $interval = new DateInterval(strtoupper($value));
        } catch (\Throwable) {
            return null;
        }

        $parts = [];

        if ($interval->y) {
            $parts[] = $interval->y.' '.Str::plural('year', $interval->y);
        }
        if ($interval->m) {
            $parts[] = $interval->m.' '.Str::plural('month', $interval->m);
        }
        if ($interval->d) {
            $parts[] = $interval->d.' '.Str::plural('day', $interval->d);
        }

        if ($interval->h) {
            $parts[] = $interval->h.' '.Str::plural('hour', $interval->h);
        }
        if ($interval->i) {
            $parts[] = $interval->i.' '.Str::plural('minute', $interval->i);
        }
        if ($interval->s && $parts === []) {
            $parts[] = $interval->s.' '.Str::plural('second', $interval->s);
        }

        if ($parts === []) {
            return null;
        }

        return implode(' ', $parts);
    }

    private function platformLabelFromOdataType(mixed $odataType): ?string
    {
        if (! is_string($odataType) || $odataType === '') {
            return null;
        }

        return match (strtolower($odataType)) {
            '#microsoft.graph.androidmanagedappprotection' => 'Android',
            '#microsoft.graph.iosmanagedappprotection' => 'iOS',
            '#microsoft.graph.windowsinformationprotectionpolicy',
            '#microsoft.graph.mdmwindowsinformationprotectionpolicy' => 'Windows',
            default => null,
        };
    }
}
