<?php

declare(strict_types=1);

namespace apexl\Io\includes;

use apexl\entityCore\collections\EntityCollection;
use apexl\entityCore\Entity as BaseEntity;
use apexl\entityCore\traits\HasCasts;
use apexl\Io\collections\LoaderConditionCollection;
use apexl\Io\collections\LoaderSortCollection;
use apexl\Io\collections\RelationCache;
use apexl\Io\collections\RelationCollection;
use apexl\Io\collections\VirtualFieldDefinitionCollection;
use apexl\Io\dto\LoaderCondition;
use apexl\Io\dto\LoaderSort;
use apexl\Io\dto\Relationship\BelongsTo;
use apexl\Io\dto\Relationship\BelongsToMany;
use apexl\Io\dto\Relationship\HasMany;
use apexl\Io\dto\Relationship\HasOne;
use apexl\Io\dto\Relationship\HasOneViaPivot;
use apexl\Io\dto\Relationship\OneToMany;
use apexl\Io\dto\VirtualFieldDefinition;
use apexl\Io\enums\EntityFinderJsonType;
use apexl\Io\exceptions\RecordNotFoundException;
use apexl\Io\factories\EntityFactory;
use apexl\Io\factories\EntityFactoryFactory;
use apexl\Io\interfaces\HasCreatorInterface;
use apexl\Io\interfaces\HasEntityFactory;
use apexl\Io\interfaces\HasModifierInterface;
use apexl\Io\interfaces\HasNotesInterface;
use apexl\Io\interfaces\HasParentInterface;
use apexl\Io\interfaces\RelationshipInterface;
use apexl\Io\operators\entityDatabaseOperator;
use apexl\Io\services\Cache;
use apexl\Io\services\EntityFinderService;
use apexl\Io\services\RelationDefinitionCache;
use apexl\Io\services\RelationResolver;
use apexl\Vault\enums\SortDirection;
use apexl\Vault\interfaces\driver;
use apexl\Vault\Vault;
use app\vendor\apexl\vault\src\Vault\exceptions\VaultException;
use Arrayy\Arrayy;
use Arrayy\Type\StringCollection;
use BackedEnum;
use Closure;
use DateTimeInterface;
use Exception;
use ForceUTF8\Encoding;
use JsonSerializable;
use ReflectionClass;
use stdClass;
use Stringy\Stringy;
use UnitEnum;
use function apexl\Io\container;

class Entity extends BaseEntity implements HasEntityFactory, JsonSerializable
{
    use HasCasts;

    public final const string HOOK__BEFORE_STORE = 'entity.store.before';
    public final const string HOOK__AFTER_STORE = 'entity.store.after';
    public final const string HOOK__BEFORE_FIRST_STORE = 'entity.store.before.first';
    public final const string HOOK__AFTER_FIRST_STORE = 'entity.store.after.first';

    public $primaryKey;
    //managing Data
    public $extends;
    protected $table;
    protected $dataFields = [];
    //optional mapping for human-readable field data.
    protected $readOnlyFields = [];

    //pager fields
    protected $safeFields;
    protected array $fieldConfig = [];
    protected $defaultPagerLimit = 25;
    protected $currentPagerOffset = 0;
    protected System $system;
    protected HookManager $hookManager;
    protected RelationCache $relationCache;
    protected Arrayy $virtualFieldsCache;
    protected Vault $db;
    private RelationDefinitionCache $relationDefinitionCache;

    public function __construct($table = '', $primaryKey = 'id', $extends = null)
    {
        $this->hookManager = container()->get(HookManager::class);
        $this->relationDefinitionCache = container()->get(RelationDefinitionCache::class);
        $this->relationCache = RelationCache::create();
        $this->virtualFieldsCache = Arrayy::create();
        $this->db = container()->get(Vault::class);

        $this->table = $table;
        $this->primaryKey = $primaryKey;
        parent::__construct();


        $this->setOperator(new entityDatabaseOperator($this->table, $this->primaryKey));
        $this->setReadOnlyFields([
            'id',
            "created",
            "created_by",
            "modified",
            "modified_by",
        ]);
        $this->extends = $extends;

        //we only want to run these if we're installed. //Hacky fix for now
        $this->getEntityDataFields();
        //set default blank object for meta
        $this->data["_metadata"] = (object) [];
    }

