<?php

declare(strict_types=1);

namespace App\Support\Verification;

use App\Models\ManagedEnvironment;
use App\Support\Links\RequiredPermissionsLinks;
use App\Support\OpsUx\RunFailureSanitizer;
use App\Support\Providers\ProviderNextStepsRegistry;
use App\Support\Providers\ProviderReasonCodes;

final class ManagedEnvironmentPermissionCheckClusters
{
    /**
     * @phpstan-type ManagedEnvironmentPermissionRow array{
     *   key:string,
     *   type:'application'|'delegated',
     *   description:?string,
     *   features:array<int,string>,
     *   status:'granted'|'missing'|'error',
     *   details:array<string,mixed>|null
     * }
     *
     * @param  array<int, array<string, mixed>>  $permissions
     * @param  array{fresh?:bool,reason_code?:string,message?:string}|null  $inventory
     * @return array<int, array<string, mixed>>
     */
    public static function buildChecks(ManagedEnvironment $tenant, array $permissions, ?array $inventory = null): array
    {
        $inventory = is_array($inventory) ? $inventory : [];

        $inventoryFresh = $inventory['fresh'] ?? true;
        $inventoryFresh = is_bool($inventoryFresh) ? $inventoryFresh : true;

        $inventoryReasonCode = $inventory['reason_code'] ?? null;
        $inventoryReasonCode = is_string($inventoryReasonCode) && $inventoryReasonCode !== ''
            ? $inventoryReasonCode
            : 'dependency_unreachable';
        $inventoryReasonCode = RunFailureSanitizer::normalizeReasonCode($inventoryReasonCode);

        $inventoryMessage = $inventory['message'] ?? null;
        $inventoryMessage = is_string($inventoryMessage) && trim($inventoryMessage) !== ''
            ? trim($inventoryMessage)
            : self::inventoryMessage($inventoryReasonCode);

        $inventoryEvidence = self::inventoryEvidence($inventory);

        /** @var array<int, ManagedEnvironmentPermissionRow> $rows */
        $rows = collect($permissions)
            ->filter(fn (mixed $row): bool => is_array($row))
            ->map(fn (array $row): array => self::normalizePermissionRow($row))
            ->values()
            ->all();

        $checks = [];

        foreach (self::definitions() as $definition) {
            $key = (string) ($definition['key'] ?? 'unknown');
            $title = (string) ($definition['title'] ?? 'Check');

            $clusterRows = array_values(array_filter($rows, fn (array $row): bool => self::matchesDefinition($definition, $row)));

            $checks[] = self::buildCheck(
                tenant: $tenant,
                key: $key,
                title: $title,
                clusterRows: $clusterRows,
                inventoryFresh: $inventoryFresh,
                inventoryReasonCode: $inventoryReasonCode,
                inventoryMessage: $inventoryMessage,
                inventoryEvidence: $inventoryEvidence,
            );
        }

        return $checks;
    }

    /**
     * @return array<int, array{key:string,title:string,mode:string,prefixes?:array<int,string>,keys?:array<int,string>,type?:string}>
     */
    public static function definitions(): array
    {
        return [
            [
                'key' => 'permissions.admin_consent',
                'title' => 'Required application permissions',
                'mode' => 'type',
                'type' => 'application',
            ],
            [
                'key' => 'permissions.directory_groups',
                'title' => 'Directory & group read access',
                'mode' => 'keys',
                'keys' => [
                    'Directory.Read.All',
                    'Group.Read.All',
                ],
            ],
            [
                'key' => 'permissions.intune_configuration',
                'title' => 'Intune configuration access',
                'mode' => 'prefixes',
                'prefixes' => [
                    'DeviceManagementConfiguration.',
                    'DeviceManagementServiceConfig.',
                ],
            ],
            [
                'key' => 'permissions.intune_apps',
                'title' => 'Intune apps access',
                'mode' => 'prefixes',
                'prefixes' => [
                    'DeviceManagementApps.',
                ],
            ],
            [
                'key' => 'permissions.intune_rbac_assignments',
                'title' => 'Intune RBAC & assignments prerequisites',
                'mode' => 'prefixes',
                'prefixes' => [
                    'DeviceManagementRBAC.',
                ],
            ],
            [
                'key' => 'provider.directory_role_definitions',
                'title' => 'Directory role definitions read access',
                'mode' => 'keys',
                'keys' => [
                    'RoleManagement.Read.Directory',
                ],
            ],
            [
                'key' => 'permissions.scripts_remediations',
                'title' => 'Scripts/remediations access',
                'mode' => 'prefixes',
                'prefixes' => [
                    'DeviceManagementScripts.',
                ],
            ],
        ];
    }

