<?php

declare(strict_types=1);

namespace App\Framework\GraphQL\Schema;

use App\Framework\Exception\ErrorCode;
use App\Framework\Exception\FrameworkException;
use App\Framework\GraphQL\Types\GraphQLScalarType;
use App\Framework\GraphQL\ValueObjects\GraphQLTypeRef;
use ReflectionNamedType;
use ReflectionType;
use ReflectionUnionType;

/**
 * Resolves PHP types to GraphQL types
 *
 * Convention over Configuration:
 * - int → Int!
 * - ?int → Int
 * - string → String!
 * - ?string → String
 * - array → [Mixed]!
 * - Custom classes → CustomType!
 * - ?CustomClass → CustomType
 */
final readonly class TypeResolver
{
    /**
     * Map of PHP scalar types to GraphQL scalar types
     */
    private const PHP_TO_GRAPHQL_SCALAR = [
        'int' => GraphQLScalarType::INT,
        'float' => GraphQLScalarType::FLOAT,
        'string' => GraphQLScalarType::STRING,
        'bool' => GraphQLScalarType::BOOLEAN,
    ];

    /**
     * Resolve ReflectionType to GraphQLTypeRef
     */
    public function resolveFromReflectionType(?ReflectionType $type): GraphQLTypeRef
    {
        if ($type === null) {
            // No type hint → Mixed (fallback)
            return GraphQLTypeRef::named('Mixed', nonNull: false);
        }

        if ($type instanceof ReflectionUnionType) {
            return $this->resolveUnionType($type);
        }

        if ($type instanceof ReflectionNamedType) {
            return $this->resolveNamedType($type);
        }

        throw FrameworkException::create(
            ErrorCode::VAL_BUSINESS_RULE_VIOLATION,
            'Unsupported reflection type'
        )->withData(['type_class' => get_class($type)]);
    }

    /**
     * Resolve named type to GraphQLTypeRef
     */
    private function resolveNamedType(ReflectionNamedType $type): GraphQLTypeRef
    {
        $typeName = $type->getName();
        $isNullable = $type->allowsNull();

        // Handle built-in types
        if ($type->isBuiltin()) {
            return $this->resolveBuiltinType($typeName, $isNullable);
        }

        // Handle custom classes
        return $this->resolveCustomType($typeName, $isNullable);
    }

    /**
     * Resolve builtin PHP type
     */
    private function resolveBuiltinType(string $typeName, bool $isNullable): GraphQLTypeRef
    {
        // Array → List type (we'll infer item type later or default to Mixed)
        if ($typeName === 'array') {
            return GraphQLTypeRef::listOf('Mixed', listNonNull: ! $isNullable, itemNonNull: false);
        }

        // Map scalar types
        if (isset(self::PHP_TO_GRAPHQL_SCALAR[$typeName])) {
            $scalarType = self::PHP_TO_GRAPHQL_SCALAR[$typeName];

            return GraphQLTypeRef::scalar($scalarType, nonNull: ! $isNullable);
        }

        // Mixed/object → Mixed type
        if ($typeName === 'mixed' || $typeName === 'object') {
            return GraphQLTypeRef::named('Mixed', nonNull: false);
        }

        throw FrameworkException::create(
            ErrorCode::VAL_BUSINESS_RULE_VIOLATION,
            "Unsupported builtin type: {$typeName}"
        )->withData(['type' => $typeName]);
    }

    /**
     * Resolve custom class type
     */
    private function resolveCustomType(string $className, bool $isNullable): GraphQLTypeRef
    {
        // Extract simple class name from fully qualified name
        $shortName = $this->extractGraphQLTypeName($className);

        return GraphQLTypeRef::named($shortName, nonNull: ! $isNullable);
    }

    /**
     * Extract GraphQL type name from PHP class name
     *
     * Examples:
     * - App\Domain\User\User → User
     * - App\Domain\User\UserType → User (removes Type suffix)
     * - App\ValueObjects\Email → Email
     */
    private function extractGraphQLTypeName(string $className): string
    {
        // Get short class name
        $parts = explode('\\', $className);
        $shortName = end($parts);

        // Remove "Type" suffix if present
        if (str_ends_with($shortName, 'Type') && strlen($shortName) > 4) {
            return substr($shortName, 0, -4);
        }

        return $shortName;
    }

    /**
     * Resolve union type (e.g., string|int)
     *
     * GraphQL doesn't support union of scalars directly,
     * so we'll create a GraphQL Union type or fall back to Mixed
     */
    private function resolveUnionType(ReflectionUnionType $unionType): GraphQLTypeRef
    {
        $types = $unionType->getTypes();
        $nonNullTypes = array_filter($types, fn ($t) => ! ($t instanceof ReflectionNamedType && $t->getName() === 'null'));

        // If only one non-null type → resolve it as nullable
        if (count($nonNullTypes) === 1) {
            $singleType = reset($nonNullTypes);
            if ($singleType instanceof ReflectionNamedType) {
                return $this->resolveNamedType($singleType);
            }
        }

        // Multiple types → for now, return Mixed
        // TODO: Create proper GraphQL Union types
        return GraphQLTypeRef::named('Mixed', nonNull: false);
    }

    /**
     * Resolve array type with item type hint from PHPDoc
     *
     * Example: @var array<User> → [User]!
     */
    public function resolveArrayWithItemType(string $itemTypeName, bool $isNullable): GraphQLTypeRef
    {
        $graphQLItemType = $this->extractGraphQLTypeName($itemTypeName);

        return GraphQLTypeRef::listOf(
            $graphQLItemType,
            listNonNull: ! $isNullable,
            itemNonNull: true // Assume items are non-null by default
        );
    }
}
