<?php

declare(strict_types=1);

namespace App\Framework\LiveComponents\Validation;

use App\Framework\LiveComponents\Exceptions\StateValidationException;

/**
 * Default state validator with automatic schema derivation
 *
 * Validates component state against automatically derived schemas.
 * Supports type checking, required fields, and structure validation.
 *
 * Type Matching Rules:
 * - Exact type match: int === int, string === string
 * - Object type match: User === User (class name comparison)
 * - Null handling: null matches 'null' type from schema
 * - Array matching: array === array (no deep structure validation yet)
 *
 * State Value Objects:
 * - Works directly with State VOs (CounterState, SearchState, etc.)
 * - State VOs must implement toArray() for validation
 */
final readonly class DefaultStateValidator implements StateValidator
{
    /**
     * Derive schema from State Value Object
     */
    public function deriveSchemaFromState(object $state): DerivedSchema
    {
        // State VOs must implement toArray() for serialization
        if (!method_exists($state, 'toArray')) {
            throw new \InvalidArgumentException(
                'State object ' . get_class($state) . ' must implement toArray() method'
            );
        }

        return DerivedSchema::fromData($state->toArray());
    }

    /**
     * Validate State VO against derived schema
     *
     * @throws StateValidationException If validation fails
     */
    public function validateState(object $state, DerivedSchema $schema): void
    {
        $result = $this->checkState($state, $schema);

        if ($result->isFailed()) {
            // Get component name from state class
            $componentName = get_class($state);

            if ($result->getErrorCount() === 1 && count($result->getFieldErrors()) === 1) {
                // Single field error
                $field = array_key_first($result->getFieldErrors());
                $error = $result->getFieldError($field);

                // Parse error message to extract type info
                if (str_contains($error, 'expects type')) {
                    preg_match("/expects type '([^']+)' but got '([^']+)'/", $error, $matches);
                    if (count($matches) === 3) {
                        throw StateValidationException::typeMismatch(
                            $componentName,
                            $field,
                            $matches[1],
                            $matches[2]
                        );
                    }
                } elseif (str_contains($error, 'is missing')) {
                    throw StateValidationException::missingField($componentName, $field, $schema);
                } elseif (str_contains($error, 'not in schema')) {
                    throw StateValidationException::unexpectedField($componentName, $field, $schema);
                }
            }

            // Multiple errors
            throw StateValidationException::multipleErrors(
                $componentName,
                array_merge($result->getErrors(), array_values($result->getFieldErrors()))
            );
        }
    }

    /**
     * Check if State VO matches schema without throwing
     */
    public function checkState(object $state, DerivedSchema $schema): ValidationResult
    {
        // State VOs must implement toArray() for validation
        if (!method_exists($state, 'toArray')) {
            return ValidationResult::invalid([
                'State object ' . get_class($state) . ' must implement toArray() method'
            ], []);
        }

        $errors = [];
        $fieldErrors = [];
        $stateData = $state->toArray();

        // Check for missing required fields
        foreach ($schema->getFields() as $field) {
            if (! array_key_exists($field, $stateData)) {
                if (! $schema->isNullable($field)) {
                    $fieldErrors[$field] = "Field '{$field}' is missing but required by schema";
                }
            }
        }

        // Check for unexpected fields and type mismatches
        foreach ($stateData as $field => $value) {
            if (! $schema->hasField($field)) {
                $fieldErrors[$field] = "Field '{$field}' is not in schema";

                continue;
            }

            $expectedType = $schema->getType($field);
            $actualType = $this->getValueType($value);

            if (! $this->typesMatch($expectedType, $actualType, $value)) {
                $fieldErrors[$field] = "Field '{$field}' expects type '{$expectedType}' but got '{$actualType}'";
            }
        }

        if (! empty($errors) || ! empty($fieldErrors)) {
            return ValidationResult::invalid($errors, $fieldErrors);
        }

        return ValidationResult::valid();
    }

    /**
     * Get type of value
     */
    private function getValueType(mixed $value): string
    {
        if ($value === null) {
            return 'null';
        }

        if (is_object($value)) {
            return get_class($value);
        }

        return get_debug_type($value);
    }

    /**
     * Check if types match
     */
    private function typesMatch(string $expectedType, string $actualType, mixed $value): bool
    {
        // Exact match
        if ($expectedType === $actualType) {
            return true;
        }

        // Null handling
        if ($expectedType === 'null') {
            return true;
        }

        // If value is null and schema expects non-null type, reject
        if ($value === null && $expectedType !== 'null') {
            return false;
        }

        // Object type matching - check if actual type is instance of expected type
        if (class_exists($expectedType) && is_object($value)) {
            return $value instanceof $expectedType;
        }

        return false;
    }
}
