<?php

declare(strict_types=1);

namespace apexl\Io\Traits;

use apexl\dto\Raw;
use apexl\Io\includes\Entity;
use apexl\Io\interfaces\RelationshipInterface;
use apexl\Io\interfaces\RelationshipViaPivotInterface;
use apexl\Vault\Vault;
use Closure;
use function apexl\Io\container;

trait HasPivotTable
{
    public function clear(?int $entityId = null): void
    {
        $pivotTable = $this->getPivotTable();
        $localEntity = $this->getLocalEntity();
        $localEntityKey = $localEntity->{$localEntity->getPrimaryKey()};
        $pivotLocal = $this->getPivotLocal();

        $query = $this->db()
            ->delete($pivotTable)
            ->where(
                sprintf('%s.%s', $pivotTable, $pivotLocal),
                $localEntityKey,
            );

        foreach ($this->getPivotFields() as $field => $value) {
            $query->where(sprintf('%s.%s', $pivotTable, $field), $value);
        }

        if (!is_null($entityId)) {
            $query->where(sprintf('%s.%s', $pivotTable, $this->pivotRemote), $entityId);
        }

        $query->execute();

        if (is_null($entityId)) {
            $this->localEntity->{$this->getName()} = $this->valueWhenEmpty();
        }
    }

    abstract public function getPivotTable(): string;

    abstract public function getLocalEntity(): Entity;

    abstract public function getPivotLocal(): string;

    abstract protected function db(): Vault;

    abstract public function getPivotFields(): array;

    abstract public function getName(): string;

    abstract public function valueWhenEmpty(): mixed;

    protected function defaultWeightCallback(): Closure
    {
        return function (RelationshipViaPivotInterface $relation): string {
            $query = $this->db->newQuery()->select($relation->getPivotTable())->fields(
                ['1 + IFNULL(MAX(weight),0)']
            )
                ->whereRaw(
                    sprintf(
                        '%s.%s = \'%s\'',
                        $relation->getPivotTable(),
                        $relation->getPivotLocal(),
                        $relation->localEntity->{$relation->localEntity->getPrimaryKey()}
                    )
                );

            foreach ($relation->getPivotFields() as $field => $value) {
                $query->whereRaw(sprintf('%s.%s = \'%s\'', $relation->getPivotTable(), $field, $value));
            }

            return sprintf(
                '(SELECT * FROM (%s) weight)',
                $query->query->getStatement(),
            );
        };
    }

    private function resolvePivotJoin(): Raw
    {
        if ($this->getRawPivotJoin()) {
            return $this->resolveRawPivotJoin();
        }

        $join = [];
        $join[] = sprintf(
            '%1$s.%2$s = %3$s.%4$s',
            $this->getAliasedRemoteTable(),
            $this->getRemoteKey(),
            $this->getPivotTableAlias(),
            $this->getPivotRemote(),
        );

        foreach ($this->getPivotFields() as $field => $value) {
            if (!$value instanceof Raw) {
                $value = Raw::create(sprintf('\'%s\'', addslashes((string) $value)));
            }

            $join[] = sprintf(
                '%s.%s = %s',
                $this->getPivotTableAlias(),
                $field,
                $value->value,
            );
        }

        return Raw::create(implode(' AND ', $join));
    }

    abstract protected function getRawPivotJoin(): ?Closure;

    protected function resolveRawPivotJoin(): Raw
    {
        return Raw::create(
            ($this->getRawPivotJoin())($this)
        );
    }

    /** @var class-string<Entity> $remoteEntity */
    public static function create(
        string $name,
        Entity $localEntity,
        string $remoteEntity,
        ?string $localKey = null,
        ?string $remoteKey = null,
        ?string $aliasedRemoteTable = null,
        ?Closure $queryCallback = null,
        ?Closure $rawJoin = null,
        ?Closure $rawPivotJoin = null,
        ?Closure $rawQuery = null,
        ?RelationshipInterface $parent = null,
        bool $noFields = false,
        ?string $pivotTable = null,
        ?string $pivotLocal = null,
        ?string $pivotRemote = null,
        array $pivotFields = [],
        bool $hasWeight = true,
        ?Closure $weightCallback = null,
        ?Closure $orderByCallback = null,
    ): self {
        $pivotTable = $pivotTable ?? self::pivotTable($localEntity::class, $remoteEntity);

        return container()->make(static::class, [
            'pivotTable' => $pivotTable,
            'pivotTableAlias' => $pivotTableAlias ?? self::pivotTableAlias($pivotTable, $name),
            'pivotLocal' => $pivotLocal ?? self::pivotLocal($localEntity::class),
            'pivotRemote' => $pivotRemote ?? self::pivotRemote($remoteEntity),
            'name' => $name,
            'localEntity' => $localEntity,
            'remoteEntity' => $remoteEntity,
            'localTable' => $localEntity->getTableName(),
            'localKey' => $localKey ?? $localEntity->getPrimaryKey(),
            'remoteKey' => $remoteKey,
            'pivotFields' => $pivotFields,
            'hasWeight' => $hasWeight,
            'parent' => $parent,
            'aliasedRemoteTable' => $aliasedRemoteTable,
            'queryCallback' => $queryCallback,
            'rawJoin' => $rawJoin,
            'rawPivotJoin' => $rawPivotJoin,
            'rawQuery' => $rawQuery,
            'noFields' => $noFields,
            'weightCallback' => $weightCallback,
            'orderByCallback' => $orderByCallback,
        ]);
    }

    abstract public function getRemoteKey(): string;

    abstract public function getPivotRemote(): string;
}
