<?php

declare(strict_types=1);

namespace App\Framework\Database\Metadata;

use App\Framework\Database\Attributes\Entity;
use App\Framework\Database\Attributes\Column;
use App\Framework\Database\Attributes\Type;
use App\Framework\Database\Exception\DatabaseException;
use ReflectionClass;
use ReflectionProperty;
use ReflectionParameter;
use ReflectionNamedType;
use ReflectionUnionType;

final class MetadataExtractor
{
    public function extractMetadata(string $entityClass): EntityMetadata
    {
        try {
            $reflection = new ReflectionClass($entityClass);
            $entityAttribute = $this->getEntityAttribute($reflection);

            if (!$entityAttribute) {
                throw new DatabaseException("Class {$entityClass} is not marked as Entity");
            }

            $tableName = $entityAttribute->tableName ?? $this->getTableNameFromClass($entityClass);
            $idColumn = $entityAttribute->idColumn ?? 'id';

            $properties = $this->extractProperties($reflection);
            $relations = $this->extractRelations($reflection);
            $idProperty = $this->findIdProperty($reflection, $properties, $idColumn);

            return new EntityMetadata(
                entityClass: $entityClass,
                tableName: $tableName,
                idColumn: $idColumn,
                idProperty: $idProperty,
                reflection: $reflection,
                properties: array_merge($properties, $relations)
            );

        } catch (\ReflectionException $e) {
            throw new DatabaseException("Failed to analyze entity {$entityClass}: {$e->getMessage()}", 0, $e);
        }
    }

    private function extractProperties(ReflectionClass $reflection): array
    {
        $properties = [];
        $constructor = $reflection->getConstructor();

        if (!$constructor) {
            return $properties;
        }

        foreach ($constructor->getParameters() as $param) {
            $propertyMetadata = $this->extractPropertyMetadata($param, $reflection);
            $properties[$param->getName()] = $propertyMetadata;
        }

        return $properties;
    }

    /**
     * Extrahiert Relation-Properties die nicht im Constructor sind
     */
    private function extractRelations(ReflectionClass $reflection): array
    {
        $relations = [];

        foreach ($reflection->getProperties() as $property) {

            if ($property->isPromoted()) {
                continue; // Überspringen, da bereits in extractProperties erfasst
            }

            $typeAttribute = $this->getTypeAttribute($property);

            if ($typeAttribute) {
                $relationMetadata = $this->extractRelationMetadata($property, $typeAttribute, $reflection);
                $relations[$property->getName()] = $relationMetadata;
                continue;
            }

            // Prüfe ob der Property-Typ eine Entity-Klasse ist
            $propertyType = $property->getType();
            if ($propertyType instanceof \ReflectionNamedType) {
                $typeName = $propertyType->getName();

                // Prüfe ob die Typ-Klasse eine Entity ist
                if (class_exists($typeName) && $this->isEntityClass($typeName)) {
                    $relationMetadata = $this->createRelationMetadataForEntityProperty($property, $typeName, $reflection);
                    $relations[$property->getName()] = $relationMetadata;
                    continue;
                }
            }
        }

        return $relations;
    }

    private function isEntityClass(string $className): bool
    {
        try {
            $classReflection = new \ReflectionClass($className);
            return $this->getEntityAttribute($classReflection) !== null;
        } catch (\ReflectionException) {
            return false;
        }
    }

