<?php

declare(strict_types=1);

namespace App\Support\Ui\ActionSurface;

use App\Support\Ui\ActionSurface\Enums\ActionSurfaceInspectAffordance;
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceSlot;
use Filament\Pages\Page;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use SplFileInfo;

final class ActionSurfaceValidator
{
    private ActionSurfaceDiscovery $discovery;

    private ActionSurfaceProfileDefinition $profileDefinition;

    private ActionSurfaceExemptions $exemptions;

    public function __construct(
        ?ActionSurfaceDiscovery $discovery = null,
        ?ActionSurfaceProfileDefinition $profileDefinition = null,
        ?ActionSurfaceExemptions $exemptions = null,
    ) {
        $this->discovery = $discovery ?? new ActionSurfaceDiscovery;
        $this->profileDefinition = $profileDefinition ?? new ActionSurfaceProfileDefinition;
        $this->exemptions = $exemptions ?? ActionSurfaceExemptions::baseline();
    }

    /**
     * @return array<int, ActionSurfaceDiscoveredComponent>
     */
    public function discoveredComponents(): array
    {
        return $this->discovery->discover();
    }

    public static function withBaselineExemptions(): self
    {
        return new self(
            discovery: new ActionSurfaceDiscovery,
            profileDefinition: new ActionSurfaceProfileDefinition,
            exemptions: ActionSurfaceExemptions::baseline(),
        );
    }

    public function validate(): ActionSurfaceValidationResult
    {
        return $this->validateComponents($this->discoveredComponents());
    }

    /**
     * @param  array<int, ActionSurfaceDiscoveredComponent>  $components
     */
    public function validateComponents(array $components): ActionSurfaceValidationResult
    {
        $issues = [];
        $discoveredClassNames = array_values(array_unique(array_merge(
            array_map(
                static fn (ActionSurfaceDiscoveredComponent $component): string => $component->className,
                $this->discovery->discover(),
            ),
            array_map(
                static fn (ActionSurfaceDiscoveredComponent $component): string => $component->className,
                $components,
            ),
        )));

        $this->validateSpec193MonitoringSurfaceInventory($issues);
        $this->validateSpec192RecordPageInventory($issues);
        $this->validateSpec195ResidualSurfaceInventory($issues, $discoveredClassNames);

        foreach ($components as $component) {
            if (! class_exists($component->className)) {
                $issues[] = new ActionSurfaceValidationIssue(
                    className: $component->className,
                    message: 'Discovered class does not exist or is not autoloadable.',
                    hint: 'Verify namespace/path and run composer dump-autoload if needed.',
                );

                continue;
            }

            $declaration = $this->resolveDeclarationForComponent($component, $issues);

            if ($declaration === null) {
                continue;
            }

            if ($declaration->componentType !== $component->componentType) {
                $issues[] = new ActionSurfaceValidationIssue(
                    className: $component->className,
                    message: sprintf(
                        'Declaration component type mismatch (%s declared, %s discovered).',
                        $declaration->componentType->name,
                        $component->componentType->name,
                    ),
                    hint: 'Use ActionSurfaceDeclaration::forResource/forPage/forRelationManager consistently.',
                );
            }

            if ($declaration->defaults->moreGroupLabel !== 'More') {
                $issues[] = new ActionSurfaceValidationIssue(
                    className: $component->className,
                    message: sprintf(
                        'Invalid more-group label "%s".',
                        $declaration->defaults->moreGroupLabel,
                    ),
                    hint: 'Set ActionSurfaceDefaults->moreGroupLabel to "More".',
                );
            }

            $this->validateRequiredSlots($component->className, $declaration, $issues);
            $this->validateBehaviorAwareContract($component->className, $declaration, $issues);
            $this->validateExemptions($component->className, $declaration, $issues);
            $this->validateExportDefaults($component->className, $declaration, $issues);
        }

        return new ActionSurfaceValidationResult(
            issues: $issues,
            componentCount: count($components),
        );
    }

