<?php

namespace Ulmus\Repository;

use Ulmus\{Attribute\Property\Field,
    Ulmus,
    Annotation,
    Attribute,
    Query,
    Common,
    Common\EntityResolver,
    Repository,
    Event,
    EntityCollection};

use Ulmus\Annotation\Property\{Filter, OrderBy, Relation, Relation\Ignore as RelationIgnore, Where, WithJoin, };

use Closure;

class RelationBuilder
{
    const SUBQUERY_FIELD_SUFFIX = "%s\$collection";

    const JOIN_FIELD_SEPARATOR = "%s\$";

    protected Repository $repository;

    protected object|string $entity;

    protected EntityResolver $resolver;

    protected array $orders;

    protected array $wheres;

    protected array $filters;

    protected array $joins;

    public function __construct(string|object $entity, ? Repository $repository = null)
    {
        $this->entity = $entity;
        $this->resolver = $entity::resolveEntity();

        if ($repository) {
            $this->repository = $repository;
        }
    }

    public function searchRelation(string $name) : object|bool
    {
        # Resolve relations here if one is called
        if ( $this->entity->isLoaded() ) {
            if ( false !== ( $dataset = $this->fetchFromDataset($name) ) ) {
                return $dataset;
            }

            return $this->resolveRelation($name) ?: $this->resolveVirtual($name) ?: false;
        }
        else {
            if ( $relation = $this->resolver->searchFieldAnnotation($name, [ Attribute\Property\Relation::class , Relation::class ] ) ) {
                return $this->instanciateEmptyObject($name, $relation);
            }
            elseif ($virtual = $this->resolveVirtual($name)) {
                return $virtual;
            }
        }

        return false;
    }

    protected function resolveVirtual(string $name) : bool|object
    {
        if (null !== ($virtual = $this->resolver->searchFieldAnnotation($name, [ Attribute\Property\Virtual::class, Annotation\Property\Virtual::class ]))) {
            if ($virtual->closure ?? false) {
                return call_user_func_array($virtual->closure, [ $this->entity ]);
            }

            return call_user_func_array([ $this->entity, $virtual->method ], [ $this->entity ]);
        }

        return false;
    }

    protected function resolveRelation(string $name) : bool|object
    {
        if ( null !== ( $relation = $this->resolver->searchFieldAnnotation($name, [ Attribute\Property\Relation::class, Relation::class ] ) ) ) {
            $this->orders = $this->resolver->searchFieldAnnotationList($name, [ Attribute\Property\OrderBy::class, OrderBy::class ] );
            $this->wheres = $this->resolver->searchFieldAnnotationList($name, [ Attribute\Property\Where::class, Where::class ] );
            $this->filters = $this->resolver->searchFieldAnnotationList($name, [ Attribute\Property\Filter::class, Filter::class ] );
            $this->joins = $this->resolver->searchFieldAnnotationList($name, [ Attribute\Property\WithJoin::class, WithJoin::class ] );

            switch( true ) {
                case $relation->isOneToOne():
                    if ( $relation->hasBridge() ) {
                        #dump($name, $relation);
                        # @TODO ! dump($this->relationAnnotations($name, $relation));
                    }
                    else {
                        $this->oneToOne($name, $relation);
                    }

                    $this->entity->eventExecute(Event\EntityRelationLoadInterface::class, $name, $this->repository);

                    return call_user_func([ $this->repository, $relation->function() ]) ?? $this->instanciateEmptyEntity($name, $relation);

                case $relation->isOneToMany():
                    $this->oneToMany($name, $relation);

                    $this->entity->eventExecute(Event\EntityRelationLoadInterface::class, $name, $this->repository);

                    return call_user_func([ $this->repository, $relation->function() ]);

                case $relation->isManyToMany():
                    $this->manyToMany($name, $relation, $relationRelation);

                    $this->entity->eventExecute(Event\EntityRelationLoadInterface::class, $name, $this->repository);

                    $results = call_user_func([ $this->repository, 'loadAll' ]);

                    if ($relation->bridgeField ?? false) {
                        $collection = $relation->bridge::entityCollection();

                        foreach($results as $entity) {
                            $collection->append($this->fetchFromDataset($relation->bridgeField, $entity->entityLoadedDataset));
                        }

                        $this->entity->{$relation->bridgeField} = $collection;
                    }

                    return $results;
            }
        }

        return false;
    }

