<?php

namespace App\Modules\Shared\Domain\Aggregate;

use App\Modules\Shared\Domain\ArrayUtils;
use App\Modules\Shared\Domain\Bus\Event\BusEvent;

abstract class AggregateRoot
{

    private array $updatedAttributes;

    private static array $allAttributes = [];
    private array $domainEvents = [];

    /**
     * @param AggregateAttribute[] $attributes
     */
    public function __construct(protected array $attributes)
    {
        $this->updatedAttributes = [];
    }

    final public function pullDomainEvents(): array
    {
        $domainEvents       = $this->domainEvents;
        $this->domainEvents = [];

        return $domainEvents;
    }

    public function getVersion(): int
    {
        return 1;
    }

    final protected function record(BusEvent $domainEvent): void
    {
        $this->domainEvents[] = $domainEvent;
    }

    protected function updateAttribute($attributeName)
    {
        $found = $this->haveAttribute($attributeName);
        if (!count($found)) {
            throw new AggregateAttributeNotExists($attributeName);
        }

        $this->updatedAttributes[$attributeName] = $found[0];
    }

    protected static function haveAttribute($attributeName)
    {
        $attributes = static::allAttributes();
        return array_values(array_filter($attributes, fn(AggregateAttribute $attr) => $attr->attribute() === $attributeName));
    }

    public static function table(): string
    {
        return '';
    }

    public function getTable(): string
    {
        return static::table();
    }

    /** @return AggregateAttribute[] */
    protected static function allAttributes(): array
    {
        if (!isset(static::$allAttributes[static::class])) {
            static::$allAttributes[static::class] = static::attributes();
        }
        return static::$allAttributes[static::class];
    }

    /** @return AggregateAttribute[] */
    protected abstract static function attributes(): array;

    public static function primary(): AggregateAttribute | null
    {
        $attributes = static::allAttributes();
        $primary = array_values(array_filter($attributes, fn(AggregateAttribute $attr) => $attr->attributeType() === 'primary'));
        return count($primary) ? $primary[0] : null;
    }

    public static function columns(): array
    {
        $attributes = static::allAttributes();
        return array_values(array_filter($attributes, fn(AggregateAttribute $attr) => $attr->attributeType() === 'column'));
    }

    /**
     * Generates a SELECT statement for the given table.
     *
     * @param string $table The table to select from.
     */
    public static function select($table, ?array $avoid = null)
    {
        $avoid = $avoid ?? [];
        $attributes = static::allAttributes();
        $avoided = array_values(ArrayUtils::filter($attributes, fn(AggregateAttribute $attr) => !in_array($attr->column(), $avoid)));
        return implode(', ', array_map(fn(AggregateAttribute $attr) => $table . '.`' . $attr->column() . '`', $avoided));
    }

    /**
     * Generates a JSON OBJECT statement for the given table.
     *
     * @param string $table The table to select from.
     */
    public static function json($table)
    {
        return implode(
            ', ',
            array_map(fn(AggregateAttribute $attr) =>
            "'" . $attr->column() . "', " . $table . '.`' . $attr->column() . '`', static::allAttributes())
        );
    }

    /**
     * Generates a SELECT statement for the given table.
     *
     * @param string $table The table to select from.
     */
    public static function selectUnique($table, ?array $attributes = null)
    {
        return implode(
            ', ',
            array_map(
                fn(AggregateAttribute $attr) => $table
                    . '.`' . $attr->column() . '` as `' . $attr->uniqueId() . '`',
                $attributes ?? static::allAttributes()
            )
        );
    }

    /**
     * Generates an INSERT statement from the aggregate attributes
     */
    public function insert()
    {
        $table = static::table();
        $columns = static::columns();
        return [
            'INSERT INTO ' . "`$table`" . ' (' . $this->selectColumns($columns) . ') VALUES (' . $this->valueQuestions($columns) . ')',
            $this->values($columns)
        ];
    }

    private function selectColumns(&$attributes)
    {
        return implode(', ', array_map(fn(AggregateAttribute $attr) => "`" . $attr->column() . "`", $attributes));
    }

    private function valueQuestions(&$attributes)
    {
        return implode(', ', array_map(fn() => '?', $attributes));
    }

    private function values(&$attributes)
    {
        return array_map(fn(AggregateAttribute $attr) => ($this->{$attr->attribute()})->value(), $attributes);
    }

    /**
     * Generates an INSERT statement from the aggregate attributes
     */
    public function update()
    {
        $table = static::table();
        $columns = static::columns();
        $primary = static::primary();
        return [
            'UPDATE ' . "`$table`" . ' SET ' . $this->updateColumns($columns) . ' WHERE ' . $primary->column() . ' = ?',
            $this->updateValues($columns, $primary)
        ];
    }

    /**
     * Generates an UPDATE statement from the aggregate attributes only attributes that are in $updatedAttributes
     */
    public function updatePartial()
    {
        $table = static::table();
        $columns = array_values($this->updatedAttributes);
        $primary = static::primary();
        return [
            'UPDATE ' . "`$table`"
                . ' SET ' . $this->updateColumns($columns) . ' WHERE ' . $primary->column() . ' = ?',
            $this->updateValues($columns, $primary)
        ];
    }

    public static function updateColumns($attributes)
    {
        return implode(', ', array_map(fn(AggregateAttribute $attr) => "`" . $attr->column() . "`=?", $attributes));
    }

    /**
     * @param AggregateAttribute[] $attributes
     * @param AggregateAttribute $primary
     * @return mixed[]
     */
    public function updateValues(array $attributes, AggregateAttribute $primary)
    {
        return array_merge(
            array_map(fn(AggregateAttribute $attr) => ($this->{$attr->attribute()})->value(), $attributes),
            [$this->{$primary->attribute()}->value()]
        );
    }

    public static function fromDTO($dto): static
    {
        $data = array_map(function (AggregateAttribute $attr) use ($dto) {
            $objName = $attr->objectName();
            if ($dto->{$attr->column()} === null) {
                return null;
            }
            return (new $objName($dto->{$attr->column()}));
        }, static::allAttributes());
        return new static(...$data);
    }

    public function toDTO(
        ?string $dtoClass = null,
        ?array $avoid = null,
        ?array $add = null
    ) {
        $data = $this->generateMap($avoid);
        if ($add) {
            $data = ArrayUtils::merge($data, $add);
        }
        if ($dtoClass) {
            return new $dtoClass(...$data);
        } else {
            return (object)$data;
        }
    }

    private function generateMap(?array $avoid = null)
    {
        return array_reduce(static::allAttributes(), function ($carry, AggregateAttribute $attr) use ($avoid) {
            if ($avoid && in_array($attr->column(), $avoid)) {
                return $carry;
            }

            $carry[$attr->column()] = $this->{$attr->attribute()}->value();
            return $carry;
        }, []);
    }

    public static function fromDTOUnique($dto, ?array $attributes = null): static
    {

        $data = array_map(function (AggregateAttribute $attr) use ($dto) {
            $objName = $attr->objectName();
            if ($dto->{$attr->uniqueId()} === null) {
                return null;
            }
            return (new $objName($dto->{$attr->uniqueId()}));
        }, $attributes ?? static::allAttributes());
        return new static(...$data);
    }

    public function toArray(): array
    {
        $attributes = static::allAttributes();
        return array_reduce(
            $attributes,
            function (array $carry, AggregateAttribute $attr) {
                $carry[$attr->column()] = $this->{$attr->attribute()}->value();
                return $carry;
            },
            []
        );
    }
}
