<?php

namespace App\Services\Intune;

use App\Models\ManagedEnvironment;
use App\Support\Providers\Capabilities\ProviderCapabilityDefinition;
use App\Support\Providers\Capabilities\ProviderCapabilityRegistry;
use App\Support\Providers\Capabilities\ProviderCapabilityStatus;
use App\Support\Verification\ManagedEnvironmentPermissionCheckClusters;
use App\Support\Verification\VerificationReportOverall;
use Carbon\CarbonInterface;
use Illuminate\Support\Carbon;

class ManagedEnvironmentRequiredPermissionsViewModelBuilder
{
    /**
     * @phpstan-type ManagedEnvironmentPermissionRow array{key:string,type:'application'|'delegated',description:?string,features:array<int,string>,status:'granted'|'missing'|'error',details:array<string,mixed>|null}
     * @phpstan-type FeatureImpact array{feature:string,missing:int,required_application:int,required_delegated:int,blocked:bool}
     * @phpstan-type CapabilityGroup array{provider_capability_key:string,label:string,status:string,provider_requirement_keys:array<int,string>,missing_requirement_keys:array<int,string>,evidence_counts:array{requirements:int,missing:int,errors:int},message:string}
     * @phpstan-type FilterState array{status:'missing'|'present'|'all',type:'application'|'delegated'|'all',features:array<int,string>,search:string}
     * @phpstan-type ViewModel array{
     *   tenant: array{id:int,external_id:string,name:string},
     *   overview: array{
     *     overall: string,
     *     counts: array{missing_application:int,missing_delegated:int,present:int,error:int},
     *     feature_impacts: array<int, FeatureImpact>,
     *     capability_groups: array<int, CapabilityGroup>,
     *     primary_capability_group: CapabilityGroup|null,
     *     freshness: array{last_refreshed_at:?string,is_stale:bool}
     *   },
     *   permissions: array<int, ManagedEnvironmentPermissionRow>,
     *   filters: FilterState,
     *   copy: array{application:string,delegated:string}
     * }
     */
    public function __construct(private readonly ManagedEnvironmentPermissionService $permissionService) {}

    /**
     * @param  array<string, mixed>  $filters
     * @return ViewModel
     */
    public function build(ManagedEnvironment $tenant, array $filters = []): array
    {
        $comparison = $this->permissionService->compare(
            $tenant,
            persist: false,
            liveCheck: false,
            useConfiguredStub: false,
        );

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

        $state = self::normalizeFilterState($filters);

        $filteredPermissions = self::applyFilterState($allPermissions, $state);
        $freshness = self::deriveFreshness(self::parseLastRefreshedAt($comparison['last_refreshed_at'] ?? null));

        $summaryPermissions = $filteredPermissions;
        $capabilityGroups = self::deriveCapabilityGroups($allPermissions, $freshness);

        return [
            'tenant' => [
                'id' => (int) $tenant->getKey(),
                'external_id' => (string) $tenant->external_id,
                'name' => (string) $tenant->name,
            ],
            'overview' => [
                'overall' => self::deriveOverallStatus($summaryPermissions, (bool) ($freshness['is_stale'] ?? true)),
                'counts' => self::deriveCounts($summaryPermissions),
                'feature_impacts' => self::deriveFeatureImpacts($summaryPermissions),
                'capability_groups' => $capabilityGroups,
                'primary_capability_group' => self::primaryCapabilityGroup($capabilityGroups),
                'freshness' => $freshness,
            ],
            'permissions' => $filteredPermissions,
            'filters' => $state,
            'copy' => [
                'application' => self::deriveCopyPayload($allPermissions, 'application', $state['features']),
                'delegated' => self::deriveCopyPayload($allPermissions, 'delegated', $state['features']),
            ],
        ];
    }