    /**
     * @param  array{mode:string,prefixes?:array<int,string>,keys?:array<int,string>,type?:string}  $definition
     * @param  ManagedEnvironmentPermissionRow  $row
     */
    public static function matchesDefinition(array $definition, array $row): bool
    {
        $mode = (string) ($definition['mode'] ?? '');
        $key = (string) ($row['key'] ?? '');

        if ($mode === 'type') {
            return ($row['type'] ?? null) === ($definition['type'] ?? null);
        }

        if ($mode === 'keys') {
            $keys = $definition['keys'] ?? [];

            return is_array($keys) && in_array($key, $keys, true);
        }

        if ($mode === 'prefixes') {
            $prefixes = $definition['prefixes'] ?? [];

            if (! is_array($prefixes)) {
                return false;
            }

            foreach ($prefixes as $prefix) {
                if (is_string($prefix) && $prefix !== '' && str_starts_with($key, $prefix)) {
                    return true;
                }
            }

            return false;
        }

        return false;
    }

    /**
     * @param  array<int, array<string, mixed>>  $permissions
     * @return array<int, ManagedEnvironmentPermissionRow>
     */
    public static function rowsForRequirementKey(array $permissions, string $requirementKey): array
    {
        $definition = self::definitionForKey($requirementKey);

        if (! is_array($definition)) {
            return [];
        }

        return collect($permissions)
            ->filter(fn (mixed $row): bool => is_array($row))
            ->map(fn (array $row): array => self::normalizePermissionRow($row))
            ->filter(fn (array $row): bool => self::matchesDefinition($definition, $row))
            ->values()
            ->all();
    }

    /**
     * @param  ManagedEnvironmentPermissionRow  $row
     * @return array<int, string>
     */
    public static function requirementKeysForPermissionRow(array $row): array
    {
        return collect(self::definitions())
            ->filter(fn (array $definition): bool => self::matchesDefinition($definition, self::normalizePermissionRow($row)))
            ->map(fn (array $definition): string => (string) ($definition['key'] ?? ''))
            ->filter()
            ->values()
            ->all();
    }

    /**
     * @return array{key:string,title:string,mode:string,prefixes?:array<int,string>,keys?:array<int,string>,type?:string}|null
     */
    public static function definitionForKey(string $requirementKey): ?array
    {
        foreach (self::definitions() as $definition) {
            if (($definition['key'] ?? null) === $requirementKey) {
                return $definition;
            }
        }

        return null;
    }