    /**
     * @param  array<int, ActionSurfaceValidationIssue>  $issues
     */
    private function validateSpec193MonitoringSurfaceInventory(array &$issues): void
    {
        $allowedClassifications = [
            'remediation_required',
            'minor_alignment_only',
            'compliant_no_op',
            'special_type_acceptable',
        ];
        $allowedPanelScopes = ['admin', 'tenant'];
        $allowedOwnerScopes = ['workspace-owned', 'workspace-visible-tenant-owned', 'tenant-owned'];
        $allowedSurfaceKinds = ['queue_workbench', 'monitoring_detail', 'monitoring_landing', 'read_only_report', 'diagnostic_exception'];
        $allowedPrimaryInspectModels = ['explicit_inspect_action', 'clickable_row', 'page_level_overview', 'matrix_itself', 'singleton_detail_surface'];
        $allowedSharedPatterns = ['operate_hub_shell', 'cluster_entry', 'none'];
        $surfaceKeys = [];

        foreach (ActionSurfaceExemptions::spec193MonitoringSurfaceInventory() as $className => $surface) {
            if (! class_exists($className)) {
                $issues[] = new ActionSurfaceValidationIssue(
                    className: $className,
                    message: 'Spec 193 inventory references a surface class that does not exist.',
                    hint: 'Keep ActionSurfaceExemptions::spec193MonitoringSurfaceInventory() aligned with the in-scope monitoring surface classes.',
                );

                continue;
            }

            $surfaceKey = (string) ($surface['surfaceKey'] ?? '');

            if ($surfaceKey === '') {
                $issues[] = new ActionSurfaceValidationIssue(
                    className: $className,
                    message: 'Spec 193 inventory entry is missing a non-empty surface key.',
                    hint: 'Provide the stable spec surface key for this monitoring surface.',
                );
            } elseif (isset($surfaceKeys[$surfaceKey])) {
                $issues[] = new ActionSurfaceValidationIssue(
                    className: $className,
                    message: sprintf('Spec 193 surface key "%s" is declared more than once.', $surfaceKey),
                    hint: 'Each in-scope monitoring surface must have a unique surface key.',
                );
            } else {
                $surfaceKeys[$surfaceKey] = true;
            }

            if (! in_array($surface['classification'] ?? null, $allowedClassifications, true)) {
                $issues[] = new ActionSurfaceValidationIssue(
                    className: $className,
                    message: 'Spec 193 classification is invalid or missing.',
                    hint: 'Use remediation_required, minor_alignment_only, compliant_no_op, or special_type_acceptable.',
                );
            }

            if (! in_array($surface['panelScope'] ?? null, $allowedPanelScopes, true)) {
                $issues[] = new ActionSurfaceValidationIssue(
                    className: $className,
                    message: 'Spec 193 panel scope is invalid or missing.',
                    hint: 'Use admin or tenant for each monitoring surface inventory entry.',
                );
            }

            if (! in_array($surface['ownerScope'] ?? null, $allowedOwnerScopes, true)) {
                $issues[] = new ActionSurfaceValidationIssue(
                    className: $className,
                    message: 'Spec 193 owner scope is invalid or missing.',
                    hint: 'Use workspace-owned, workspace-visible-tenant-owned, or tenant-owned.',
                );
            }

            if (! in_array($surface['surfaceKind'] ?? null, $allowedSurfaceKinds, true)) {
                $issues[] = new ActionSurfaceValidationIssue(
                    className: $className,
                    message: 'Spec 193 surface kind is invalid or missing.',
                    hint: 'Use queue_workbench, monitoring_detail, monitoring_landing, read_only_report, or diagnostic_exception.',
                );
            }

            if (! in_array($surface['primaryInspectModel'] ?? null, $allowedPrimaryInspectModels, true)) {
                $issues[] = new ActionSurfaceValidationIssue(
                    className: $className,
                    message: 'Spec 193 primary inspect model is invalid or missing.',
                    hint: 'Use an allowed inspect model such as explicit_inspect_action, clickable_row, or page_level_overview.',
                );
            }

            if (! in_array($surface['sharedPattern'] ?? null, $allowedSharedPatterns, true)) {
                $issues[] = new ActionSurfaceValidationIssue(
                    className: $className,
                    message: 'Spec 193 shared pattern is invalid or missing.',
                    hint: 'Use operate_hub_shell, cluster_entry, or none.',
                );
            }

            if (! is_string($surface['canonicalNoun'] ?? null) || trim((string) $surface['canonicalNoun']) === '') {
                $issues[] = new ActionSurfaceValidationIssue(
                    className: $className,
                    message: 'Spec 193 canonical noun must be non-empty.',
                    hint: 'Use the stable operator-facing noun for the monitoring surface.',
                );
            }

            $classification = (string) ($surface['classification'] ?? '');
            $exceptionReason = $surface['exceptionReason'] ?? null;

            if ($classification === 'special_type_acceptable') {
                if (! is_string($exceptionReason) || trim($exceptionReason) === '') {
                    $issues[] = new ActionSurfaceValidationIssue(
                        className: $className,
                        message: 'Special-type acceptable Spec 193 surfaces require an explicit exception reason.',
                        hint: 'Document why this surface intentionally differs from the standard monitoring hierarchy.',
                    );
                }
            } elseif ($exceptionReason !== null && trim((string) $exceptionReason) !== '') {
                $issues[] = new ActionSurfaceValidationIssue(
                    className: $className,
                    message: 'Only special-type acceptable Spec 193 surfaces may carry an exception reason.',
                    hint: 'Clear the exception reason for remediation, minor-alignment, and compliant surfaces.',
                );
            }

            if ($classification === 'remediation_required' && ($surface['requiresHeaderRemediation'] ?? false) !== true) {
                $issues[] = new ActionSurfaceValidationIssue(
                    className: $className,
                    message: 'Remediation-required Spec 193 surfaces must mark header remediation as required.',
                    hint: 'Set requiresHeaderRemediation to true for remediation_required surfaces.',
                );
            }

            if (($surface['requiresExplicitDeclaration'] ?? false) === true && ! method_exists($className, 'actionSurfaceDeclaration')) {
                $issues[] = new ActionSurfaceValidationIssue(
                    className: $className,
                    message: 'Spec 193 surface requires an explicit action-surface declaration, but the class does not define one.',
                    hint: 'Add actionSurfaceDeclaration() to the page class or clear requiresExplicitDeclaration if the surface is intentionally declaration-free.',
                );
            }
        }
    }