    /**
     * @param  array<int, ManagedEnvironmentPermissionRow>  $permissions
     * @param  array{last_refreshed_at:?string,is_stale:bool}  $freshness
     * @return array<int, CapabilityGroup>
     */
    public static function deriveCapabilityGroups(array $permissions, array $freshness): array
    {
        /** @var ProviderCapabilityRegistry $registry */
        $registry = app(ProviderCapabilityRegistry::class);

        return array_map(
            static fn (ProviderCapabilityDefinition $definition): array => self::deriveCapabilityGroup(
                definition: $definition,
                permissions: $permissions,
                isStale: (bool) ($freshness['is_stale'] ?? true),
            ),
            array_values($registry->all()),
        );
    }

    /**
     * @param  array<int, CapabilityGroup>  $groups
     * @return CapabilityGroup|null
     */
    public static function primaryCapabilityGroup(array $groups): ?array
    {
        if ($groups === []) {
            return null;
        }

        usort($groups, static function (array $a, array $b): int {
            $aStatus = ProviderCapabilityStatus::tryFrom((string) ($a['status'] ?? 'unknown')) ?? ProviderCapabilityStatus::Unknown;
            $bStatus = ProviderCapabilityStatus::tryFrom((string) ($b['status'] ?? 'unknown')) ?? ProviderCapabilityStatus::Unknown;

            return $aStatus->priority() <=> $bStatus->priority();
        });

        return $groups[0];
    }

    /**
     * @param  array<int, ManagedEnvironmentPermissionRow>  $permissions
     * @return CapabilityGroup
     */
    private static function deriveCapabilityGroup(
        ProviderCapabilityDefinition $definition,
        array $permissions,
        bool $isStale,
    ): array {
        $rowsByRequirement = [];

        foreach ($definition->providerRequirementKeys as $requirementKey) {
            $rowsByRequirement[$requirementKey] = ManagedEnvironmentPermissionCheckClusters::rowsForRequirementKey($permissions, $requirementKey);
        }

        $rows = array_values(array_merge(...array_values($rowsByRequirement ?: [[]])));
        $missingRows = array_values(array_filter(
            $rows,
            static fn (array $row): bool => ($row['status'] ?? null) === 'missing',
        ));
        $errorRows = array_values(array_filter(
            $rows,
            static fn (array $row): bool => ($row['status'] ?? null) === 'error',
        ));
        $missingRequirementKeys = [];

        foreach ($rowsByRequirement as $requirementKey => $requirementRows) {
            foreach ($requirementRows as $row) {
                if (in_array(($row['status'] ?? null), ['missing', 'error'], true)) {
                    $missingRequirementKeys[] = (string) $requirementKey;
                    break;
                }
            }
        }

        $status = match (true) {
            $rows === [] => ProviderCapabilityStatus::NotApplicable,
            $errorRows !== [] => ProviderCapabilityStatus::Unknown,
            $missingRows !== [] => ProviderCapabilityStatus::Missing,
            $isStale => ProviderCapabilityStatus::Unknown,
            default => ProviderCapabilityStatus::Supported,
        };

        $message = match ($status) {
            ProviderCapabilityStatus::Supported => "{$definition->label} capability is supported by stored permission evidence.",
            ProviderCapabilityStatus::Missing => "{$definition->label} capability is missing required provider permissions.",
            ProviderCapabilityStatus::Unknown => "{$definition->label} capability needs refreshed permission evidence.",
            ProviderCapabilityStatus::Blocked => "{$definition->label} capability is blocked.",
            ProviderCapabilityStatus::NotApplicable => "{$definition->label} capability has no mapped permission rows for this tenant.",
        };

        return [
            'provider_capability_key' => $definition->key,
            'label' => $definition->label,
            'status' => $status->value,
            'provider_requirement_keys' => $definition->providerRequirementKeys,
            'missing_requirement_keys' => array_values(array_unique($missingRequirementKeys)),
            'evidence_counts' => [
                'requirements' => count($rows),
                'missing' => count($missingRows),
                'errors' => count($errorRows),
            ],
            'message' => $message,
        ];
    }