    private function createRelationMetadataForEntityProperty(
        \ReflectionProperty $property,
        string $targetClass,
        \ReflectionClass $parentReflection
    ): PropertyMetadata {
        $propertyName = $property->getName();

        $relationType = $this->determineRelationType($property);

        if($relationType === 'belongsTo') {
            return $this->createBelongsToRelationMetadata($property, $targetClass, $parentReflection);
        }

        return $this->createHasRelationMetadata($property, $targetClass, $parentReflection, $relationType);

/*
        // Foreign Key ist der Primary Key der Ziel-Entity
        $targetReflection = new \ReflectionClass($targetClass);
        $targetEntityAttribute = $this->getEntityAttribute($targetReflection);
        $foreignKey = $targetEntityAttribute?->idColumn ?? 'id';

        // Local Key ist die entsprechende Spalte in der aktuellen Entity
        // Für "image" Property -> "image_id"
        $localKey = $propertyName . '_id';


        // Typ-Analyse der Property
        $type = $property->getType();
        $typeInfo = $this->analyzeType($type);

        return new PropertyMetadata(
            name: $propertyName,
            columnName: '',
            type: $typeInfo['mainType'],
            nullable: $typeInfo['nullable'],
            hasDefault: true,
            defaultValue: null,
            allTypes: $typeInfo['allTypes'],
            primary: false,
            autoIncrement: false,
            isRelation: true,
            relationTargetClass: $targetClass,
            relationForeignKey: $foreignKey,
            relationLocalKey: $localKey,
            relationType: 'one-to-one'
        );*/
    }

    public function determineRelationType(\ReflectionProperty $property): string
    {
        $propertyType = $property->getType();

        if($propertyType instanceof \ReflectionNamedType && $propertyType->getName() === 'array') {
            return 'hasMany';
        }

        $parentReflection = $property->getDeclaringClass();
        $possibleForeignKeyProperty = $property->getName() . 'Id';
        
        if($this->hasConstructorParameter($parentReflection, $possibleForeignKeyProperty)) {
            return 'belongsTo';
        }

        $possibleForeignKeyProperty2 = $property->getName() . '_id';
        if($this->hasConstructorParameter($parentReflection, $possibleForeignKeyProperty2)) {
            return 'belongsTo';
        }

        return 'one-to-one';
    }

    private function hasConstructorParameter(ReflectionClass $reflection, string $paramName): bool
    {
        $constructor = $reflection->getConstructor();
        if(!$constructor) {
            return false;
        }

        foreach($constructor->getParameters() as $param) {
            if($param->getName() === $paramName) {
                return true;
            }
        }
        return false;
    }

    private function createBelongsToRelationMetadata(\ReflectionProperty $property, string $targetClass, ReflectionClass $parentReflection): PropertyMetadata
    {
        $propertyName = $property->getName();

        $foreignKeyProperty = $this->findForeignKeyProperty($propertyName, $parentReflection);

        if(!$foreignKeyProperty) {
            throw new DatabaseException("Could not find foreign key property for property {$propertyName} in class {$parentReflection->getName()}");
        }

        $foreignKeyColumnName = $this->getColumnName($foreignKeyProperty, $parentReflection);

        $type = $property->getType();
        $typeInfo = $this->analyzeType($type);

        return new PropertyMetadata(
            name: $propertyName,
            columnName: '',
            type: $typeInfo['mainType'],
            nullable: $typeInfo['nullable'],
            hasDefault: true,
            defaultValue: null,
            allTypes: $typeInfo['allTypes'],
            primary: false,
            autoIncrement: false,
            isRelation: true,
            relationTargetClass: $targetClass,
            relationForeignKey: $foreignKeyColumnName,  // Jetzt die Spalte, nicht der Parameter-Name
            relationLocalKey: '', // Wird in belongsTo nicht verwendet
            relationType: 'belongsTo'
        );
    }

    private function findForeignKeyProperty(string $propertyName, ReflectionClass $parentReflection): ?string
    {
        $constructor = $parentReflection->getConstructor();
        if (!$constructor) {
            return null;
        }

        // Suche nach {property}Id
        foreach ($constructor->getParameters() as $param) {
            if ($param->getName() === $propertyName . 'Id') {
                return $param->getName();
            }
        }

        // Suche nach {property}_id (falls das der Parameter-Name ist)
        foreach ($constructor->getParameters() as $param) {
            if ($param->getName() === $propertyName . '_id') {
                return $param->getName();
            }
        }

        return null;
    }