    /**
     * @param  array<int, ActionSurfaceValidationIssue>  $issues
     */
    private function validateSpec192RecordPageInventory(array &$issues): void
    {
        $allowedClassifications = [
            'remediation_required',
            'minor_alignment_only',
            'compliant_reference',
            'workflow_heavy_special_type',
        ];
        $allowedPanelScopes = ['admin', 'tenant'];
        $allowedOwnerScopes = ['workspace-owned', 'tenant-owned'];
        $allowedRouteKinds = ['view', 'edit'];
        $surfaceKeys = [];

        foreach (ActionSurfaceExemptions::spec192RecordPageInventory() as $className => $surface) {
            if (! class_exists($className)) {
                $issues[] = new ActionSurfaceValidationIssue(
                    className: $className,
                    message: 'Spec 192 inventory references a page class that does not exist.',
                    hint: 'Keep ActionSurfaceExemptions::spec192RecordPageInventory() aligned with the in-scope page classes.',
                );

                continue;
            }

            $surfaceKey = (string) ($surface['surfaceKey'] ?? '');

            if ($surfaceKey === '') {
                $issues[] = new ActionSurfaceValidationIssue(
                    className: $className,
                    message: 'Spec 192 inventory entry is missing a non-empty surface key.',
                    hint: 'Provide the stable spec surface key for this page.',
                );
            } elseif (isset($surfaceKeys[$surfaceKey])) {
                $issues[] = new ActionSurfaceValidationIssue(
                    className: $className,
                    message: sprintf('Spec 192 surface key "%s" is declared more than once.', $surfaceKey),
                    hint: 'Each in-scope page must have a unique surface key.',
                );
            } else {
                $surfaceKeys[$surfaceKey] = true;
            }

            if (! in_array($surface['classification'] ?? null, $allowedClassifications, true)) {
                $issues[] = new ActionSurfaceValidationIssue(
                    className: $className,
                    message: 'Spec 192 classification is invalid or missing.',
                    hint: 'Use remediation_required, minor_alignment_only, compliant_reference, or workflow_heavy_special_type.',
                );
            }

            if (! in_array($surface['panelScope'] ?? null, $allowedPanelScopes, true)) {
                $issues[] = new ActionSurfaceValidationIssue(
                    className: $className,
                    message: 'Spec 192 panel scope is invalid or missing.',
                    hint: 'Use the concrete panel scope for the record page inventory entry.',
                );
            }

            if (! in_array($surface['ownerScope'] ?? null, $allowedOwnerScopes, true)) {
                $issues[] = new ActionSurfaceValidationIssue(
                    className: $className,
                    message: 'Spec 192 owner scope is invalid or missing.',
                    hint: 'Use workspace-owned or tenant-owned.',
                );
            }

            if (! in_array($surface['routeKind'] ?? null, $allowedRouteKinds, true)) {
                $issues[] = new ActionSurfaceValidationIssue(
                    className: $className,
                    message: 'Spec 192 route kind is invalid or missing.',
                    hint: 'Use view or edit.',
                );
            }

            if (! is_string($surface['canonicalNoun'] ?? null) || trim((string) $surface['canonicalNoun']) === '') {
                $issues[] = new ActionSurfaceValidationIssue(
                    className: $className,
                    message: 'Spec 192 canonical noun must be non-empty.',
                    hint: 'Use the stable operator-facing noun for the surface.',
                );
            }

            $classification = (string) ($surface['classification'] ?? '');
            $exceptionReason = $surface['exceptionReason'] ?? null;

            if ($classification === 'workflow_heavy_special_type') {
                if (! is_string($exceptionReason) || trim($exceptionReason) === '') {
                    $issues[] = new ActionSurfaceValidationIssue(
                        className: $className,
                        message: 'Workflow-heavy Spec 192 pages require an explicit exception reason.',
                        hint: 'Document why this surface is intentionally exempt from the standard record-page rule.',
                    );
                }
            } elseif ($exceptionReason !== null && trim((string) $exceptionReason) !== '') {
                $issues[] = new ActionSurfaceValidationIssue(
                    className: $className,
                    message: 'Only workflow-heavy Spec 192 pages may carry an exception reason.',
                    hint: 'Clear the exception reason for standard, minor-alignment, and compliant-reference surfaces.',
                );
            }

            if (($surface['maxVisiblePrimaryActions'] ?? null) !== 1) {
                $issues[] = new ActionSurfaceValidationIssue(
                    className: $className,
                    message: 'Spec 192 maxVisiblePrimaryActions must stay pinned to 1.',
                    hint: 'The bounded header contract allows at most one visible primary header action.',
                );
            }

            if ($classification === 'remediation_required' && ($surface['allowsPrimaryNavigation'] ?? false) === true) {
                $issues[] = new ActionSurfaceValidationIssue(
                    className: $className,
                    message: 'Remediation-required Spec 192 surfaces must not allow primary navigation.',
                    hint: 'Move pure navigation into contextual placement outside the header.',
                );
            }
        }
    }

    /**
     * @param  array<int, ActionSurfaceValidationIssue>  $issues
     * @param  array<int, string>  $discoveredClassNames
     */
    private function validateSpec195ResidualSurfaceInventory(array &$issues, array $discoveredClassNames): void
    {
        $issues = array_merge(
            $issues,
            self::validateSpec195ResidualInventoryFixture(
                inventory: ActionSurfaceExemptions::spec195ResidualSurfaceInventory(),
                discoveredClasses: $discoveredClassNames,
                baselineExemptions: ActionSurfaceExemptions::baseline()->all(),
                residualCandidateClasses: $this->spec195ResidualCandidateClasses($discoveredClassNames),
            ),
        );
    }