    /**
     * Function to get the fields for the entity (from the primary table).
     * Allows us to check what data is storable and strip data that isn't.
     * Will only return fields not specified as 'read only' in the entity settings.
     * @see the $readOnlyFields property.
     */
    public function getEntityDataFields()
    {
        if (empty($this->dataFields)) {
            $this->dataFields = $this->operator->getTableColumns();
            foreach ($this->dataFields as $entityField) {
                if (!in_array($entityField->Field, $this->readOnlyFields)) {
                    $this->safeFields[] = $entityField;
                }
            }
        }

        return $this->safeFields;
    }

    public function getTableColumns(): array
    {
        return $this->operator->getTableColumns();
    }

    /**
     * @throws RecordNotFoundException
     */
    public static function fromPrimaryKey(mixed $primaryKey): static
    {
        return static::from(new static()->getPrimaryKey(), $primaryKey);
    }

    /**
     * @throws RecordNotFoundException
     */
    public static function from(string $key, mixed $value): static
    {
        $entity = new static();

        if ($key === $entity->getPrimaryKey()) {
            $entity->load($value);
        } else {
            $entity->loadBy($key, $value);
        }

        if ($entity->{$entity->primaryKey} === null) {
            throw new RecordNotFoundException();
        }

        return $entity;
    }

    public function getPrimaryKey(): string
    {
        return $this->primaryKey;
    }

    public static function factory(): EntityFactory
    {
        return container()->make(EntityFactoryFactory::class)->make(static::class);
    }

    public static function entityFactoryBaseClass(): string
    {
        return EntityFactory::class;
    }

    public static function find(
        int|string $id,
        array $fields = ['*'],
    ): ?static {
        return new static()->_find($id, $fields);
    }

    protected function _find(
        int|string $id,
        array $fields = ['*'],
    ): ?static {
        return $this->_findWhere($this->getPrimaryKey(), $id, $fields);
    }

    protected function _findWhere(
        string $field,
        int|string|float|bool $value,
        array $fields = ['*'],
    ): ?static {
        $result = $this->_findMultiple(
            conditions: [
                LoaderCondition::equals(sprintf('%s.%s', $this->getTableName(), $field), $value),
            ],
            limit: 1,
            fields: $fields,
        );

        return $result->pop();
    }

    protected function _findMultiple(
        array|LoaderConditionCollection $conditions = [],
        ?int $limit = 20,
        int $offset = 0,
        string|array|LoaderSort|LoaderSortCollection $sorts = [],
        array|StringCollection $fields = ['*'],
        false|null|array &$paging = false,
    ): EntityCollection {
        $query = $this->findMultipleQuery(
            conditions: $conditions,
            limit: $limit,
            offset: $offset,
            sorts: $sorts,
            fields: $fields,
        );

        $items = $query->execute()->fetchAllAssoc();

        if ($paging !== false) {
            $result = $query->toCountQuery()->execute()->fetch();

            $paging = [
                $result->count,
                $limit ? (int) ceil($result->count / $limit) : 1,
            ];
        }

        $entities = array_map(fn(array $item): static => (new static())->hydrate($item), $items);

        return EntityCollection::createFromArray($entities);
    }

    public function findMultipleQuery(
        array|LoaderConditionCollection $conditions = [],
        ?int $limit = 20,
        int $offset = 0,
        string|array|LoaderSort|LoaderSortCollection $sorts = [],
        array|StringCollection $fields = ['*'],
        EntityFinderJsonType $json = EntityFinderJsonType::NONE,
        bool $group = false,
        bool $autoIncludePrimaryKey = true,
        ?string $tableAlias = null,
    ): driver {
        $entityFinderService = System::makeRegisteredService(EntityFinderService::class, [
            'entity' => $this,
            'json' => $json,
        ]);

        return $entityFinderService->query(
            conditions: $conditions,
            limit: $limit,
            offset: $offset,
            sorts: $sorts,
            fields: $fields,
            group: $group,
            autoIncludePrimaryKey: $autoIncludePrimaryKey,
            tableAlias: $tableAlias,
        );
    }

    public function hydrate(array $data): static
    {
        foreach ($data as $key => $value) {
            if ($relation = $this->relation($key)) {
                if (is_null($value)) {
                    $this->relationCache->set($relation->name, null);

                    continue;
                }

                if (is_string($value) && json_validate($value)) {
                    $value = json_decode($value, true);
                }

                $this->hydrateRelation($relation, $value);
                continue;
            }


            $this->setField($key, $value);
        }

        return $this;
    }