    protected function applyFilter(Repository $repository, string $name) : Repository
    {
        foreach($this->filters ?? [] as $filter) {
            $repository = call_user_func_array([ $this->entity, $filter->method ], [ $repository, $name, false ]);
        }

        return $repository;
    }

    protected function applyWhere() : void
    {
        if ($this->wheres) {
            $this->repository->open();

            foreach($this->wheres as $condition) {
                $this->repository->where($condition->field, $condition->getValue($this->entity), $condition->operator);
            }

            $this->repository->close();
        }
    }

    protected function applyOrderBy() : void
    {
        foreach($this->orders as $item) {
            $this->repository->orderBy($item->field, $item->order);
        }
    }

    protected function instanciateEmptyEntity(string $name, Relation|Attribute\Property\Relation $relation) : object
    {
        $class = $relation->entity ?? $this->resolver->properties[$name]['type'];

        return new $class();
    }


    protected function instanciateEmptyObject(string $name, Relation|Attribute\Property\Relation $relation) : object
    {
        switch( true ) {
            case $relation->isOneToOne():
                return $this->instanciateEmptyEntity($name, $relation);

            case $relation->isOneToMany():
                return ($relation->entity ?? $this->resolver->properties[$name]['type'])::entityCollection();

            case $relation->isManyToMany():
                extract($this->relationAnnotations($name, $relation));

                return $relation->bridgeField ?? false ? $relation->bridge::entityCollection() : $relationRelation->entity::entityCollection();
        }

        throw new \InvalidArgumentException("Unknown or no relation was provided as relation type.");
    }

    protected function fetchFromDataset($name, ? array $data = null) : object|bool
    {
        $annotation = $this->resolver->searchFieldAnnotation($name, [ Attribute\Property\Join::class, Annotation\Property\Join::class ]) ?:
            $this->resolver->searchFieldAnnotation($name, [ Attribute\Property\Relation::class, Annotation\Property\Relation::class ]);

        if ( $annotation ) {
            $vars = [];
            $len = strlen( $name ) + 1;

            $isRelation = ( $annotation instanceof Relation ) || ( $annotation instanceof Attribute\Property\Relation );

            if ( $isRelation && $annotation->isManyToMany() ) {
                $entity = $this->relationAnnotations($name, $annotation)['relationRelation']->entity;
            }
            else {
                $entity = $annotation->entity ?? $this->resolver->properties[$name]['type'];
            }

            $name = strtolower($name);

            foreach($data ?: $this->entity->entityLoadedDataset as $key => $value) {

                if ( $key === sprintf(static::SUBQUERY_FIELD_SUFFIX, strtolower($name)) ) {
                    if ($value) {
                        if ( null === ( $dataset = \json_decode($value, true) ) ) {
                            throw new \Exception(sprintf("JSON error '%s' from '%s'", \json_last_error_msg(), $value));
                        }

                        return $entity::entityCollection()->fromArray($dataset)->iterate(fn($e) => $e->resetVirtualProperties());
                    }
                    else {
                        return $entity::entityCollection();
                    }
                }
                elseif ( substr($key, 0, $len ) === sprintf(static::JOIN_FIELD_SEPARATOR, strtolower($name)) ) {
                    $vars[substr($key, $len)] = $value;
                }
                else {
                    # load here for API objects !
                }
            }

            if ($vars) {
                if ( [] !== $data = (array_values(array_unique($vars)) !== [ null ] ? $vars : []) ) {
                    return ( new $entity() )->fromArray($data)->resetVirtualProperties();
                }
                else {
                    return new $entity();
                }
            }
        }

        return false;
    }