    /**
     * @param  array<string, array<string, mixed>>  $inventory
     * @param  array<int, string>  $discoveredClasses
     * @param  array<string, string>  $baselineExemptions
     * @param  array<int, string>  $residualCandidateClasses
     * @return array<int, ActionSurfaceValidationIssue>
     */
    public static function validateSpec195ResidualInventoryFixture(
        array $inventory,
        array $discoveredClasses,
        array $baselineExemptions,
        array $residualCandidateClasses = [],
    ): array {
        $issues = [];
        $allowedDiscoveryStates = [
            'primary_discovered',
            'primary_discovered_baseline_exempt',
            'outside_primary_discovery',
        ];
        $allowedClosureDecisions = [
            'generic_contract_enrollment',
            'intentional_exemption',
            'separately_governed',
            'retired_no_longer_relevant',
            'harmless_special_case',
        ];
        $allowedReasonCategories = [
            'system_triage_surface',
            'workflow_specific_governance',
            'break_glass_repair_utility',
            'read_mostly_context_detail',
            'disabled_or_actionless_surface',
            'selector_routing_only',
            'registration_form_with_dedicated_rbac',
            'landing_routing_surface',
            'dashboard_shell_widget_owned',
            'security_flow_exception',
        ];
        $allowedPanelPlanes = ['admin', 'tenant', 'system'];
        $allowedSurfaceKinds = [
            'system_detail',
            'system_utility',
            'selector',
            'wizard',
            'landing',
            'dashboard_shell',
            'recovery_flow',
            'read_mostly_context',
        ];
        $allowedFollowUpActions = [
            'none',
            'tighten_reason',
            'add_guard_only',
            'add_focused_test',
            'consider_enrollment',
        ];
        $allowedEvidenceKinds = [
            'guard_test',
            'feature_livewire_test',
            'authorization_test',
            'workflow_spec',
            'audit_test',
            'db_only_surface_test',
        ];
        $discoveredLookup = array_fill_keys($discoveredClasses, true);
        $surfaceKeys = [];

        foreach ($inventory as $className => $surface) {
            if (! class_exists($className)) {
                $issues[] = new ActionSurfaceValidationIssue(
                    className: $className,
                    message: 'Spec 195 residual inventory references a surface class that does not exist.',
                    hint: 'Keep ActionSurfaceExemptions::spec195ResidualSurfaceInventory() aligned with the in-scope residual surface classes.',
                );

                continue;
            }

            $surfaceKey = (string) ($surface['surfaceKey'] ?? '');

            if ($surfaceKey === '') {
                $issues[] = new ActionSurfaceValidationIssue(
                    className: $className,
                    message: 'Spec 195 residual inventory entry is missing a non-empty surface key.',
                    hint: 'Provide the stable spec surface key for this residual surface.',
                );
            } elseif (isset($surfaceKeys[$surfaceKey])) {
                $issues[] = new ActionSurfaceValidationIssue(
                    className: $className,
                    message: sprintf('Spec 195 residual surface key "%s" is declared more than once.', $surfaceKey),
                    hint: 'Each residual surface must have a unique stable key.',
                );
            } else {
                $surfaceKeys[$surfaceKey] = true;
            }

            if (($surface['pageClass'] ?? null) !== $className) {
                $issues[] = new ActionSurfaceValidationIssue(
                    className: $className,
                    message: 'Spec 195 residual inventory pageClass must exactly match the keyed class.',
                    hint: 'Keep the array key and pageClass field aligned for reviewer clarity.',
                );
            }

            if (! is_string($surface['surfaceName'] ?? null) || trim((string) $surface['surfaceName']) === '') {
                $issues[] = new ActionSurfaceValidationIssue(
                    className: $className,
                    message: 'Spec 195 residual inventory surfaceName must be non-empty.',
                    hint: 'Use a human-readable review label such as "System Ops View Run".',
                );
            }

            if (! in_array($surface['panelPlane'] ?? null, $allowedPanelPlanes, true)) {
                $issues[] = new ActionSurfaceValidationIssue(
                    className: $className,
                    message: 'Spec 195 panel plane is invalid or missing.',
                    hint: 'Use admin, tenant, or system.',
                );
            }

            if (! in_array($surface['surfaceKind'] ?? null, $allowedSurfaceKinds, true)) {
                $issues[] = new ActionSurfaceValidationIssue(
                    className: $className,
                    message: 'Spec 195 surface kind is invalid or missing.',
                    hint: 'Use one of the documented residual surface kinds from the logical contract.',
                );
            }

            if (! in_array($surface['closureDecision'] ?? null, $allowedClosureDecisions, true)) {
                $issues[] = new ActionSurfaceValidationIssue(
                    className: $className,
                    message: 'Spec 195 closure decision is invalid or missing.',
                    hint: 'Use generic_contract_enrollment, intentional_exemption, separately_governed, retired_no_longer_relevant, or harmless_special_case.',
                );
            }

            $expectedDiscoveryState = isset($discoveredLookup[$className])
                ? (array_key_exists($className, $baselineExemptions) ? 'primary_discovered_baseline_exempt' : 'primary_discovered')
                : 'outside_primary_discovery';

            if (($surface['discoveryState'] ?? null) !== $expectedDiscoveryState) {
                $issues[] = new ActionSurfaceValidationIssue(
                    className: $className,
                    message: sprintf(
                        'Spec 195 discovery state is not truthful. Expected "%s".',
                        $expectedDiscoveryState,
                    ),
                    hint: 'Keep discoveryState aligned with the primary validator discovery result and baseline exemption registry.',
                );
            }

            if (! in_array($surface['discoveryState'] ?? null, $allowedDiscoveryStates, true)) {
                $issues[] = new ActionSurfaceValidationIssue(
                    className: $className,
                    message: 'Spec 195 discovery state is invalid or missing.',
                    hint: 'Use primary_discovered, primary_discovered_baseline_exempt, or outside_primary_discovery.',
                );
            }

            if (! is_string($surface['explicitReason'] ?? null) || trim((string) $surface['explicitReason']) === '') {
                $issues[] = new ActionSurfaceValidationIssue(
                    className: $className,
                    message: 'Spec 195 explicit reason must be non-empty.',
                    hint: 'Document the concrete operator or review reason for the chosen closure decision.',
                );
            }

            $closureDecision = (string) ($surface['closureDecision'] ?? '');
            $reasonCategory = $surface['reasonCategory'] ?? null;

            if ($closureDecision === 'generic_contract_enrollment') {
                if (is_string($reasonCategory) && trim($reasonCategory) !== '') {
                    $issues[] = new ActionSurfaceValidationIssue(
                        className: $className,
                        message: 'Generic-contract enrollment entries must not carry a reason category.',
                        hint: 'Clear reasonCategory when the residual surface is fully enrolled into the generic contract.',
                    );
                }

                if (! method_exists($className, 'actionSurfaceDeclaration')) {
                    $issues[] = new ActionSurfaceValidationIssue(
                        className: $className,
                        message: 'Generic-contract enrollment requires actionSurfaceDeclaration().',
                        hint: 'Enroll the surface into the declaration-backed contract before classifying it as generic_contract_enrollment.',
                    );
                }
            } elseif (! in_array($reasonCategory, $allowedReasonCategories, true)) {
                $issues[] = new ActionSurfaceValidationIssue(
                    className: $className,
                    message: 'Spec 195 reason category is invalid or missing for a non-enrolled residual surface.',
                    hint: 'Use one of the allowed Spec 195 reason categories for intentional exemptions, separate governance, retired surfaces, and harmless special cases.',
                );
            }

            if (! in_array($surface['followUpAction'] ?? null, $allowedFollowUpActions, true)) {
                $issues[] = new ActionSurfaceValidationIssue(
                    className: $className,
                    message: 'Spec 195 follow-up action is invalid or missing.',
                    hint: 'Use none, tighten_reason, add_guard_only, add_focused_test, or consider_enrollment.',
                );
            }

            if (! is_bool($surface['mustRemainBaselineExempt'] ?? null)) {
                $issues[] = new ActionSurfaceValidationIssue(
                    className: $className,
                    message: 'Spec 195 mustRemainBaselineExempt must be boolean.',
                    hint: 'Use true only when the discovered page must remain in baseline().',
                );
            }

            if (! is_bool($surface['mustNotRemainBaselineExempt'] ?? null)) {
                $issues[] = new ActionSurfaceValidationIssue(
                    className: $className,
                    message: 'Spec 195 mustNotRemainBaselineExempt must be boolean.',
                    hint: 'Use true when the residual surface must stay out of baseline().',
                );
            }

            if (($surface['mustRemainBaselineExempt'] ?? false) === true && ($surface['mustNotRemainBaselineExempt'] ?? false) === true) {
                $issues[] = new ActionSurfaceValidationIssue(
                    className: $className,
                    message: 'Spec 195 baseline flags cannot both be true.',
                    hint: 'A residual surface can either stay in baseline() or be required to stay out of it, but not both.',
                );
            }

            if (($surface['mustRemainBaselineExempt'] ?? false) === true && ! array_key_exists($className, $baselineExemptions)) {
                $issues[] = new ActionSurfaceValidationIssue(
                    className: $className,
                    message: 'Spec 195 says this residual surface must remain baseline-exempt, but baseline() does not include it.',
                    hint: 'Keep discovered special surfaces aligned between baseline() and spec195ResidualSurfaceInventory().',
                );
            }

            if (($surface['mustNotRemainBaselineExempt'] ?? false) === true && array_key_exists($className, $baselineExemptions)) {
                $issues[] = new ActionSurfaceValidationIssue(
                    className: $className,
                    message: 'Spec 195 says this residual surface must not remain baseline-exempt, but baseline() still includes it.',
                    hint: 'Remove the stale baseline exemption or change the residual closure classification.',
                );
            }

            $evidence = $surface['evidence'] ?? null;

            if (! is_array($evidence) || $evidence === []) {
                $issues[] = new ActionSurfaceValidationIssue(
                    className: $className,
                    message: 'Spec 195 residual surfaces require at least one structured evidence descriptor.',
                    hint: 'Add one or more evidence entries with kind, reference, and proves.',
                );
            } else {
                foreach ($evidence as $index => $descriptor) {
                    if (! is_array($descriptor)) {
                        $issues[] = new ActionSurfaceValidationIssue(
                            className: $className,
                            message: sprintf('Spec 195 evidence entry #%d must be an array.', $index + 1),
                            hint: 'Use structured evidence descriptors with kind, reference, and proves.',
                        );

                        continue;
                    }

                    if (! in_array($descriptor['kind'] ?? null, $allowedEvidenceKinds, true)) {
                        $issues[] = new ActionSurfaceValidationIssue(
                            className: $className,
                            message: sprintf('Spec 195 evidence entry #%d kind is invalid or missing.', $index + 1),
                            hint: 'Use guard_test, feature_livewire_test, authorization_test, workflow_spec, audit_test, or db_only_surface_test.',
                        );
                    }

                    if (! is_string($descriptor['reference'] ?? null) || trim((string) $descriptor['reference']) === '') {
                        $issues[] = new ActionSurfaceValidationIssue(
                            className: $className,
                            message: sprintf('Spec 195 evidence entry #%d reference must be non-empty.', $index + 1),
                            hint: 'Point reviewers at the concrete test file or source artifact that proves the classification.',
                        );
                    }

                    if (! is_string($descriptor['proves'] ?? null) || trim((string) $descriptor['proves']) === '') {
                        $issues[] = new ActionSurfaceValidationIssue(
                            className: $className,
                            message: sprintf('Spec 195 evidence entry #%d proves text must be non-empty.', $index + 1),
                            hint: 'Explain what the referenced evidence actually proves about the residual surface.',
                        );
                    }
                }
            }
        }

        $candidateClasses = array_values(array_unique(array_merge(
            $residualCandidateClasses,
            self::spec195BaselineResidualCandidateClasses($baselineExemptions),
        )));
        sort($candidateClasses);

        foreach ($candidateClasses as $className) {
            if (array_key_exists($className, $inventory)) {
                continue;
            }

            $classPath = self::spec195ClassPath($className);

            $issues[] = new ActionSurfaceValidationIssue(
                className: $className,
                message: 'Residual action surface is missing a Spec 195 closure entry.',
                hint: $classPath !== null
                    ? sprintf(
                        'Add %s to ActionSurfaceExemptions::spec195ResidualSurfaceInventory() and classify it via the reviewer workflow. File: %s',
                        $className,
                        $classPath,
                    )
                    : 'Add the missing residual surface to ActionSurfaceExemptions::spec195ResidualSurfaceInventory() and classify it via the reviewer workflow.',
            );
        }

        return $issues;
    }