    public function relation(string $relation): ?RelationshipInterface
    {
        return System::getRegisteredService(RelationResolver::class)
            ->resolve($relation, $this);
    }

    private function hydrateRelation(RelationshipInterface|string $relation, array $data): void
    {
        $_relation = $relation instanceof RelationshipInterface ? $relation : $this->relation($relation);

        if (!$_relation) {
            return;
        }

        $this->relationCache->set($_relation->name, $_relation->hydrate($data));
    }

    /**
     * Allow entities to override this and specify a 'nice name'
     * @return string
     */
    public function getTableName(): string
    {
        return $this->table;
    }

    public static function findWhere(
        string $field,
        mixed $value,
        array $fields = ['*'],
    ): ?static {
        return new static()->_findWhere($field, $value, $fields);
    }

    public static function findMultiple(
        array|LoaderConditionCollection $conditions = [],
        ?int $limit = 20,
        int $offset = 0,
        string|array|LoaderSort|LoaderSortCollection $sorts = [],
        array|StringCollection $fields = ['*'],
        false|null|array &$paging = false,
    ): EntityCollection {
        return new static()->_findMultiple(
            $conditions,
            $limit,
            $offset,
            $sorts,
            $fields,
            $paging,
        );
    }

    /**
     * Allow entities to override this and specify a 'nice name'
     * @return string
     */
    public function getNiceName(): string
    {
        return Stringy::create($this->getEntityName())->snakeize()->humanize()->toString();
    }

    public function getEntityName(): string
    {
        return new ReflectionClass($this)->getShortName();
    }

    public function titleField(): string
    {
        return (string) ($this->name ?? $this->{$this->getPrimaryKey()});
    }

    public function getEntityType(): string
    {
        return __CLASS__;
    }

    public function getEntityTrueName(): string
    {
        return str_replace('Entity', '', $this->getEntityName());
    }

    /**     * By default, we want to store the current data object
     * @return $this
     * @throws Exception
     */
    public function store(bool $timestamps = true): static
    {
        $inserting = $this->getPrimaryKeyValue() === null;

        //make sure dataFields is set.
        $this->getEntityDataFields();
        $this->setStandardFields($timestamps);

        $data = $this->callHooks([
            $inserting ? Entity::HOOK__BEFORE_FIRST_STORE : null,
            Entity::HOOK__BEFORE_STORE
        ], [...$this->data], $this);

        parent::__call('store', [$data]);
        $this->virtualFieldsCache->clear();

        $this->callHooks([
            $inserting ? Entity::HOOK__AFTER_FIRST_STORE : null,
            Entity::HOOK__AFTER_STORE
        ], [...$this->data], $this);

        return $this;
    }

    public function getPrimaryKeyValue(): string|int|null
    {
        return $this->{$this->getPrimaryKey()};
    }

    public function setStandardFields(bool $timestamps = true): void
    {
        //set created / modified stamps if the fields exist
        if ($timestamps && isset($this->dataFields['created']) && !isset($this->data['created'])) {
            $this->data['created'] = time();
        }

        if ($timestamps && isset($this->dataFields['modified'])) {
            $this->data['modified'] = time();
        }
    }

    protected function callHooks(array $hooks, mixed $input, mixed ...$args): mixed
    {
        $data = $input;
        foreach (array_filter($hooks) as $hook) {
            $data = $this->callHook($hook, $input, ...$args);
        }

        return $data;
    }

    protected function callHook(string $hook, mixed $input, mixed ...$args): mixed
    {
        $result = $this->hookManager->processHook(
            self::entityHook($hook, $this::class),
            $input,
            ...$args,
        );

        return $this->hookManager->processHook(
            $hook,
            $result,
            ...$args,
        );
    }

    /** @param class-string<Entity> $entityClass */
    public static function entityHook(string $hook, string $entityClass): string
    {
        return sprintf('%s.%s', $hook, $entityClass);
    }

    public function __call($name, $arguments)
    {
        if ($this->isRelationField($name)) {
            return $this->relation($name, ...$arguments);
        }

        if ($this->isVirtualField($name)) {
            return $this->getVirtualField($name, ...$arguments);
        }

        return parent::__call($name, $arguments);
    }

    public function isRelationField(string $field): bool
    {
        return !!$this->relation($field);
    }

    public function isVirtualField(string $field): bool
    {
        return $this->virtualFields()->has($field);
    }

