<?php

declare(strict_types=1);

namespace App\Framework\Database\StoredProcedure;

use App\Framework\Database\ResultInterface;
use App\Framework\Database\TypeConverter;

/**
 * Maps database results to PHP objects
 */
final readonly class ResultMapper
{
    public function __construct(
        private TypeConverter $typeConverter
    ) {
    }

    /**
     * Map a database result to an array of objects of the specified class
     *
     * @template T of object
     * @param ResultInterface $result The database result
     * @param class-string<T> $className The class to map to
     * @return array<T> Array of mapped objects
     */
    public function mapToObjects(ResultInterface $result, string $className): array
    {
        $objects = [];
        $reflectionClass = new \ReflectionClass($className);

        foreach ($result->fetchAll() as $row) {
            $objects[] = $this->createObject($reflectionClass, $row);
        }

        return $objects;
    }

    /**
     * Map a database result to a single object of the specified class
     *
     * @template T of object
     * @param ResultInterface $result The database result
     * @param class-string<T> $className The class to map to
     * @return T|null The mapped object or null if no result
     */
    public function mapToObject(ResultInterface $result, string $className): ?object
    {
        $row = $result->fetch();
        if (! $row) {
            return null;
        }

        $reflectionClass = new \ReflectionClass($className);

        return $this->createObject($reflectionClass, $row);
    }

    /**
     * Map a database result to an associative array
     *
     * @param ResultInterface $result The database result
     * @return array<string, mixed> The mapped associative array
     */
    public function mapToAssociativeArray(ResultInterface $result): array
    {
        return $result->fetch() ?: [];
    }

    /**
     * Map a database result to an array of associative arrays
     *
     * @param ResultInterface $result The database result
     * @return array<array<string, mixed>> Array of mapped associative arrays
     */
    public function mapToAssociativeArrays(ResultInterface $result): array
    {
        return $result->fetchAll();
    }

    /**
     * Create an object from a database row
     *
     * @template T of object
     * @param \ReflectionClass<T> $reflectionClass The reflection class
     * @param array<string, mixed> $row The database row
     * @return T The created object
     */
    private function createObject(\ReflectionClass $reflectionClass, array $row): object
    {
        // Create a new instance of the class
        $object = $reflectionClass->newInstanceWithoutConstructor();

        // Map properties
        foreach ($row as $column => $value) {
            // Convert column name to property name (e.g., user_id -> userId)
            $propertyName = $this->convertColumnNameToPropertyName($column);

            // Check if the property exists
            if (! $reflectionClass->hasProperty($propertyName)) {
                continue;
            }

            $property = $reflectionClass->getProperty($propertyName);
            $property->setAccessible(true);

            // Convert the value if needed
            $convertedValue = $this->convertValueForProperty($property, $value);

            // Set the property value
            $property->setValue($object, $convertedValue);
        }

        return $object;
    }

    /**
     * Convert a column name to a property name (e.g., user_id -> userId)
     */
    private function convertColumnNameToPropertyName(string $columnName): string
    {
        return lcfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', $columnName))));
    }

    /**
     * Convert a database value to the appropriate PHP type for a property
     *
     * @param \ReflectionProperty $property The property to set
     * @param mixed $value The value from the database
     * @return mixed The converted value
     */
    private function convertValueForProperty(\ReflectionProperty $property, mixed $value): mixed
    {
        if ($value === null) {
            return null;
        }

        // Get the property type
        $type = $property->getType();
        if (! $type) {
            return $value; // No type information, return as is
        }

        $typeName = $type->getName();

        // Handle built-in types
        return match($typeName) {
            'int' => (int)$value,
            'float' => (float)$value,
            'bool' => (bool)$value,
            'string' => (string)$value,
            'array' => is_array($value) ? $value : json_decode($value, true),
            default => $this->handleComplexType($typeName, $value)
        };
    }

    /**
     * Handle complex types (objects, value objects, etc.)
     *
     * @param string $typeName The type name
     * @param mixed $value The value from the database
     * @return mixed The converted value
     */
    private function handleComplexType(string $typeName, mixed $value): mixed
    {
        // Check if the type is a class
        if (class_exists($typeName)) {
            // Use the TypeConverter if available
            if ($this->typeConverter !== null) {
                // Create a simple property metadata for conversion
                $propertyMetadata = new class ($typeName) {
                    public function __construct(private string $type)
                    {
                    }

                    public function getType(): string
                    {
                        return $this->type;
                    }

                    public function getName(): string
                    {
                        return 'dummy';
                    }

                    public function isNullable(): bool
                    {
                        return true;
                    }
                };

                return $this->typeConverter->convertFromDatabase($value, $propertyMetadata);
            }

            // Basic conversion for simple value objects with a constructor that takes a single value
            if (method_exists($typeName, '__construct')) {
                $constructor = new \ReflectionMethod($typeName, '__construct');
                if ($constructor->getNumberOfParameters() === 1) {
                    return new $typeName($value);
                }
            }
        }

        // Return as is if we can't convert
        return $value;
    }
}