    /**
     * @param  array<int, ActionSurfaceValidationIssue>  $issues
     */
    private function resolveDeclarationForComponent(
        ActionSurfaceDiscoveredComponent $component,
        array &$issues,
    ): ?ActionSurfaceDeclaration {
        $className = $component->className;

        if (! method_exists($className, 'actionSurfaceDeclaration')) {
            $this->validateClassExemptionOrFail($className, $issues);

            return null;
        }

        try {
            $declaration = $className::actionSurfaceDeclaration();
        } catch (\Throwable $throwable) {
            $issues[] = new ActionSurfaceValidationIssue(
                className: $className,
                message: 'actionSurfaceDeclaration() threw an exception: '.$throwable->getMessage(),
                hint: 'Ensure actionSurfaceDeclaration() is static and does not depend on request state.',
            );

            return null;
        }

        if (! $declaration instanceof ActionSurfaceDeclaration) {
            $issues[] = new ActionSurfaceValidationIssue(
                className: $className,
                message: 'actionSurfaceDeclaration() must return ActionSurfaceDeclaration.',
                hint: 'Return ActionSurfaceDeclaration::forResource/forPage/forRelationManager(...).',
            );

            return null;
        }

        return $declaration;
    }

    /**
     * @param  array<int, ActionSurfaceValidationIssue>  $issues
     */
    private function validateClassExemptionOrFail(string $className, array &$issues): void
    {
        $reason = $this->exemptions->reasonForClass($className);

        if ($reason === null) {
            $residualSurface = ActionSurfaceExemptions::spec195ResidualSurface($className);

            if ($residualSurface !== null) {
                $closureDecision = (string) ($residualSurface['closureDecision'] ?? '');

                if ($closureDecision === 'generic_contract_enrollment') {
                    $issues[] = new ActionSurfaceValidationIssue(
                        className: $className,
                        message: 'Residual surface is marked for generic-contract enrollment but still lacks actionSurfaceDeclaration().',
                        hint: 'Add actionSurfaceDeclaration() or change the Spec 195 closure decision if the surface is intentionally staying separate.',
                    );
                }

                return;
            }

            $issues[] = new ActionSurfaceValidationIssue(
                className: $className,
                message: 'Missing action-surface declaration, baseline exemption, and Spec 195 residual closure entry.',
                hint: 'Add actionSurfaceDeclaration(), register a baseline exemption with a non-empty reason, or classify the surface in spec195ResidualSurfaceInventory().',
            );

            return;
        }

        if (trim($reason) === '') {
            $issues[] = new ActionSurfaceValidationIssue(
                className: $className,
                message: 'Component exemption reason must be non-empty.',
                hint: 'Provide a concrete, non-empty justification in ActionSurfaceExemptions::baseline().',
            );
        }
    }