    public function virtualFields(): VirtualFieldDefinitionCollection
    {
        $key = sprintf('virtual_fields:%s', static::class);

        return System::getRegisteredService(Cache::class)->remember(
            $key,
            function (): VirtualFieldDefinitionCollection {

                $fields = VirtualFieldDefinitionCollection::createFromArray(
                    array_map(
                        fn($sql, $field): VirtualFieldDefinition => VirtualFieldDefinition::create(
                            field: $field,
                            table: $this->getTableName(),
                            definition: $sql,
                        ),
                        $this->definedVirtualFields(),
                        array_keys($this->definedVirtualFields())
                    )
                );

                $this->relations()
                    ->filter(fn(RelationshipInterface $relationship): bool => $relationship instanceof OneToMany)
                    ->each(function (RelationshipInterface $relationship) use ($fields): void {
                        if ($relationship instanceof OneToMany) {
                            $fields->push(
                                VirtualFieldDefinition::create(
                                    field: sprintf('%s_count', $relationship->getName()),
                                    table: $this->getTableName(),
                                    definition: [
                                        'sql' => fn(): string => $relationship->countSql(),
                                    ],
                                )
                            );
                        }
                    });

                $keys = $fields->map(fn(VirtualFieldDefinition $field): string => $field->field)->toArray();

                return $fields->replaceAllKeys($keys);
            }
        );
    }

    protected function definedVirtualFields(): array
    {
        return [];
    }

    public function relations(): RelationCollection
    {
        if (!$this->relationDefinitionCache->has($this)) {
            $definedRelations = $this->addAutoRelations($this->definedRelations());

            $relations = RelationCollection::createFromArray($definedRelations);

            $keys = $relations->map(fn(RelationshipInterface $relation): string => $relation->getName())->toArray();

            $relations = $relations->replaceAllKeys($keys);

            $this->relationDefinitionCache->set($this, $relations);
        }

        return $this->relationDefinitionCache->get($this);
    }

    protected function addAutoRelations(array $relations): array
    {
        if ($this instanceof HasCreatorInterface) {
            $relations[] = $this->creatorRelation();
        }

        if ($this instanceof HasModifierInterface) {
            $relations[] = $this->modifierRelation();
        }

        if ($this instanceof HasParentInterface) {
            $relations[] = $this->parentRelation();
        }

        if ($this instanceof HasNotesInterface) {
            $relations[] = $this->latestNoteRelation();
        }

        return $relations;
    }

    protected function definedRelations(): array
    {
        return [];
    }

    protected function getVirtualField($field)
    {
        if (!$this->virtualFieldsCache->has($field)) {
            $this->virtualFieldsCache->set($field, $this->lazyLoadVirtualField($field));
        }

        return $this->virtualFieldsCache->get($field);
    }

    private function lazyLoadVirtualField(string $field): mixed
    {
        $sql = $this->virtualFields()->get($field)->getSql();

        $row = $this->db
            ->select($this->getTableName())
            ->fields(sprintf('(%s) AS result', $sql))
            ->where($this->getPrimaryKey(), $this->getField($this->getPrimaryKey()))
            ->limit(1)
            ->execute()
            ->fetch();

        $this->virtualFieldsCache->set($field, $row?->result);

        return $this->virtualFieldsCache->get($field);
    }

    public function getField($field): mixed
    {
        if ($this->isDataField($field)) {
            return $this->getDataField($field);
        }

        if ($this->isVirtualField($field)) {
            return $this->getVirtualField($field);
        }

        if ($this->isRelationField($field)) {
            $bits = explode('__', $field);
            if (count($bits) > 1) {

                $relationField = array_shift($bits);
                $field = implode('__', $bits);

                $relation = $this->getRelationField($relationField);

                if ($relation === null) {
                    return null;
                }

                if ($relation instanceof Entity) {
                    return $relation->getField($field);
                }

                return $relation->map(fn(Entity $entity): mixed => $entity->getField($field));
            }

            return $this->getRelationField($field);
        }

        return null;
    }

    public function isDataField(string $field): bool
    {
        return array_key_exists($field, $this->getEntityDataFieldNames());
    }

    public function getEntityDataFieldNames(): array
    {
        return array_map(fn(object $field): string => $field->Field, $this->dataFields);
    }

