<?php

declare(strict_types=1);

namespace App\Support\Ui\ActionSurface;

use App\Support\Ui\ActionSurface\Enums\ActionSurfaceInspectAffordance;
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceSlot;

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 = [];

        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->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 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) {
            $issues[] = new ActionSurfaceValidationIssue(
                className: $className,
                message: 'Missing action-surface declaration and no component exemption exists.',
                hint: 'Add actionSurfaceDeclaration() or register a baseline exemption with a non-empty reason.',
            );

            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 {
        $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;
        }

        if (ActionSurfaceInspectAffordance::tryFrom($mode) instanceof ActionSurfaceInspectAffordance) {
            return;
        }

        $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.',
        );
    }

    /**
     * @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.',
        );
    }
}
