<?php

declare(strict_types=1);

namespace Apexl\Io\Traits;

use Apexl\Dto\Raw;
use Apexl\Io\Dtos\Relationship\Decorators\DefaultPivotWeight;
use Apexl\Io\Entity;
use Apexl\Io\Interfaces\PivotSqlFragment;
use Apexl\Io\Interfaces\QueryDecorator;
use Apexl\Io\Interfaces\RelationOrderBy;
use Apexl\Io\Interfaces\RelationshipInterface;
use Apexl\Io\Interfaces\RelationSqlFragment;
use Apexl\Vault\Exceptions\ExecutionException;
use Apexl\Vault\Vault;

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();
        }
    }

    /**
     * @throws ExecutionException
     */
    public function updatePivot(int|string|Entity $itemId, array $attributes): void
    {
        $itemId = $this->ensureEntityId($itemId);

        $pivotTable = $this->getPivotTable();
        $localEntity = $this->getLocalEntity();
        $localEntityKey = $localEntity->{$localEntity->getPrimaryKey()};

        $query = $this->db()
            ->update($pivotTable)
            ->fields($attributes)
            ->where(sprintf('%s.%s', $pivotTable, $this->getPivotLocal()), $localEntityKey)
            ->where(sprintf('%s.%s', $pivotTable, $this->getPivotRemote()), $itemId);

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

        $query->execute();
    }

    abstract protected function ensureEntityId(string|int|Entity $entity): int|string;

    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(): PivotSqlFragment
    {
        return new DefaultPivotWeight($this->db());
    }

    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(): ?PivotSqlFragment;

    protected function resolveRawPivotJoin(): Raw
    {
        return Raw::create(
            $this->getRawPivotJoin()->render($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,
        ?QueryDecorator $queryCallback = null,
        ?RelationSqlFragment $rawJoin = null,
        ?PivotSqlFragment $rawPivotJoin = null,
        ?QueryDecorator $rawQuery = null,
        ?RelationshipInterface $parent = null,
        bool $noFields = false,
        ?string $pivotTable = null,
        ?string $pivotLocal = null,
        ?string $pivotRemote = null,
        array $pivotFields = [],
        bool $hasWeight = true,
        ?PivotSqlFragment $weightCallback = null,
        ?RelationOrderBy $orderByCallback = null,
    ): self {
        $pivotTable = $pivotTable ?? self::pivotTable($localEntity::class, $remoteEntity);

        return \Apexl\Io\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;
}