    private function getDataField($field): mixed
    {
        if (array_key_exists($field, $this->data)) {
            return $this->data[$field];
        }

        // @todo Lazy load field

        return null;
    }

    private function getRelationField($field): null|Entity|EntityCollection
    {
        if (!$this->relationCache->has($field)) {
            $this->relationCache->set($field, $this->lazyLoadRelation($field));
        }

        return $this->relationCache->get($field) ?: $this->relation(
            $field
        )->valueWhenEmpty();
    }

    private function lazyLoadRelation($field): EntityCollection|Entity|null
    {
        $relation = $this->relation($field);

        return $relation->lazyLoad($this->getField($this->getPrimaryKey()));
    }

    /**
     * Method to delete attached entity (or provided id)
     * @param null $id
     * @return $this
     * @throws Exception
     * @noinspection PhpHierarchyChecksInspection
     */
    public function delete($id = null): Entity
    {
        if ($id) {
            $this->{$this->primaryKey} = $id;
        }
        $delete = $this->hookManager->processHook('entityPreDelete', $this, $this->getEntityName());
        if ($delete) {
            parent::__call('delete', [$this->{$this->primaryKey}]);
        }
        $this->hookManager->processHook('entityPostDelete', $this, $this->getEntityName());
        //deleting? clear the data
        $this->data = [];

        return $this;
    }

    /**
     * Function to get all results, paged
     * @param null $params
     * @param array $filters
     * @param array $orderBy
     * @return array
     * @throws Exception
     */
    public function loadByPage($params = null, $filters = [], $orderBy = []): array
    {
        $this->setPagerLimit(($params['limit'] ?? false));
        $this->setCurrentPagerOffset(($params['offset'] ?? 0));
        //if we have a page param set, calculate offset on the fly.
        if ($page = $params['page'] ?? false) {
            //if we have a page, assume we need a limit, if its 0 set a default.
            $this->defaultPagerLimit = $this->defaultPagerLimit === 0 ? 25 : $this->defaultPagerLimit;
            $this->currentPagerOffset = ($this->defaultPagerLimit * $page) - $this->defaultPagerLimit;
        }

        $totalResults = $this->totalEntities($filters);
        $totalPages = $this->defaultPagerLimit == 0 ? 1 : ceil(($totalResults / (int) $this->defaultPagerLimit));

        $data = $this->loadMultiple($filters, $orderBy, $this->defaultPagerLimit, $this->currentPagerOffset, false);

        return [
            'data' => $data,
            'totalData' => $totalResults,
            'totalPages' => $totalPages,
            'totalResults' => count($data),
        ];
    }

    public function setPagerLimit($limit)
    {
        $this->defaultPagerLimit = $limit;

        return $this;
    }

    public function setCurrentPagerOffset($offset): Entity
    {
        $this->currentPagerOffset = $offset;

        return $this;
    }

    /**
     * @throws VaultException
     */
    public function totalEntities(array $filters = []): int
    {
        return $this->operator->totalEntities($filters);
    }

    /**
     * Entity load multiple
     * @param array $conditions
     * @param array $orderBy
     * @param int|bool $limit
     * @param int|bool $offset
     * @param bool $asEntities
     * @return array
     * @throws Exception
     */
    public function loadMultiple(
        array $conditions = [],
        array $orderBy = [],
        $limit = null,
        $offset = null,
        bool $asEntities = true
    ): array {
        $loaded = [];
        if (isset($orderBy[1]) && is_string($orderBy[1])) {
            $orderBy[1] = strtolower($orderBy[1]) == 'desc' ? SortDirection::DESC : SortDirection::ASC;
        }
        $results = parent::loadMultiple($conditions, $orderBy, $limit, $offset);
        //make sure data is UTF8 Safe, construct entities if required (default)
        if (!empty($results)) {
            foreach ($results as $result) {
                $data = $this->makeUTF8Safe($result);
                $entityType = get_called_class();
                //set entity data (prevents us from having to reload entities)
                $entity = new $entityType();
                //we setData here even if we don't ultimately want the entities,
                //so we can take advantage of the formatting fixes and allow for any custom setData functionality
                $entity->setData((array) $data);
                //build an array of loaded entities, or just get the data if that's what we want.
                $entity = $this->hookManager->processHook('entityPostLoadMultiple', $entity, $entity->getEntityName());
                //TN do not key by ID as we need to preserve order with a 0 keyed array. This is to allow JS consumers to maintain the order.
                if ($asEntities) {
                    $loaded[] = $entity;
                } else {
                    $loaded[] = $entity->getData();
                }
            }
        }

        return $loaded;
    }