    public function oneToOne(string $name, Relation|Attribute\Property\Relation $relation) : Repository
    {
        return $this->oneToMany($name, $relation)->limit(1);
    }

    public function oneToMany(string $name, Relation|Attribute\Property\Relation $relation) : Repository
    {
        $baseEntity = $relation->entity ?? $this->resolver->properties[$name]['type'];

        $this->repository = $baseEntity::repository();

        $this->applyWhere();

        $this->applyOrderBy();

        $field = $relation->key;

        if ($relation->foreignKey) {
            if ( $relation->generateKey ) {
                $value = call_user_func_array($relation->generateKey, [ $this->entity ]);
            }
            else {
                $value = $this->entity->$field;
            }

            $this->repository->where( is_object($relation->foreignKey) ? $relation->foreignKey : $baseEntity::field($relation->foreignKey), $value );
        }

        return $this->applyFilter($this->repository, $name);
    }

    public function manyToMany(string $name, Relation|Attribute\Property\Relation $relation, Relation|Attribute\Property\Relation & $relationRelation = null, bool $selectBridgeField = true) : Repository
    {
        extract($this->relationAnnotations($name, $relation));

        $this->repository = $relationRelation->entity::repository();

        $bridgeAlias = $relation->bridgeField ?? uniqid("bridge_");

        $relationAlias = $relation->field;

        $this->repository->select("{$this->repository->alias}.*")
            ->join(Query\Join::TYPE_INNER, $bridgeEntity->tableName(), $relation->bridge::field($relationRelation->key, $bridgeAlias), $relationRelation->entity::field($relationRelation->foreignKey), $bridgeAlias, function($join) {

            })->join(Query\Join::TYPE_INNER, $this->entity::resolveEntity()->tableName(), $relation->bridge::field($bridgeRelation->key, $bridgeAlias), $this->entity::field($bridgeRelation->foreignKey, $relationAlias), $relationAlias, function($join) {

            })->where( $this->entity::field($bridgeRelation->foreignKey, $relationAlias), is_string($this->entity) ? $this->entity::field($bridgeRelation->foreignKey) : $this->entity->{$bridgeRelation->foreignKey} );

        $this->applyWhere();

        $this->applyOrderBy();

        if ($selectBridgeField && $relation->bridgeField) {
            $this->repository->selectEntity($relation->bridge, $bridgeAlias, $bridgeAlias);
        }

        return $this->applyFilter($this->repository, $name);
    }

    public static function relationAnnotations(string $name, Relation|Attribute\Property\Relation $relation) : array
    {
        if ( $relation->isOneToOne() || $relation->isManyToMany() ) {
            if ( ! $relation->hasBridge() ) {
                throw new \Exception("Your many-to-many @Relation() from variable `$name` is missing a 'bridge' field.");
            }

            $bridgeEntity = Ulmus::resolveEntity($relation->bridge);
            $bridgeRelation = $bridgeEntity->searchFieldAnnotation($relation->field, [ Attribute\Property\Relation::class, Relation::class ]);
            $relationRelation = $bridgeEntity->searchFieldAnnotation($relation->foreignField, [ Attribute\Property\Relation::class, Relation::class ]);

            if ($relationRelation === null) {
                throw new \Exception("@Relation annotation not found for field `{$relation->foreignField}` in entity {$relation->bridge}");
            }

            $relationRelation->entity ??= $relation->bridge::resolveEntity()->properties[$relation->foreignField]['type'];

            return [
                'bridgeEntity' => $bridgeEntity,
                'bridgeRelation' => $bridgeRelation,
                'relationRelation' => $relationRelation,
            ];
        }

        return [];
    }
}