<?php

namespace App\Modules\Shared\Domain\Aggregate;

abstract class AggregateRoot
{

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

    protected function updateAttribute($attributeName, $attribute)
    {
        if (!$this->haveAttribute($attributeName)) {
            throw new AggregateAttributeNotExists($attributeName);
        }

        $this->updatedAttributes[$attributeName] = $this->{$attributeName};
    }

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

    /** @return AgregateAttribute[] */
    protected static function table(): string
    {
        return '';
    }

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

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

    protected static function columns(): array
    {
        $attributes = static::attributes();
        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)
    {
        return implode(', ', array_map(fn (AggregateAttribute $attr) => $table . '.`' . $attr->column() . '`', static::attributes()));
    }

    /**
     * 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 = static::columns();
        $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
    {
        if ($dto === null) {
            return null;
        }
        $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::attributes());
        return new static(...$data);
    }
}