    private function makeUTF8Safe(array|stdClass $data): object
    {
        $object = (object) [];
        foreach ($data as $key => $value) {
            $object->$key = Encoding::fixUTF8($value);
        }

        return $object;
    }

    /**
     * Override entityCore\Entity:getData()
     * Force loaded values through __Get for processing.
     * @return array
     * @todo Caching!
     */
    public function getData($forDisplay = true): array|bool
    {
        $data = parent::getData();
        if ($forDisplay) {
            $usableData = [];
            //force the loaded data to be passed through __get
            foreach ($data as $field => $value) {
                $value = $this->hrData($field, $value);
                if ($value !== Utils::HIDE_FROM_DISPLAY) {
                    $usableData[$field] = $value;
                }
            }

            return $usableData;
        }

        return $data;
    }

    public function hrData($fieldName, $data)
    {
        if (isset($this->fieldConfig[$fieldName]) && isset($this->fieldConfig[$fieldName]['display'])) {
            return $this->fieldConfig[$fieldName]['display']($data, $this);
        }

        return $data;
    }

    /**
     * Add metadata to the object, this will not be stored in the database unless manually done
     * @param $name
     * @param $data
     * @return $this
     */
    public function addMetadata($name, $data): Entity
    {
        $this->data["_metadata"]->$name = $data;

        return $this;
    }

    /**
     * Get data from metadata store, if it exists. Null otherwise
     * @param $name
     * @return mixed
     */
    public function getMetadata($name)
    {
        return $this->data["_metadata"]->$name ?? null;
    }

    /**
     * Function to flag certain data fields as read only (data changed in these fields will not be stored against the original loaded entity)
     * @param string $field
     * @return Entity
     */
    public function addReadOnlyField(string $field): Entity
    {
        $this->readOnlyFields[] = $field;

        return $this;
    }

    public function getReadOnlyFields(): array
    {
        return $this->readOnlyFields;
    }

    /**
     * Function to flag certain data fields as read only (data changed in these fields will not be stored against the original loaded entity)
     * @param array $fields
     */
    public function setReadOnlyFields(array $fields)
    {
        $this->readOnlyFields = $fields;
    }

    public function hrName($fieldName)
    {
        if (isset($this->fieldConfig[$fieldName]) && isset($this->fieldConfig[$fieldName]['name'])) {
            return $this->fieldConfig[$fieldName]['name'];
        }

        return $fieldName;
    }

    public function virtualField(string $field): ?VirtualFieldDefinition
    {
        return $this->virtualFields()->get($field);
    }

    public function jsonSerialize(): mixed
    {
        $data = [];
        foreach (array_keys($this->data) as $key) {
            $value = $this->valueForSerialization($this->__get($key));
            $data[$key] = $value;
        }

        return [
            ...$data,
            ...$this->virtualFieldsCache->toArray(),
            'relations' => $this->relationCache->toArray(),
        ];
    }

    protected function valueForSerialization(mixed $value): mixed
    {
        if ($value instanceof DateTimeInterface) {
            return $value->format('c');
        }

        if ($value instanceof BackedEnum) {
            return $value->value;
        }

        if ($value instanceof UnitEnum) {
            return $value->name;
        }

        if ($value instanceof Entity) {
            return $value->{$value->primaryKey};
        }

        return $value;
    }

    public function __get($name)
    {
        return $this->getCasted($name);
    }

    public function __set($name, $value): void
    {
        $this->setCasted($name, $value);
    }

    /** @param class-string<Entity> $remoteEntity */
    protected function hasOne(
        string $name,
        string $remoteEntity,
        null|array|string $localKey = null,
        null|array|string $remoteKey = null,
        ?string $aliasedRemoteTable = null,
        ?RelationshipInterface $parent = null,
        ?Closure $queryCallback = null,
        ?Closure $rawJoin = null,
        ?Closure $rawQuery = null,
        null|array|RelationCollection|RelationshipInterface $forcedRelation = null,
    ): HasOne {
        return HasOne::create(
            $name,
            $this,
            $remoteEntity,
            $localKey,
            $remoteKey,
            $aliasedRemoteTable,
            $parent,
            $queryCallback,
            $rawJoin,
            $rawQuery,
            $forcedRelation
        );
    }