    /**
     * @param  array<int, ManagedEnvironmentPermissionRow>  $permissions
     */
    public static function deriveOverallStatus(array $permissions, bool $hasStaleFreshness = false): string
    {
        $hasMissingApplication = collect($permissions)->contains(
            fn (array $row): bool => $row['status'] === 'missing' && $row['type'] === 'application',
        );

        if ($hasMissingApplication) {
            return VerificationReportOverall::Blocked->value;
        }

        $hasErrors = collect($permissions)->contains(
            fn (array $row): bool => $row['status'] === 'error',
        );

        $hasMissingDelegated = collect($permissions)->contains(
            fn (array $row): bool => $row['status'] === 'missing' && $row['type'] === 'delegated',
        );

        if ($hasErrors || $hasMissingDelegated || $hasStaleFreshness) {
            return VerificationReportOverall::NeedsAttention->value;
        }

        return VerificationReportOverall::Ready->value;
    }

    /**
     * @return array{last_refreshed_at:?string,is_stale:bool}
     */
    public static function deriveFreshness(?CarbonInterface $lastRefreshedAt, ?CarbonInterface $referenceTime = null): array
    {
        $reference = $referenceTime instanceof Carbon
            ? $referenceTime->copy()
            : ($referenceTime !== null ? Carbon::instance($referenceTime) : now());

        $lastRefreshed = $lastRefreshedAt instanceof Carbon
            ? $lastRefreshedAt
            : ($lastRefreshedAt !== null ? Carbon::instance($lastRefreshedAt) : null);

        $isStale = $lastRefreshed === null
            || $lastRefreshed->lt($reference->copy()->subDays(30));

        return [
            'last_refreshed_at' => $lastRefreshed?->toIso8601String(),
            'is_stale' => $isStale,
        ];
    }

    /**
     * @param  array<int, ManagedEnvironmentPermissionRow>  $permissions
     * @return array{missing_application:int,missing_delegated:int,present:int,error:int}
     */
    public static function deriveCounts(array $permissions): array
    {
        $counts = [
            'missing_application' => 0,
            'missing_delegated' => 0,
            'present' => 0,
            'error' => 0,
        ];

        foreach ($permissions as $row) {
            if (($row['status'] ?? null) === 'missing') {
                if (($row['type'] ?? null) === 'delegated') {
                    $counts['missing_delegated'] += 1;
                } else {
                    $counts['missing_application'] += 1;
                }

                continue;
            }

            if (($row['status'] ?? null) === 'granted') {
                $counts['present'] += 1;

                continue;
            }

            if (($row['status'] ?? null) === 'error') {
                $counts['error'] += 1;
            }
        }

        return $counts;
    }

    /**
     * @param  array<int, ManagedEnvironmentPermissionRow>  $permissions
     * @return array<int, FeatureImpact>
     */
    public static function deriveFeatureImpacts(array $permissions): array
    {
        /** @var array<string, FeatureImpact> $impacts */
        $impacts = [];

        foreach ($permissions as $row) {
            $features = array_values(array_unique($row['features'] ?? []));

            foreach ($features as $feature) {
                if (! isset($impacts[$feature])) {
                    $impacts[$feature] = [
                        'feature' => $feature,
                        'missing' => 0,
                        'required_application' => 0,
                        'required_delegated' => 0,
                        'blocked' => false,
                    ];
                }

                if (($row['type'] ?? null) === 'delegated') {
                    $impacts[$feature]['required_delegated'] += 1;
                } else {
                    $impacts[$feature]['required_application'] += 1;
                }

                if (($row['status'] ?? null) === 'missing') {
                    $impacts[$feature]['missing'] += 1;

                    if (($row['type'] ?? null) === 'application') {
                        $impacts[$feature]['blocked'] = true;
                    }
                }
            }
        }

        $values = array_values($impacts);

        usort($values, static function (array $a, array $b): int {
            $blocked = (int) ($b['blocked'] <=> $a['blocked']);
            if ($blocked !== 0) {
                return $blocked;
            }

            $missing = (int) (($b['missing'] ?? 0) <=> ($a['missing'] ?? 0));
            if ($missing !== 0) {
                return $missing;
            }

            return strcmp((string) ($a['feature'] ?? ''), (string) ($b['feature'] ?? ''));
        });

        return $values;
    }