    /**
     * @param  array<int, ActionSurfaceValidationIssue>  $issues
     */
    private function validateRequiredSlots(
        string $className,
        ActionSurfaceDeclaration $declaration,
        array &$issues,
    ): void {
        foreach ($this->profileDefinition->requiredSlots($declaration->profile) as $slot) {
            $requirement = $declaration->slot($slot);

            if ($requirement === null) {
                $issues[] = new ActionSurfaceValidationIssue(
                    className: $className,
                    slot: $slot,
                    message: 'Required slot is not declared.',
                    hint: 'Declare slot as satisfied or exempt with a reason.',
                );

                continue;
            }

            if (! $requirement->isExempt()) {
                if ($slot === ActionSurfaceSlot::InspectAffordance) {
                    $this->validateInspectAffordanceSlot($className, $requirement, $issues);
                }

                continue;
            }

            $exemption = $declaration->exemption($slot);

            if ($exemption === null || ! $exemption->hasReason()) {
                $issues[] = new ActionSurfaceValidationIssue(
                    className: $className,
                    slot: $slot,
                    message: 'Slot is marked exempt but exemption reason is missing or empty.',
                    hint: 'Use ->exempt(slot, "reason") with a non-empty reason.',
                );
            }
        }
    }

    /**
     * @param  array<int, ActionSurfaceValidationIssue>  $issues
     */
    private function validateInspectAffordanceSlot(
        string $className,
        ActionSurfaceSlotRequirement $requirement,
        array &$issues,
    ): void {
        $this->resolveInspectAffordance($className, $requirement, $issues);
    }