    /**
     * @param class-string<Entity> $remoteEntity
     */
    protected function belongsTo(
        string $name,
        string $remoteEntity,
        null|array|string $localKey = null,
        null|array|string $remoteKey = null,
        ?RelationshipInterface $parent = null,
        ?Closure $queryCallback = null,
        ?Closure $rawJoin = null,
        ?Closure $rawQuery = null,
        ?string $aliasedRemoteTable = null,
    ): BelongsTo {
        return BelongsTo::create(
            $name,
            $this,
            $remoteEntity,
            $localKey,
            $remoteKey,
            $parent,
            $queryCallback,
            $rawJoin,
            $rawQuery,
            $aliasedRemoteTable,
        );
    }

    /**
     * @param class-string<Entity> $remoteEntity
     */
    protected function hasOneViaPivot(
        string $name,
        string $remoteEntity,
        null|string|array $localKey = null,
        null|string|array $remoteKey = null,
        ?string $aliasedRemoteTable = null,
        ?Closure $queryCallback = null,
        ?Closure $rawJoin = null,
        ?Closure $rawPivotJoin = null,
        ?Closure $rawQuery = null,
        null|array|RelationCollection|RelationshipInterface $parent = null,
        bool $noFields = false,
        ?string $pivotTable = null,
        ?string $pivotLocal = null,
        ?string $pivotRemote = null,
        array $pivotFields = [],
        bool $hasWeight = true,
        ?Closure $weightCallback = null,
    ): HasOneViaPivot {
        return HasOneViaPivot::create(
            name: $name,
            localEntity: $this,
            remoteEntity: $remoteEntity,
            localKey: $localKey,
            remoteKey: $remoteKey,
            aliasedRemoteTable: $aliasedRemoteTable,
            queryCallback: $queryCallback,
            rawJoin: $rawJoin,
            rawPivotJoin: $rawPivotJoin,
            rawQuery: $rawQuery,
            parent: $parent,
            noFields: $noFields,
            pivotTable: $pivotTable,
            pivotLocal: $pivotLocal,
            pivotRemote: $pivotRemote,
            pivotFields: $pivotFields,
            hasWeight: $hasWeight,
            weightCallback: $weightCallback,
        );
    }

    /**
     * @param class-string<Entity> $remoteEntity
     */
    protected function hasMany(
        string $name,
        string $remoteEntity,
        null|string|array $localKey = null,
        null|string|array $remoteKey = null,
        ?string $aliasedRemoteTable = null,
        ?Closure $queryCallback = null,
        ?Closure $rawJoin = null,
        ?Closure $rawPivotJoin = null,
        ?Closure $rawQuery = null,
        null|array|RelationCollection|RelationshipInterface $parent = null,
        bool $noFields = false,
        ?string $pivotTable = null,
        ?string $pivotLocal = null,
        ?string $pivotRemote = null,
        array $pivotFields = [],
        bool $hasWeight = true,
        ?Closure $weightCallback = null,
    ): HasMany {
        return HasMany::create(
            name: $name,
            localEntity: $this,
            remoteEntity: $remoteEntity,
            localKey: $localKey,
            remoteKey: $remoteKey,
            aliasedRemoteTable: $aliasedRemoteTable,
            queryCallback: $queryCallback,
            rawJoin: $rawJoin,
            rawPivotJoin: $rawPivotJoin,
            rawQuery: $rawQuery,
            parent: $parent,
            noFields: $noFields,
            pivotTable: $pivotTable,
            pivotLocal: $pivotLocal,
            pivotRemote: $pivotRemote,
            pivotFields: $pivotFields,
            hasWeight: $hasWeight,
            weightCallback: $weightCallback,

        );
    }

    /**
     * @param class-string<Entity> $remoteEntity
     */
    protected function belongsToMany(
        string $name,
        string $remoteEntity,
        null|string|array $localKey = null,
        null|string|array $remoteKey = null,
        ?Closure $queryCallback = null,
        ?Closure $rawJoin = null,
        ?Closure $rawQuery = null,
        ?RelationshipInterface $parent = null,
        ?string $aliasedRemoteTable = null,

    ): BelongsToMany {
        return BelongsToMany::create(
            $name,
            $this,
            $remoteEntity,
            $localKey,
            $remoteKey,
            $queryCallback,
            $rawJoin,
            $rawQuery,
            $parent,
            $aliasedRemoteTable,
        );
    }

}
