<?php

declare(strict_types=1);

namespace apexl\Io\dto\Relationship;

use apexl\dto\Dto;
use apexl\Io\includes\Entity;
use apexl\Io\interfaces\RelationshipInterface;
use apexl\Vault\interfaces\driver;
use Arrayy\Type\StringCollection;
use Closure;
use ValueError;

abstract readonly class AbstractRelationship extends Dto implements RelationshipInterface
{
    /**
     * @var Closure(RelationshipInterface $relation, driver $query): driver
     */
    protected Closure $queryCallback;
    protected string $aliasedRemoteTable;

    /** @param class-string<Entity> $remoteEntity */
    protected function __construct(
        public string $name,
        public Entity $localEntity,
        public string $remoteEntity,
        public string $localTable,
        public string $localKey,
        public ?string $remoteKey,
        protected ?RelationshipInterface $parent = null,
        ?string $aliasedRemoteTable = null,
        ?Closure $queryCallback = null,
        protected ?Closure $rawJoin = null,
        protected ?Closure $rawQuery = null,
        public bool $noFields = false,
    ) {
        $this->queryCallback = $queryCallback ?? fn(RelationshipInterface $relationship, driver $query) => $query;
        $this->aliasedRemoteTable = $aliasedRemoteTable ?? $this->aliasedRemoteTable();
    }

    protected function aliasedRemoteTable(): string
    {
        return sprintf('%s__%s', $this->name, $this->getRemoteTable());
    }

    public function getRemoteTable(): string
    {
        return $this->getRemoteEntity()->getTableName();
    }

    public function getRemoteEntity(): Entity
    {
        return new ($this->remoteEntity)();
    }

    abstract public function fieldsSelectString(StringCollection $fields, StringCollection $remaining): string;

    public function getLocalEntity(): Entity
    {
        return $this->localEntity;
    }

    public function isDescendedFromOneToMany(): bool
    {
        if ($this->parent === null) {
            return false;
        }

        if ($this->parent instanceof OneToMany) {
            return true;
        }

        return $this->parent->isDescendedFromOneToMany();
    }

    public function getLocalKey(): string
    {
        return $this->localKey;
    }

    public function freshEntity(): Entity
    {
        return $this->getRemoteEntity();
    }

    public function getAliasedRemoteTable(): string
    {
        if ($this->parent) {
            return sprintf('%s__%s', $this->parent->getAliasedRemoteTable(), $this->aliasedRemoteTable);
        }

        return $this->aliasedRemoteTable;
    }

    public function getLocalTable(): string
    {
        return $this->localTable;
    }

    protected function getRemoteKey(): string
    {
        return $this->remoteKey ?? $this->getRemoteEntity()->getPrimaryKey();
    }

    protected function prefix(): string
    {
        $prefix = $this->name;

        if ($this->parent) {
            $prefix = sprintf('%s__%s', $this->parent->prefix(), $prefix);
        }

        return $prefix;
    }

    protected function ensureEntity(string|int|Entity $entity): Entity
    {
        if ($entity instanceof Entity) {
            return $entity;
        }

        return $this->remoteEntity::find($entity);
    }

    protected function ensureEntityId(string|int|Entity $entity): int|string
    {
        if ($entity instanceof Entity) {
            return $entity->{$entity->getPrimaryKey()};
        }

        return $entity;
    }

    protected function assertEntityType(Entity $entity): void
    {
        if (!$entity instanceof $this->remoteEntity) {
            /** @noinspection PhpMultipleClassDeclarationsInspection */
            throw new ValueError(
                sprintf('%s only accepts entities of type %s', $this->getName(), $this->remoteEntity)
            );
        }
    }

    public function getName(): string
    {
        if ($this->parent) {
            return sprintf('%s__%s', $this->parent->getName(), $this->name);
        }

        return $this->name;
    }
}