    /**
     * @param  array<int, ActionSurfaceValidationIssue>  $issues
     */
    private function validateBehaviorAwareContract(
        string $className,
        ActionSurfaceDeclaration $declaration,
        array &$issues,
    ): void {
        if (! $declaration->requiresBehaviorAwareContract()) {
            return;
        }

        if ($declaration->surfaceType === null) {
            $issues[] = new ActionSurfaceValidationIssue(
                className: $className,
                message: 'Behavior-aware declarations must define a surface type.',
                hint: 'Pass an ActionSurfaceType when creating the declaration.',
            );

            return;
        }

        if (! $this->profileDefinition->allowsSurfaceType($declaration->profile, $declaration->surfaceType)) {
            $issues[] = new ActionSurfaceValidationIssue(
                className: $className,
                message: sprintf(
                    'Surface type "%s" is incompatible with profile "%s".',
                    $declaration->surfaceType->value,
                    $declaration->profile->value,
                ),
                hint: 'Choose a surface type allowed for the profile or change the profile to match the rendered list behavior.',
            );

            return;
        }

        $requirement = $declaration->slot(ActionSurfaceSlot::InspectAffordance);

        if (! $requirement instanceof ActionSurfaceSlotRequirement || $requirement->isExempt()) {
            return;
        }

        $affordance = $this->resolveInspectAffordance($className, $requirement, $issues);

        if (! $affordance instanceof ActionSurfaceInspectAffordance) {
            return;
        }

        if (! $declaration->surfaceType->allowsInspectAffordance($affordance)) {
            $allowed = implode(', ', array_map(
                static fn (ActionSurfaceInspectAffordance $allowedAffordance): string => $allowedAffordance->value,
                $declaration->surfaceType->allowedInspectAffordances(),
            ));

            $issues[] = new ActionSurfaceValidationIssue(
                className: $className,
                slot: ActionSurfaceSlot::InspectAffordance,
                message: sprintf(
                    'Inspect affordance "%s" is incompatible with surface type "%s".',
                    $affordance->value,
                    $declaration->surfaceType->value,
                ),
                hint: sprintf('Allowed: %s.', $allowed),
            );
        }

        if ($affordance->isPrimaryLinkColumn() && trim((string) $declaration->primaryLinkColumnReason()) === '') {
            $issues[] = new ActionSurfaceValidationIssue(
                className: $className,
                slot: ActionSurfaceSlot::InspectAffordance,
                message: 'Primary link column inspect affordance requires a non-empty reason.',
                hint: 'Call ->withPrimaryLinkColumnReason("why row click is not the right primary inspect model").',
            );
        }
    }

    /**
     * @param  array<int, ActionSurfaceValidationIssue>  $issues
     */
    private function resolveInspectAffordance(
        string $className,
        ActionSurfaceSlotRequirement $requirement,
        array &$issues,
    ): ?ActionSurfaceInspectAffordance {
        $mode = $requirement->details;

        if (! is_string($mode) || trim($mode) === '') {
            $issues[] = new ActionSurfaceValidationIssue(
                className: $className,
                slot: ActionSurfaceSlot::InspectAffordance,
                message: 'Inspect affordance must declare how inspection is provided (clickable_row, view_action, or primary_link_column).',
                hint: 'Use ->satisfy(ActionSurfaceSlot::InspectAffordance, ActionSurfaceInspectAffordance::ClickableRow->value).',
            );

            return null;
        }

        $affordance = ActionSurfaceInspectAffordance::tryFrom($mode);

        if ($affordance instanceof ActionSurfaceInspectAffordance) {
            return $affordance;
        }

        $issues[] = new ActionSurfaceValidationIssue(
            className: $className,
            slot: ActionSurfaceSlot::InspectAffordance,
            message: sprintf('Invalid inspect affordance mode "%s".', $mode),
            hint: 'Allowed: clickable_row, view_action, primary_link_column.',
        );

        return null;
    }