    private function createHasRelationMetadata(\ReflectionProperty $property, string $targetClass, ReflectionClass $parentReflection, string $relationType): PropertyMetadata
    {
        $propertyName = $property->getName();

        $targetReflection = new ReflectionClass($targetClass);
        $targetEntityAttribute = $this->getEntityAttribute($targetReflection);
        $foreignKey = $targetEntityAttribute?->idColumn ?? 'id';

        $entityAttribute = $this->getEntityAttribute($parentReflection);
        $localKey = $entityAttribute?->idColumn ?? 'id';

        $type = $property->getType();
        $typeInfo = $this->analyzeType($type);

        return new PropertyMetadata(
            name: $propertyName,
            columnName: '',
            type: $typeInfo['mainType'],
            nullable: $typeInfo['nullable'],
            hasDefault: true,
            defaultValue: $relationType === 'hasMany' ? [] : null,
            allTypes: $typeInfo['allTypes'],
            primary: false,
            autoIncrement: false,
            isRelation: true,
            relationTargetClass: $targetClass,
            relationForeignKey: $foreignKey,
            relationLocalKey: $localKey,
            relationType: $relationType
        );
    }


    private function extractRelationMetadata(ReflectionProperty $property, Type $typeAttribute, ReflectionClass $classReflection): PropertyMetadata
    {
        $propertyName = $property->getName();

        // Ermittle foreign key automatisch falls nicht gesetzt
        $foreignKey = $typeAttribute->foreignKey;
        if (!$foreignKey) {
            // Konvention: {parent_class}_id
            $shortName = strtolower($classReflection->getShortName());
            $foreignKey = $shortName . '_id';
        }

        // Ermittle local key automatisch falls nicht gesetzt
        $localKey = $typeAttribute->localKey;
        if (!$localKey) {
            // Verwende ID-Property der Entity
            $entityAttribute = $this->getEntityAttribute($classReflection);
            $localKey = $entityAttribute?->idColumn ?? 'id';
        }

        // Typ-Analyse der Property
        $type = $property->getType();
        $typeInfo = $this->analyzeType($type);

        return new PropertyMetadata(
            name: $propertyName,
            columnName: '', // Relationen haben keine direkte Spalte
            type: $typeInfo['mainType'],
            nullable: $typeInfo['nullable'],
            hasDefault: true, // Relationen haben immer einen Default (leeres Array)
            defaultValue: [],
            allTypes: $typeInfo['allTypes'],
            primary: false,
            autoIncrement: false,
            isRelation: true,
            relationTargetClass: $typeAttribute->targetClass,
            relationForeignKey: $foreignKey,
            relationLocalKey: $localKey,
            relationType: $typeAttribute->type
        );
    }

    private function extractPropertyMetadata(ReflectionParameter $param, ReflectionClass $classReflection): PropertyMetadata
    {
        $paramName = $param->getName();

        // Column-Name aus Attribute oder Parameter-Name
        $columnName = $this->getColumnName($paramName, $classReflection);

        // Typ-Analyse
        $type = $param->getType();
        $typeInfo = $this->analyzeType($type);

        // Default-Werte und Nullable
        $hasDefault = $param->isDefaultValueAvailable();
        $defaultValue = $hasDefault ? $param->getDefaultValue() : null;
        $nullable = $typeInfo['nullable'] || $hasDefault;

        // Primary Key und Auto-Increment Eigenschaften aus Attribut auslesen
        $columnAttribute = null;
        $primary = false;
        $autoIncrement = false;

        try {
            $property = $classReflection->getProperty($paramName);
            $columnAttribute = $this->getColumnAttribute($property);
            if ($columnAttribute) {
                $primary = $columnAttribute->primary;
                $autoIncrement = $columnAttribute->autoIncrement;
                if ($columnAttribute->nullable) {
                    $nullable = true;
                }
            }
        } catch (\ReflectionException) {
            // Property nicht gefunden
        }

        return new PropertyMetadata(
            name: $paramName,
            columnName: $columnName,
            type: $typeInfo['mainType'],
            nullable: $nullable,
            hasDefault: $hasDefault,
            defaultValue: $defaultValue,
            allTypes: $typeInfo['allTypes'],
            primary: $primary,
            autoIncrement: $autoIncrement
        );
    }

