<?php

namespace Velis\Model\RelationLoader;

use Velis\Model\DataObject;
use Velis\Model\RelationLoader\Exceptions\NotLoadedRelationException;
use Velis\Model\RelationLoader\Exceptions\UndefinedRelationException;
use Velis\Model\RelationLoader\Relations\HasManyRelation;
use Velis\Model\RelationLoader\Relations\HasManyThroughRelation;
use Velis\Model\RelationLoader\Relations\HasOneRelation;
use Velis\Model\RelationLoader\Relations\HasOneThroughRelation;
use Velis\Model\RelationLoader\Relations\Relation;

trait RelationTrait
{
    /**
     * Container for the eager loaded relations.
     *
     * @var array<string, DataObject|DataObject[]|null>
     */
    public array $relationsContainer = [];

    // region Relations' definitions
    /**
     * @template T
     * @param class-string<T> $model
     * @param string $foreignKey Name of the foreign key.
     * @param string|null $localKey In most cases, foreign and local keys are named in the same way.
     *                              So if the local key's name is not provided, use the foreign key's name.
     * @return HasOneRelation<T, static>
     */
    protected function hasOne(string $model, string $foreignKey, ?string $localKey = null): HasOneRelation
    {
        return new HasOneRelation($model, $foreignKey, $localKey, $this);
    }

    /**
     * @template T
     * @param class-string<T> $model
     * @param string $foreignKey Name of the foreign key.
     * @param string|null $localKey In most cases, foreign and local keys are named in the same way.
     *                              So if the local key's name is not provided, use the foreign key's name.
     * @return HasManyRelation<T, static>
     */
    protected function hasMany(string $model, string $foreignKey, ?string $localKey = null): HasManyRelation
    {
        return new HasManyRelation($model, $foreignKey, $localKey, $this);
    }

    /**
     * @template T
     * @param class-string<T> $model
     * @return HasManyThroughRelation<T, static>
     */
    protected function hasManyThrough(
        string $model,
        string $junctionTable,
        string $junctionForeignKey,
        string $foreignKey,
        string $junctionLocalKey,
        string $relatedKey,
        ?string $tableName = null
    ): HasManyThroughRelation {
        return new HasManyThroughRelation(
            $model,
            $junctionTable,
            $junctionForeignKey,
            $foreignKey,
            $junctionLocalKey,
            $relatedKey,
            $this,
            $tableName
        );
    }

    /**
     * @template T
     * @param class-string<T> $model
     * @return HasOneThroughRelation<T, static>
     */
    protected function hasOneThrough(
        string $model,
        string $junctionTable,
        string $junctionForeignKey,
        string $foreignKey,
        string $junctionLocalKey,
        string $relatedKey
    ): HasOneThroughRelation {
        return new HasOneThroughRelation(
            $model,
            $junctionTable,
            $junctionForeignKey,
            $foreignKey,
            $junctionLocalKey,
            $relatedKey,
            $this
        );
    }
    // endregion

    // region Getters

    /**
     * Check whether relation was loaded.
     */
    public function hasRelation(string $relationName): bool
    {
        return array_key_exists($relationName, $this->relationsContainer)
            || array_key_exists('relation' . ucfirst($relationName), $this->relationsContainer);
    }

    /**
     * Get related model(s) from the container, using full name (with "relation" prefix) or short name.
     * If a relation is yet not loaded, it will be loaded.
     *
     * @return DataObject|DataObject[]|null
     * @throws NotLoadedRelationException
     * @throws UndefinedRelationException
     */
    public function getRelation(string $relationName): array|DataObject|null
    {
        if (array_key_exists($relationName, $this->relationsContainer)) {
            return $this->relationsContainer[$relationName];
        }

        $relationFullName = 'relation' . ucfirst($relationName);
        if (array_key_exists($relationFullName, $this->relationsContainer)) {
            return $this->relationsContainer[$relationFullName];
        }

        return $this->loadAskedRelation($relationName);
    }
    // endregion

    // region Setters
    /**
     * Manually set relation for the target model.
     */
    public function setRelation(string $relationName, $relatedData): void
    {
        $this->relationsContainer[$relationName] = $relatedData;
    }

    /**
     * Load relation that was not previously declared in the specific target model.
     * Pass the proper Relation object (for instance: HasManyRelation, HasOneRelation, etc.)
     *
     * @template T of Relation
     * @param T $relation
     * @return T
     */
    public function useRawRelation(Relation $relation): Relation
    {
        $relation->setTargetModel($this);

        return $relation;
    }
    // endregion

    // region Utility
    /**
     * @throws UndefinedRelationException
     */
    private function getRelationMethodName(string $relationName): string
    {
        if (method_exists($this, $relationName)) {
            return $relationName;
        }

        if (method_exists($this, 'relation' . ucfirst($relationName))) {
            return 'relation' . ucfirst($relationName);
        }

        throw new UndefinedRelationException(relationName: $relationName, targetModel: $this);
    }

    /**
     * @throws NotLoadedRelationException
     * @throws UndefinedRelationException
     * @return DataObject|DataObject[]|null
     */
    private function loadAskedRelation(string $relationName)
    {
        $relationName = $this->getRelationMethodName($relationName);

        $relation = $this->$relationName();

        if ($relation instanceof HasOneRelation || $relation instanceof HasOneThroughRelation) {
            $relatedData = $relation->getOne();
            $this->setRelation($relationName, $relatedData);

            return $relatedData;
        }

        $relatedData = $relation->get();
        $this->setRelation($relationName, $relatedData);

        return $relatedData;
    }
    // endregion
}