    /**
     * Copy payload semantics:
     * - Always Missing-only
     * - Always Type fixed by button (application vs delegated)
     * - Respects Feature filter only
     * - Ignores Search
     *
     * @param  array<int, ManagedEnvironmentPermissionRow>  $permissions
     * @param  'application'|'delegated'  $type
     * @param  array<int, string>  $featureFilter
     */
    public static function deriveCopyPayload(array $permissions, string $type, array $featureFilter = []): string
    {
        $featureFilter = array_values(array_unique(array_filter(array_map('strval', $featureFilter))));

        $payload = collect($permissions)
            ->filter(function (array $row) use ($type, $featureFilter): bool {
                if (($row['status'] ?? null) !== 'missing') {
                    return false;
                }

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

                if ($featureFilter === []) {
                    return true;
                }

                $rowFeatures = $row['features'] ?? [];

                return count(array_intersect($featureFilter, $rowFeatures)) > 0;
            })
            ->pluck('key')
            ->map(fn (mixed $key): string => (string) $key)
            ->filter()
            ->unique()
            ->sort()
            ->values()
            ->all();

        return implode("\n", $payload);
    }

    /**
     * @param  array<int, ManagedEnvironmentPermissionRow>  $permissions
     * @return array<int, ManagedEnvironmentPermissionRow>
     */
    public static function applyFilterState(array $permissions, array $state): array
    {
        $status = $state['status'] ?? 'missing';
        $type = $state['type'] ?? 'all';
        $features = $state['features'] ?? [];
        $search = $state['search'] ?? '';

        $search = is_string($search) ? trim($search) : '';
        $searchLower = strtolower($search);

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

        $filtered = collect($permissions)
            ->filter(function (array $row) use ($status, $type, $features): bool {
                $rowStatus = $row['status'] ?? null;
                $rowType = $row['type'] ?? null;

                if ($status === 'missing' && ! in_array($rowStatus, ['missing', 'error'], true)) {
                    return false;
                }

                if ($status === 'present' && $rowStatus !== 'granted') {
                    return false;
                }

                if ($type !== 'all' && $rowType !== $type) {
                    return false;
                }

                if ($features === []) {
                    return true;
                }

                $rowFeatures = $row['features'] ?? [];

                return count(array_intersect($features, $rowFeatures)) > 0;
            })
            ->when($searchLower !== '', function ($collection) use ($searchLower) {
                return $collection->filter(function (array $row) use ($searchLower): bool {
                    $key = strtolower((string) ($row['key'] ?? ''));
                    $description = strtolower((string) ($row['description'] ?? ''));

                    return str_contains($key, $searchLower) || ($description !== '' && str_contains($description, $searchLower));
                });
            })
            ->values()
            ->all();

        usort($filtered, static function (array $a, array $b): int {
            $weight = static function (array $row): int {
                return match ($row['status'] ?? null) {
                    'missing' => 0,
                    'error' => 1,
                    default => 2,
                };
            };

            $cmp = $weight($a) <=> $weight($b);
            if ($cmp !== 0) {
                return $cmp;
            }

            return strcmp((string) ($a['key'] ?? ''), (string) ($b['key'] ?? ''));
        });

        return $filtered;
    }

    /**
     * @param  array<string, mixed>  $filters
     * @return FilterState
     */
    public static function normalizeFilterState(array $filters): array
    {
        $status = (string) ($filters['status'] ?? 'missing');
        $type = (string) ($filters['type'] ?? 'all');
        $features = $filters['features'] ?? [];
        $search = (string) ($filters['search'] ?? '');

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

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

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

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

        return [
            'status' => $status,
            'type' => $type,
            'features' => $features,
            'search' => $search,
        ];
    }

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

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

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

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

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

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

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

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

    private static function parseLastRefreshedAt(mixed $value): ?Carbon
    {
        if ($value instanceof Carbon) {
            return $value;
        }

        if ($value instanceof CarbonInterface) {
            return Carbon::instance($value);
        }

        if (is_string($value) && $value !== '') {
            try {
                return Carbon::parse($value);
            } catch (\Throwable) {
                return null;
            }
        }

        return null;
    }
}