    private function getColumnName(string $paramName, ReflectionClass $classReflection): string
    {
        try {
            $property = $classReflection->getProperty($paramName);
            $columnAttribute = $this->getColumnAttribute($property);
            return $columnAttribute?->name ?? $paramName;
        } catch (\ReflectionException) {
            return $paramName;
        }
    }

    private function analyzeType(?\ReflectionType $type): array
    {
        if (!$type) {
            return [
                'mainType' => 'mixed',
                'allTypes' => ['mixed'],
                'nullable' => true
            ];
        }

        if ($type instanceof ReflectionNamedType) {
            return [
                'mainType' => $type->getName(),
                'allTypes' => [$type->getName()],
                'nullable' => $type->allowsNull()
            ];
        }

        if ($type instanceof ReflectionUnionType) {
            $types = [];
            $nullable = false;

            foreach ($type->getTypes() as $unionType) {
                if ($unionType instanceof ReflectionNamedType) {
                    $typeName = $unionType->getName();
                    if ($typeName === 'null') {
                        $nullable = true;
                    } else {
                        $types[] = $typeName;
                    }
                }
            }

            return [
                'mainType' => $types[0] ?? 'mixed',
                'allTypes' => $types,
                'nullable' => $nullable
            ];
        }

        return [
            'mainType' => 'mixed',
            'allTypes' => ['mixed'],
            'nullable' => true
        ];
    }

    private function getEntityAttribute(ReflectionClass $reflection): ?Entity
    {
        $attributes = $reflection->getAttributes(Entity::class);
        return $attributes ? $attributes[0]->newInstance() : null;
    }

    private function getColumnAttribute(ReflectionProperty $property): ?Column
    {
        $attributes = $property->getAttributes(Column::class);
        return $attributes ? $attributes[0]->newInstance() : null;
    }

    private function getTypeAttribute(ReflectionProperty $property): ?Type
    {
        $attributes = $property->getAttributes(Type::class);
        return $attributes ? $attributes[0]->newInstance() : null;
    }

    private function getTableNameFromClass(string $className): string
    {
        $shortName = new ReflectionClass($className)->getShortName();
        return strtolower($shortName) . 's';
    }

    /**
     * Ermittelt den Property-Namen, der als ID dient
     */
    private function findIdProperty(ReflectionClass $reflection, array $properties, string $idColumn): string
    {
        // 1. Suche nach Property mit primary=true im Column-Attribut
        foreach ($properties as $propName => $propMetadata) {
            try {
                $property = $reflection->getProperty($propName);
                $columnAttribute = $this->getColumnAttribute($property);

                if ($columnAttribute && $columnAttribute->primary) {
                    return $propName;
                }
            } catch (\ReflectionException) {
                // Property nicht gefunden, ignorieren
            }
        }

        // 2. Suche nach Property, die dem idColumn entspricht
        foreach ($properties as $propName => $propMetadata) {
            if ($propMetadata->columnName === $idColumn) {
                return $propName;
            }
        }

        // 3. Fallback: Property mit Namen 'id'
        if (isset($properties['id'])) {
            return 'id';
        }

        // 4. Wenn nichts passt: Erste Property (nicht ideal, aber besser als nichts)
        if (!empty($properties)) {
            return array_key_first($properties);
        }

        throw new DatabaseException("Keine ID-Property für Entity {$reflection->getName()} gefunden");
    }
}
