<?php

declare(strict_types=1);

namespace apexl\Io\modules\tabulator\Tabulatable;

use Apexl\Io\Cache\Interfaces\CacheStreamInterface;
use apexl\Io\includes\RouteManager;
use apexl\Io\modules\tabulator\Collections\TabulatorColumnDtoCollection;
use apexl\Io\modules\tabulator\Collections\TabulatorFilterCollection;
use apexl\Io\modules\tabulator\Collections\TabulatorSortCollection;
use apexl\Io\modules\tabulator\Dto\TabulatorColumnDto;
use apexl\Io\modules\tabulator\Dto\TabulatorColumnGroup;
use apexl\Io\modules\tabulator\Dto\TabulatorFilter;
use apexl\Io\modules\tabulator\Dto\TabulatorSort;
use apexl\Io\modules\tabulator\Interfaces\HasTabulatorViewInterface;
use apexl\Io\modules\tabulator\Interfaces\HasTreeStructureInterface;
use apexl\Io\modules\tabulator\Services\TabulatorFieldRenamer;
use apexl\Vault\enums\SortDirection;
use Arrayy\Type\StringCollection;
use DI\Attribute\Inject;
use DI\Container;
use Wizaplace\Etl\Row;
use Wizaplace\Etl\Transformers\Transformer;

abstract class Tabulatable extends Transformer
{
    protected array $columnGroups = [];
    protected StringCollection $fields;
    private TabulatorColumnDtoCollection $columns;
    private TabulatorColumnDtoCollection $displayColumns;
    /** @var array<string, StringCollection> */
    private array $fieldsCache = [];

    public function __construct(
        private readonly Container $container,
        #[Inject('cache.default')]
        private readonly CacheStreamInterface $cache,
        private readonly TabulatorFieldRenamer $fieldRenamer,
        protected readonly RouteManager $routeManager,
        protected HasTabulatorViewInterface $entity,
        StringCollection|array $fields = [],
        protected bool $excludeAjax = true,
    ) {
        if (!$fields instanceof StringCollection) {
            $fields = StringCollection::createFromArray($fields);
        }

        if (is_subclass_of($entity, HasTreeStructureInterface::class)) {
            $fields->add('parent_id');
        }

        $this->fields = $fields;
    }

    public function sort(): TabulatorSortCollection
    {
        return new TabulatorSortCollection([
            new TabulatorSort(
                field: str_replace('.', '__', $this->sortField()),
                direction: $this->sortDirection(),
            ),
        ]);
    }

    public function sortField(): string
    {
        return 'created';
    }

    public function sortDirection(): SortDirection
    {
        return SortDirection::DESC;
    }

    public function transform(Row $row): void
    {
        $this->columns()->each(
            function (TabulatorColumnDto $column) use ($row): void {
                if (!$this->fields->isEmpty() && !$this->fields->contains($column->field)) {
                    return;
                }

                if ($this->excludeAjax && $column->ajax !== null) {
                    $row->set($column->field, null);
                    return;
                }
                $value = $this->container->call($column->callback, [
                    'entity' => $this->entity,
                    'field' => $column->field
                ]);
                if ($value === null && $column->default !== null) {
                    $value = $column->default;
                }

                $row->set($column->field, $value);
            }
        );

        $allowedFields = $this->fields($this->excludeAjax);
        foreach ($row->toArray() as $key => $value) {
            if (!$allowedFields->contains($key)) {
                $row->remove($key);
            }
        }
    }

    public function columns(): TabulatorColumnDtoCollection
    {
        if (!isset($this->columns)) {
            $key = sprintf('/tabulatable/columns/%s', static::class);

            $this->columns = $this->cache->remember(
                $key,
                fn() => $this->_columns()
            );
        }

        return $this->columns;
    }

    abstract protected function _columns(): TabulatorColumnDtoCollection;

    public function fields(bool $excludeAjax = true, bool $includeDataFields = true, array $wanted = []): StringCollection
    {
        $cacheKey = ($excludeAjax ? '1' : '0') . ($includeDataFields ? '1' : '0');
        if (isset($this->fieldsCache[$cacheKey])) {
            return $this->fieldsCache[$cacheKey];
        }

        $fields = StringCollection::createFromObject(
            $this->displayColumns()
                ->values()
                ->filter(fn(TabulatorColumnDto $column) => empty($wanted) || in_array($column->field, $wanted))
                ->reduce(
                    function (StringCollection $carry, TabulatorColumnDto $column) use ($excludeAjax, $includeDataFields): StringCollection {
                        if ($excludeAjax && $column->ajax) {
                            return $carry;
                        }

                        foreach ($column->fields as $field) {
                            $carry->add($field);
                        }

                        if ($includeDataFields) {
                            foreach ($column->dataFields as $field) {
                                $carry->add($field);
                            }
                        }

                        return $carry;
                    },
                    new StringCollection()
                )
        );
        if ($this->entity instanceof HasTreeStructureInterface) {
            $fields->add('parent_id');
        }
        return $this->fieldsCache[$cacheKey] = $fields;
    }

    public function displayColumns(): TabulatorColumnDtoCollection
    {
        return $this->displayColumns ??= $this->columns()
            ->filter(fn(TabulatorColumnDto $column) => !str_starts_with($column->field, '_'))
            ->values();
    }

    public function column(string $field): ?TabulatorColumnDto
    {
        $field = $this->fieldRenamer->rename($field);

        return $this->columns()->find(fn(TabulatorColumnDto $column) => $column->field === $field) ?: null;
    }

    public function immutableSorts(): TabulatorSortCollection
    {
        return new TabulatorSortCollection([
            new TabulatorSort(
                field: 'created',
                direction: SortDirection::DESC,
            ),
        ]);
    }

    public function filteredColumns(array $fields): TabulatorColumnDtoCollection
    {
        $fields = StringCollection::createFromArray($fields);

        return $this->columns()
            ->filter(fn(TabulatorColumnDto $column) => $fields->contains($column->field))
            ->values();
    }

    public function defaultFilterCollection(): TabulatorFilterCollection
    {
        return TabulatorFilterCollection::createFromArray($this->defaultFilters());
    }

    /**
     * @return TabulatorFilter[]
     */
    public function defaultFilters(): array
    {
        return [];
    }

    protected function getColumnGroup(string $title): TabulatorColumnGroup
    {
        if (!in_array($title, $this->columnGroups)) {
            $this->columnGroups[] = $title;
        }

        return new TabulatorColumnGroup(id: array_search($title, $this->columnGroups), title: $title);
    }
}
