<?php /** @noinspection PhpMultipleClassDeclarationsInspection */

declare(strict_types=1);

namespace apexl\Io\dto\Relationship;

use apexl\entityCore\collections\EntityCollection;
use apexl\Io\collections\LoaderConditionCollection;
use apexl\Io\dto\LoaderCondition;
use apexl\Io\enums\EntityFinderJsonType;
use apexl\Io\includes\Entity;
use apexl\Io\interfaces\RelationshipInterface;
use apexl\Vault\enums\WhereCondition;
use apexl\Vault\enums\WhereType;
use Arrayy\Type\StringCollection;
use Closure;
use TypeError;

abstract readonly class OneToOne extends AbstractRelationship implements RelationshipInterface
{
    /** @var class-string<Entity> $remoteEntity */
    protected function __construct(
        string $name,
        Entity $localEntity,
        string $remoteEntity,
        string $localTable,
        string $localKey,
        ?string $remoteKey,
        ?RelationshipInterface $parent = null,
        ?string $aliasedRemoteTable = null,
        ?Closure $queryCallback = null,
        ?Closure $rawJoin = null,
        ?Closure $rawQuery = null,
        bool $noFields = false,
    ) {
        parent::__construct(
            $name,
            $localEntity,
            $remoteEntity,
            $localTable,
            $localKey,
            $remoteKey,
            $parent,
            $aliasedRemoteTable,
            $queryCallback,
            $rawJoin,
            $rawQuery,
            $noFields,
        );
    }

    public function lazyLoad(int|string $localKey): ?Entity
    {
        return null;
    }

    public function hydrate(array $data): Entity
    {
        return $this->freshEntity()->hydrate($data);
    }

    public function add(int|Entity ...$entity): void
    {
        $_entity = $entity[0];

        $this->set($_entity);
    }

    public function remove(int|Entity $entity): void
    {
        $this->clear();
    }

    public function valueWhenEmpty(): null
    {
        return null;
    }

    public function fieldsSelectString(StringCollection $fields, StringCollection $remaining): string
    {
        $fields = $fields->map(fn(string $field): string => sprintf(
            "'%s', %s.%s",
            $field,
            $this->getAliasedRemoteTable(),
            $field
        ));

        $fields = $fields->merge($remaining->map(fn(string $sql, string $field): string => sprintf(
            "'%s', %s",
            $field,
            $sql,
        ), true));

        $fields->append(
            sprintf(
                "'_key', %s.%s",
                $this->getAliasedRemoteTable(),
                $this->getRemoteEntity()->getPrimaryKey(),
            ),
        );


        if ($this->parent) {
            $fields->append(
                sprintf(
                    "'_parent', %s.%s",
                    $this->parent->getAliasedRemoteTable(),
                    $this->parent->getRemoteEntity()->getPrimaryKey(),
                ),
            );
        }

        $sql = sprintf(
        /** @lang TEXT */
            <<<'SQL'
            SELECT JSON_OBJECT(%1$s)
            FROM %2$s as %3$s
            %4$s
            WHERE %5$s
            LIMIT 1
            SQL,
            $fields->implode(', '),
            $this->getRemoteTable(),
            $this->getAliasedRemoteTable(),

            $this->hasParent() ? $this->parent->join($this) : '',
            $this->where(),
        );


        return sprintf('(%s) AS _%s', $sql, $this->prefix());
    }

    public function hasParent(): bool
    {
        return $this->parent !== null;
    }

    public function join(RelationshipInterface $child, string $carry = ''): string
    {
        $carry = sprintf(
        /** @lang TEXT */
            <<<'SQL'
                    JOIN %1$s as %2$s
                       ON `%2$s`.`%3$s` = `%4$s`.`%5$s`
                %6$s
                SQL,
            $this->getRemoteTable(),
            $this->getAliasedRemoteTable(),
            $child->getLocalKey(),
            $child->getAliasedRemoteTable(),
            $child->getRemoteKey(),
            $carry,
        );

        return $this->hasParent() ? $this->parent->join($this, $carry) : $carry;
    }

    public function where(): string
    {
        if ($this->hasParent()) {
            return $this->parent->where();
        }

        return sprintf(
            '%s.%s = %s.%s',
            $this->getAliasedRemoteTable(),
            $this->getRemoteKey(),
            $this->getLocalTable(),
            $this->localKey,
        );
    }

    public function loaderConditionForField(
        string $field,
        WhereCondition $condition,
        array|bool|float|int|null|string $value,
        WhereType $type = WhereType::AND,
    ): LoaderCondition {
        return new LoaderCondition(
            $this->subSelectStringSingleField($field),
            $condition,
            $value,
            $type,
        );
    }

    public function subSelectStringSingleField(
        string $field,
        ?string $table = null,
    ): string {
        return sprintf(
            'JSON_VALUE((%s), \'$.%s\')',
            $this->subSelectString([$field], $table),
            str_replace('__', '.', $field)
        );

    }

    public function subSelectString(
        array $fields,
        ?string $table = null,
    ): string {
        $query = $this->getRemoteEntity()->findMultipleQuery(
            conditions: $this->conditions($table),
            limit: 1,
            fields: $fields,
            json: EntityFinderJsonType::ONE,
            tableAlias: $this->getAliasedRemoteTable(),
        );

        return ($this->queryCallback)($this, $query)->query->getStatement();
    }

    protected function conditions(?string $table = null): LoaderConditionCollection
    {
        return LoaderConditionCollection::createFromArray([
            LoaderCondition::ColumnsEqual(
                sprintf('%s.%s', $this->getAliasedRemoteTable(), $this->getRemoteKey()),
                sprintf('%s.%s', $table ?? $this->getLocalTable(), $this->localKey),
            ),
        ]);
    }

    protected function assertSetType(array|EntityCollection|Entity|int $entity): void
    {
        if (is_array($entity) || $entity instanceof EntityCollection) {
            throw new TypeError('Attempt to set multiple entities on a OneToOne relationship');
        }
    }
}