    /**
     * @param  array<int, ManagedEnvironmentPermissionRow>  $clusterRows
     * @return array<string, mixed>
     */
    private static function buildCheck(
        ManagedEnvironment $tenant,
        string $key,
        string $title,
        array $clusterRows,
        bool $inventoryFresh,
        string $inventoryReasonCode,
        string $inventoryMessage,
        array $inventoryEvidence,
    ): array {
        if (! $inventoryFresh) {
            return [
                'key' => $key,
                'title' => $title,
                'status' => VerificationCheckStatus::Warn->value,
                'severity' => VerificationCheckSeverity::Medium->value,
                'blocking' => false,
                'reason_code' => $inventoryReasonCode,
                'message' => $inventoryMessage,
                'evidence' => $inventoryEvidence,
                'next_steps' => self::nextSteps($tenant, $inventoryReasonCode),
            ];
        }

        if ($clusterRows === []) {
            return [
                'key' => $key,
                'title' => $title,
                'status' => VerificationCheckStatus::Skip->value,
                'severity' => VerificationCheckSeverity::Info->value,
                'blocking' => false,
                'reason_code' => 'not_applicable',
                'message' => 'Not applicable for this tenant.',
                'evidence' => [],
                'next_steps' => [],
            ];
        }

        $missingApplication = array_values(array_filter(
            $clusterRows,
            static fn (array $row): bool => $row['status'] === 'missing' && $row['type'] === 'application',
        ));

        $missingDelegated = array_values(array_filter(
            $clusterRows,
            static fn (array $row): bool => $row['status'] === 'missing' && $row['type'] === 'delegated',
        ));

        $errored = array_values(array_filter(
            $clusterRows,
            static fn (array $row): bool => $row['status'] === 'error',
        ));

        $evidence = array_values(array_unique(array_merge(
            self::evidence($missingApplication, $missingDelegated, $errored),
            $inventoryEvidence,
        ), SORT_REGULAR));

        if ($missingApplication !== [] || $errored !== []) {
            $missingKeys = array_values(array_unique(array_merge(
                array_map(static fn (array $row): string => $row['key'], $missingApplication),
                array_map(static fn (array $row): string => $row['key'], $errored),
            )));

            $reasonCode = self::permissionMissingReasonCode($key);
            $message = self::missingApplicationMessage($key, $missingKeys);

            return [
                'key' => $key,
                'title' => $title,
                'status' => VerificationCheckStatus::Fail->value,
                'severity' => VerificationCheckSeverity::Critical->value,
                'blocking' => true,
                'reason_code' => $reasonCode,
                'message' => $message,
                'evidence' => $evidence,
                'next_steps' => self::nextSteps($tenant, $reasonCode),
            ];
        }

        if ($missingDelegated !== []) {
            $missingKeys = array_values(array_unique(array_map(static fn (array $row): string => $row['key'], $missingDelegated)));
            $message = sprintf('Missing delegated permission(s): %s', implode(', ', array_slice($missingKeys, 0, 6)));

            return [
                'key' => $key,
                'title' => $title,
                'status' => VerificationCheckStatus::Warn->value,
                'severity' => VerificationCheckSeverity::Medium->value,
                'blocking' => false,
                'reason_code' => ProviderReasonCodes::ProviderPermissionRefreshFailed,
                'message' => $message,
                'evidence' => $evidence,
                'next_steps' => self::nextSteps($tenant, ProviderReasonCodes::ProviderPermissionRefreshFailed),
            ];
        }

        return [
            'key' => $key,
            'title' => $title,
            'status' => VerificationCheckStatus::Pass->value,
            'severity' => VerificationCheckSeverity::Info->value,
            'blocking' => false,
            'reason_code' => 'ok',
            'message' => 'All required permissions are granted.',
            'evidence' => [],
            'next_steps' => [],
        ];
    }

    /**
     * @param  array<int, string>  $missingKeys
     */
    private static function missingApplicationMessage(string $key, array $missingKeys): string
    {
        if ($key === 'permissions.intune_rbac_assignments') {
            $listed = implode(', ', array_slice($missingKeys, 0, 6));

            return $listed !== ''
                ? sprintf('Missing required Intune RBAC read permission(s): %s. RBAC inventory and backup history will remain read-only.', $listed)
                : 'Missing required Intune RBAC read permissions. RBAC inventory and backup history will remain read-only.';
        }

        return $missingKeys !== []
            ? sprintf('Missing required application permission(s): %s', implode(', ', array_slice($missingKeys, 0, 6)))
            : 'Missing required permissions.';
    }

    private static function permissionMissingReasonCode(string $key): string
    {
        if ($key === 'permissions.intune_rbac_assignments') {
            return ProviderReasonCodes::IntuneRbacPermissionMissing;
        }

        return ProviderReasonCodes::ProviderPermissionMissing;
    }