    /**
     * @param  array<int, ActionSurfaceValidationIssue>  $issues
     */
    private function validateExemptions(
        string $className,
        ActionSurfaceDeclaration $declaration,
        array &$issues,
    ): void {
        foreach ($declaration->exemptions() as $slotValue => $exemption) {
            if (! $exemption->hasReason()) {
                $issues[] = new ActionSurfaceValidationIssue(
                    className: $className,
                    slot: ActionSurfaceSlot::from($slotValue),
                    message: 'Exemption reason must be non-empty.',
                    hint: 'Provide a concise reason for each exempted slot.',
                );
            }
        }
    }

    /**
     * @param  array<int, ActionSurfaceValidationIssue>  $issues
     */
    private function validateExportDefaults(
        string $className,
        ActionSurfaceDeclaration $declaration,
        array &$issues,
    ): void {
        if (! $this->profileDefinition->requiresExportDefaultBulk($declaration->profile)) {
            return;
        }

        if ($declaration->defaults->exportIsDefaultBulkActionForReadOnly) {
            return;
        }

        $bulkExemption = $declaration->exemption(ActionSurfaceSlot::ListBulkMoreGroup);

        if ($bulkExemption instanceof ActionSurfaceExemption && $bulkExemption->hasReason()) {
            return;
        }

        $issues[] = new ActionSurfaceValidationIssue(
            className: $className,
            slot: ActionSurfaceSlot::ListBulkMoreGroup,
            message: 'ReadOnly/RunLog profile disables Export default but no bulk-slot exemption reason was provided.',
            hint: 'Keep exportIsDefaultBulkActionForReadOnly=true or exempt ListBulkMoreGroup with a reason.',
        );
    }

    /**
     * @param  array<int, string>  $discoveredClassNames
     * @return array<int, string>
     */
    private function spec195ResidualCandidateClasses(array $discoveredClassNames): array
    {
        $candidates = $this->spec195SystemResidualCandidateClasses($discoveredClassNames);

        foreach (self::spec195BaselineResidualCandidateClasses(ActionSurfaceExemptions::baseline()->all()) as $className) {
            $candidates[] = $className;
        }

        $candidates = array_values(array_unique($candidates));
        sort($candidates);

        return $candidates;
    }

    /**
     * @param  array<int, string>  $discoveredClassNames
     * @return array<int, string>
     */
    private function spec195SystemResidualCandidateClasses(array $discoveredClassNames): array
    {
        $discoveredLookup = array_fill_keys($discoveredClassNames, true);
        $classes = [];

        foreach ($this->collectPhpClasses($this->appFilamentSystemPagesPath()) as $className) {
            if (isset($discoveredLookup[$className])) {
                continue;
            }

            if ($className === 'App\\Filament\\System\\Pages\\Auth\\Login') {
                continue;
            }

            if (! class_exists($className) || ! is_subclass_of($className, Page::class)) {
                continue;
            }

            $classes[] = $className;
        }

        sort($classes);

        return $classes;
    }

    private function appFilamentSystemPagesPath(): string
    {
        return base_path('app/Filament/System/Pages');
    }

    /**
     * @param  array<string, string>  $baselineExemptions
     * @return array<int, string>
     */
    private static function spec195BaselineResidualCandidateClasses(array $baselineExemptions): array
    {
        $classes = [];

        foreach (array_keys($baselineExemptions) as $className) {
            if (! self::qualifiesAsSpec195BaselineResidualCandidate($className)) {
                continue;
            }

            $classes[] = $className;
        }

        sort($classes);

        return $classes;
    }

    private static function qualifiesAsSpec195BaselineResidualCandidate(string $className): bool
    {
        if (in_array($className, [
            'App\\Filament\\Pages\\BreakGlassRecovery',
            'App\\Filament\\Pages\\ChooseEnvironment',
            'App\\Filament\\Pages\\ChooseWorkspace',
            'App\\Filament\\Pages\\EnvironmentDashboard',
        ], true)) {
            return true;
        }

        return str_starts_with($className, 'App\\Filament\\Pages\\Tenancy\\')
            || str_starts_with($className, 'App\\Filament\\Pages\\Workspaces\\');
    }

    /**
     * @return array<int, string>
     */
    private function collectPhpClasses(string $directory): array
    {
        if (! is_dir($directory)) {
            return [];
        }

        $iterator = new RecursiveIteratorIterator(
            new RecursiveDirectoryIterator($directory, RecursiveDirectoryIterator::SKIP_DOTS),
        );

        $classes = [];

        /** @var SplFileInfo $file */
        foreach ($iterator as $file) {
            if (! $file->isFile() || ! str_ends_with($file->getFilename(), '.php')) {
                continue;
            }

            $classes[] = $this->classNameFromPath($file->getPathname());
        }

        sort($classes);

        return $classes;
    }

    private function classNameFromPath(string $path): string
    {
        $normalizedPath = str_replace('\\', '/', $path);
        $normalizedAppPath = str_replace('\\', '/', app_path());
        $relativePath = ltrim(substr($normalizedPath, strlen($normalizedAppPath)), '/');

        return 'App\\'.str_replace('/', '\\', substr($relativePath, 0, -4));
    }

    private static function spec195ClassPath(string $className): ?string
    {
        if (! str_starts_with($className, 'App\\')) {
            return null;
        }

        $path = base_path('app/'.str_replace('\\', '/', substr($className, 4)).'.php');

        return is_file($path) ? $path : null;
    }
}