    /**
     * @param  array<int, ManagedEnvironmentPermissionRow>  $missingApplication
     * @param  array<int, ManagedEnvironmentPermissionRow>  $missingDelegated
     * @param  array<int, ManagedEnvironmentPermissionRow>  $errored
     * @return array<int, array{kind:string,value:int|string}>
     */
    private static function evidence(array $missingApplication, array $missingDelegated, array $errored): array
    {
        $pointers = [];

        foreach (array_merge($missingApplication, $missingDelegated, $errored) as $row) {
            $pointers[] = [
                'kind' => 'missing_permission',
                'value' => (string) ($row['key'] ?? ''),
            ];

            $pointers[] = [
                'kind' => 'permission_type',
                'value' => (string) ($row['type'] ?? 'application'),
            ];

            foreach (($row['features'] ?? []) as $feature) {
                if (! is_string($feature) || $feature === '') {
                    continue;
                }

                $pointers[] = [
                    'kind' => 'feature',
                    'value' => $feature,
                ];
            }
        }

        $unique = [];

        foreach ($pointers as $pointer) {
            $key = $pointer['kind'].':'.(string) $pointer['value'];
            $unique[$key] = $pointer;
        }

        return array_values($unique);
    }

    /**
     * @param  array<string, mixed>  $inventory
     * @return array<int, array{kind:string,value:int|string}>
     */
    private static function inventoryEvidence(array $inventory): array
    {
        $pointers = [];

        $appId = $inventory['app_id'] ?? null;
        if (is_string($appId) && $appId !== '') {
            $pointers[] = [
                'kind' => 'app_id',
                'value' => $appId,
            ];
        }

        $observedCount = $inventory['observed_permissions_count'] ?? null;
        if (is_int($observedCount) || (is_numeric($observedCount) && (string) (int) $observedCount === (string) $observedCount)) {
            $pointers[] = [
                'kind' => 'observed_permissions_count',
                'value' => (int) $observedCount,
            ];
        }

        return $pointers;
    }

    private static function inventoryMessage(string $reasonCode): string
    {
        return match ($reasonCode) {
            ProviderReasonCodes::PlatformIdentityMissing => 'Platform app identity is not configured. Verification used no runtime fallback.',
            ProviderReasonCodes::PlatformIdentityIncomplete => 'Platform app identity is incomplete. Fix the central platform configuration, then retry verification.',
            ProviderReasonCodes::DedicatedCredentialMissing => 'Dedicated connection credentials are missing. Add credentials, then retry verification.',
            ProviderReasonCodes::DedicatedCredentialInvalid => 'Dedicated connection credentials are invalid. Update the dedicated credential, then retry verification.',
            ProviderReasonCodes::ProviderConsentFailed => 'Admin consent failed. Retry admin consent before verification can continue.',
            ProviderReasonCodes::ProviderConsentRevoked => 'Admin consent was revoked. Grant admin consent again before verification can continue.',
            default => 'Unable to refresh observed permissions inventory during this run. Retry verification.',
        };
    }

    /**
     * @return array<int, array{label: string, url: string}>
     */
    private static function nextSteps(ManagedEnvironment $tenant, string $reasonCode): array
    {
        $steps = app(ProviderNextStepsRegistry::class)->forReason($tenant, $reasonCode);

        if ($steps !== []) {
            return $steps;
        }

        return [
            [
                'label' => 'Open required permissions',
                'url' => RequiredPermissionsLinks::requiredPermissions($tenant),
            ],
        ];
    }

    /**
     * @param  array<string, mixed>  $row
     * @return ManagedEnvironmentPermissionRow
     */
    private static function normalizePermissionRow(array $row): array
    {
        $key = (string) ($row['key'] ?? '');
        $type = (string) ($row['type'] ?? 'application');
        $status = (string) ($row['status'] ?? 'missing');
        $description = $row['description'] ?? null;
        $features = $row['features'] ?? [];
        $details = $row['details'] ?? null;

        if (! in_array($type, ['application', 'delegated'], true)) {
            $type = 'application';
        }

        if (! in_array($status, ['granted', 'missing', 'error'], true)) {
            $status = 'missing';
        }

        if (! is_string($description) || $description === '') {
            $description = null;
        }

        if (! is_array($features)) {
            $features = [];
        }

        $features = array_values(array_unique(array_filter(array_map('strval', $features))));

        if (! is_array($details)) {
            $details = null;
        }

        return [
            'key' => $key,
            'type' => $type,
            'description' => $description,
            'features' => $features,
            'status' => $status,
            'details' => $details,
        ];
    }
}
